You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

12 KiB

id title permalink prev
advanced-performance-it-IT Performance Avanzata docs/advanced-performance-it-IT.html perf-it-IT.html

Una tra le prime domande che la gente si pone quando considera React per un progetto è se l'applicazione sarà altrettanto veloce e scattante di una versione equivalente non basata su React. L'idea di ripetere il rendering di un intero sottoalbero di componenti in risposta a ciascun cambiamento dello stato rende la gente curiosa se questo processo influisce negativamente sulle prestazioni. React utilizza diverse tecniche intelligenti per minimizzare il numero di operazioni costose sul DOM richieste dall'aggiornamento della UI.

Evitare di riconciliare il DOM

React fa uso di un DOM virtuale, che è un descrittore di un sottoalbero DOM visualizzato nel browser. Questa rappresentazione parallela permette a React di evitare di creare nodi DOM e accedere nodi esistenti, che è di gran lunga più lento di operazioni su oggetti JavaScript. Quando le proprietà di un componente o il suo stato cambiano, React decide se un'aggiornamento effettivo del DOM sia necessario costruendo un nuovo virtual DOM e confrontandolo con quello vecchio. Solo nel caso in cui non siano uguali, React riconcilierà il DOM, applicando il minor numero di mutamenti possibile.

In aggiunta a questo, React offre una funzione per il ciclo di vita del componente, shouldComponentUpdate, che viene scatenata prima che il processo di ri-rendering cominci (il confronto del DOM virtuale e una possibile eventuale riconciliazione del DOM), dando allo sviluppatore la possibilità di cortocircuitare questo processo. L'implementazione predefinita di questa funzione restituisce true, lasciando che React effettui l'aggiornamento:

shouldComponentUpdate: function(nextProps, nextState) {
  return true;
}

Tieni in mente che React invocherà questa funzione abbastanza spesso, quindi l'implementazione deve essere veloce.

Supponiamo che hai un'applicazione di messaggistica con parecchi thread di conversazioni. Supponi che solo uno dei thread sia cambiato. Se implementassimo shouldComponentUpdate sul componente ChatThread, React potrebbe saltare la fase di rendering per gli altri thread:

shouldComponentUpdate: function(nextProps, nextState) {
  // TODO: restituisci true se il thread attuale è diverso
  // da quello precedente.
}

Quindi, riassumendo, React evita di effettuare operazioni costose sul DOM richieste a riconciliare sottoalberi del DOM, permettendo all'utente di cortocircuitare il processo usando shouldComponentUpdate, e, per i casi in cui si debba aggiornare, confrontando i DOM virtuali.

shouldComponentUpdate in azione

Ecco un sottoalbero di componenti. Per ciascuno di essi viene indicato cosa shouldComponentUpdate ha restituito e se i DOM virtuali siano equivalenti o meno. Infine, il colore del cerchio indica se il componente sia stato riconciliato o meno.

Nell'esempio precedente, dal momento che shouldComponentUpdate ha restituito false per il sottoalbero di radice C2, React non ha avuto bisogno di generare il nuovo DOM virtuale, e quindi non ha nemmeno avuto bisogno di riconciliare il DOM. Nota che React non ha nemmeno avuto bisogno di invocare shouldComponentUpdate su C4 e C5.

Per C1 e C3, shouldComponentUpdate ha restituito true, quindi React è dovuto scendere giù fino alle foglie e controllarle. Per C6 ha restituito true; dal momento che i DOM virtuali non erano equivalenti, ha dovuto riconciliare il DOM. L'ultimo caso interessante è C8. Per questo nodo React ha dovuto calcolare il DOM virtuale, ma dal momento che era uguale al vecchio, non ha dovuto riconciliare il suo DOM.

Nota che React ha dovuto effettuare mutazioni del DOM soltanto per C6, che era inevitabile. Per C8, lo ha evitato confrontando i DOM virtuali, e per il sottoalbero di C2 e C7, non ha neppure dovuto calcolare il DOM virtuale in quanto è stato esonerato da shouldComponentUpdate.

Quindi, come dovremmo implementare shouldComponentUpdate? Supponiamo di avere un componente che visualizza soltanto un valore stringa:

React.createClass({
  propTypes: {
    value: React.PropTypes.string.isRequired
  },

  render: function() {
    return <div>{this.props.value}</div>;
  }
});

Potremmo facilmente implementare shouldComponentUpdate come segue:

shouldComponentUpdate: function(nextProps, nextState) {
  return this.props.value !== nextProps.value;
}

Finora tutto a posto, maneggiare queste semplici strutture proprietà e stato è molto facile. Potremmo anche generalizzare un'implementazione basata sull'uguaglianza superficiale e farne il mix dentro i componenti. Infatti, React fornisce già una tale implementazione: PureRenderMixin.

Ma che succede se le proprietà o lo stato del tuo componente sono strutture dati mutevoli? Supponiamo che la proprietà che il componente riceve sia, anziché una stringa come 'bar', un oggetto JavaScript che contiene una stringa, come { foo: 'bar' }:

React.createClass({
  propTypes: {
    value: React.PropTypes.object.isRequired
  },

  render: function() {
    return <div>{this.props.value.foo}</div>;
  }
});

L'implementazione di shouldComponentUpdate che avevamo prima non funzionerebbe sempre come ci aspettiamo:

// assumiamo che this.props.value sia { foo: 'bar' }
// assumiamo che nextProps.value sia { foo: 'bar' },
// ma questo riferimento è diverso da this.props.value
this.props.value !== nextProps.value; // true

Il problema è che shouldComponentUpdate restituirà true quando la proprietà non è in realtà cambiata. Per risolvere questo problema, potremmo proporre questa implementazione alternativa:

shouldComponentUpdate: function(nextProps, nextState) {
  return this.props.value.foo !== nextProps.value.foo;
}

In breve, abbiamo finito per effettuare un confronto in profondità per assicurarci di accorgerci correttamente dei cambiamenti. In termini di prestazioni, questo approccio è molto costoso. Non scala in quanto dovremmo scrivere codice diverso per valutare l'uguaglianza in profondità per ciascun modello. Inoltre, potrebbe anche non funzionare per nulla se non gestiamo correttamente i riferimenti agli oggetti. Supponiamo che il componente sia usato da un genitore:

React.createClass({
  getInitialState: function() {
    return { value: { foo: 'bar' } };
  },

  onClick: function() {
    var value = this.state.value;
    value.foo += 'bar'; // ANTI-PATTERN!
    this.setState({ value: value });
  },

  render: function() {
    return (
      <div>
        <InnerComponent value={this.state.value} />
        <a onClick={this.onClick}>Click me</a>
      </div>
    );
  }
});

La prima volta che viene effettuato il rendering del componente interno, la sua proprietà value avrà il valore { foo: 'bar' }. Se l'utente clicca l'ancora, lo stato del componente genitore sarà aggiornato a { value: { foo: 'barbar' } }, scatenando il processo di ri-rendering sul componente interno, il quale riceverà { foo: 'barbar' } come il nuovo valore della proprietà.

Il problema è che, dal momento che il genitore e il componente interno condividono un riferimento allo stesso oggetto, quando l'oggetto viene modificato nella riga 2 della funzione onClick, la proprietà che il componente interno possedeva cambierà anch'essa. Quindi, quando il processo di ri-rendering inizia, e shouldComponentUpdate viene invocato, this.props.value.foo sarà uguale a nextProps.value.foo, perché infatti, this.props.value si riferisce allo stesso oggetto di nextProps.value.

Di conseguenza, dal momento che non ci accorgiamo del cambiamento della proprietà e cortocircuitiamo il processo di ri-rendering, la UI non sarà aggiornata da 'bar' a 'barbar'.

Immutable-js viene in nostro soccorso

Immutable-js è una libreria di collezioni JavaScript scritta da Lee Byron, che Facebook ha recentemente rilasciato come open source. Fornisce collezioni immutabili e persistenti attraverso condivisione strutturale. Vediamo cosa significano queste proprietà:

  • Immutabile: una volta creata, una collezione non può essere alterata in un momento successivo.
  • Persistente: nuove collezioni possono essere create da una collezione precedente e una mutazione come un assegnamento. La collezione originale è ancora valida dopo che la nuova collezione è stata creata.
  • Condivisione Strutturale: nuove collezioni sono create riutilizzando quanto più possibile della stessa struttura della collezione originale, riducendo le operazioni di copia al minimo, per ottenere efficienza spaziale e prestazioni accettabili. Se la nuova collezione è identica all'originale, l'originale è spesso restituita.

L'immutabilità ci permette di tenere traccia dei cambiamenti in modo economico; un cambiamento risulterà sempre in un nuovo oggetto, quindi dobbiamo soltanto controllare se il riferimento all'oggeto sia cambiato. Ad esempio, in questo codice regolare JavaScript:

var x = { foo: "bar" };
var y = x;
y.foo = "baz";
x === y; // true

Sebbene y sia stato modificato, dal momento che si tratta di un riferimento allo stesso oggetto di x, questo confronto restituisce true. Tuttavia, questo codice potrebbe essere scritto usando immutable-js come segue:

var SomeRecord = Immutable.Record({ foo: null });
var x = new SomeRecord({ foo: 'bar'  });
var y = x.set('foo', 'baz');
x === y; // false

In questo caso, poiché un nuovo riferimento è restituito quando si modifica x, possiamo assumere in tutta sicurezza che x sia cambiato.

Un'altra maniera possibile di tener traccia dei cambiamenti potrebbe essere il dirty checking, ovvero usare un flag impostato dai metodi setter. Un problema con questo approccio è che ti forza ad usare i setter e scrivere un sacco di codice aggiuntivo, oppure instrumentare in qualche modo le tue classi. In alternativa, puoi effettuare una copia profonda dell'oggetto immediatamente prima della mutazione ed effettuare un confronto in profondità per determinare se vi è stato un cambiamento oppure no. Un problema con questo approccio è che sia deepCopy che deepCompare sono operazioni costose.

Quindi, le strutture dati Immutable ti forniscono una maniera economica e concisa di osservare i cambiamenti degli oggetti, che è tutto ciò che ci serve per implementare shouldComponentUpdate. Pertanto, se modelliamo gli attributi delle proprietà e dello stato usando le astrazioni fornite da immutable-js saremo in grado di usare PureRenderMixin e ottenere un grande aumento di prestazioni.

Immutable-js e Flux

Se stai usando Flux, dovresti cominciare a scrivere i tuoi store usando immutable-js. Dài un'occhiata alla API completa.

Vediamo una delle possibili maniere di modellare l'esempio dei thread usando strutture dati Immutable. Anzitutto, dobbiamo definire un Record per ciascuna delle entità che desideriamo modellare. I Record sono semplicemente contenitori immutabili che contengono valori per un insieme specifico di campi:

var User = Immutable.Record({
  id: undefined,
  name: undefined,
  email: undefined
});

var Message = Immutable.Record({
  timestamp: new Date(),
  sender: undefined,
  text: ''
});

La funzione Record riceve un oggetto che definisce i campi che l'oggetto possiede e i loro valori predefiniti.

Lo store dei messaggi potrebbe tenere traccia degli utenti e dei messaggi usando due liste:

this.users = Immutable.List();
this.messages = Immutable.List();

Dovrebbe essere abbastanza banale implementare funzioni che gestiscono ciascun tipo di payload. Ad esempio, quando lo store vede un payload che rappresenta un messaggio, possiamo semplicemente creare un nuovo record e metterlo in coda alla lista di messaggi:

this.messages = this.messages.push(new Message({
  timestamp: payload.timestamp,
  sender: payload.sender,
  text: payload.text
});

Nota che dal momento che le strutture dati sono immutabili, dobbiamo assegnare il valore di ritorno del metodo push a this.messages.

Dal punto di vista di React, se usiamo strutture dati immutable-js anche per contenere lo stato del componente, possiamo fare il mix di PureRenderMixin in tutti i nostri componenti e cortocircuitare il processo di ri-rendering.