Lo scorso 2 febbraio 2017, Ann Taylor ha scritto l’articolo 12 Things We Need to See From WordPress Page Builders in 2017 + Who Already Gets It Right su CodeInWP. Lavoriamo al continuo miglioramento di Page Builder Sandwich da oltre un anno: facciamo ricerche, ascoltiamo i nostri clienti, quindi pensavamo che Page Builder Sandwich sarebbe entrato nell’articolo di CodeInWP dedicato a “chi ha fatto centro”. Invece non è stato così, a causa delle prestazioni. 
Così abbiamo chiesto chiarimenti ad Ann, e lei ha risposto:
…Ho provato un po’ la versione gratuita. È molto promettente: tanti moduli, interfaccia piuttosto intuitiva, un’ottima esperienza WYSIWYG, tutto si trascina facilmente. Tuttavia, quando avevo circa 10 moduli sulla pagina, è diventato difficile continuare a lavorarci a causa della bassa velocità di modifica…
Quindi, secondo lei, Page Builder Sandwich presenta problemi di prestazioni. E dato che ascoltiamo sempre i feedback, abbiamo deciso di verificare.
Testare le prestazioni
Ci serviva un modo per verificare le prestazioni di PBS; dato che PBS viene eseguito principalmente nel browser tramite Javascript, il profiler Timeline di Chrome era lo strumento giusto per questo lavoro. Il profiler della timeline è in grado di identificare e misurare quali processi sono in corso in una pagina web durante il runtime, il che è perfetto per individuare i colli di bottiglia delle prestazioni.
Abbiamo pensato di testare prima il passaggio più semplice in assoluto: passare il mouse su più elementi e righe. Semplice, vero? Eravamo sicuri che non avremmo riscontrato problemi di prestazioni con un’operazione così basilare. Purtroppo ci sbagliavamo.
Una volta premuto il pulsante di registrazione, abbiamo mosso il mouse sullo schermo per circa 2 secondi, quindi abbiamo premuto il pulsante di stop. Ecco la timeline risultante dal profiler:
Ed ecco la sorpresa: è colmo di colli di bottiglia delle prestazioni!
Nel grafico succedono molte cose, ma il problema più evidente è la presenza di molte aree viola (cosa che non dovrebbe accadere). Inoltre, queste aree viola sono visibilmente molto ampie e sembrano raggiungere tutte il picco del grafico. Noterete anche che il grafico è costantemente saturo e che le aree viola non scendono quasi mai a zero. Sembra che il browser sia sempre occupato a fare qualcosa e che non riposi quasi mai.
Ma cosa significano davvero tutte quelle aree viola?
Il profiler è stato così gentile da indicarci che si chiamano reflow. Ecco la definizione di cosa sia un reflow dal sito Google Developer:
Il reflow è il nome del processo del browser web per il ricalcolo delle posizioni e delle geometrie degli elementi nel documento… il reflow è un’operazione che blocca l’utente nel browser.
Ciò significa che muovere il mouse in PBS innesca una marea di reflow. La cosa peggiore è che ogni reflow mette temporaneamente in pausa il browser affinché possa completare i propri calcoli. Più reflow significano più pause del browser, rendendo l’esperienza ancora più scattosa (janky). In parole povere, molte aree viola = male.
Si è scoperto quindi che Ann aveva ragione: avevamo davvero un enorme collo di bottiglia nelle prestazioni, quindi il nostro team si è messo subito al lavoro per risolverlo tempestivamente.
Risolvere il nostro problema di reflow / jank
Prima di ogni altra cosa, dovevamo fare delle ricerche per scoprire cosa causasse questo reflow in primo luogo. Abbiamo scoperto che, a quanto pare, molte operazioni comuni in Javascript possono indurre il browser a causare un reflow.
Quasi tutte le funzioni utilizzabili per ottenere larghezza, altezza o posizione di un elemento DOM possono innescare un reflow. Paul Irish ha stilato un elenco dettagliato di tutte le proprietà e funzioni in Javascript in grado di farlo. La cattiva notizia è che Page Builder Sandwich fa largo uso di queste proprietà e funzioni, le quali causano tutte il reflow:
- elem.getBoundingClientRect()
- window.getComputedStyle()
- window.scrollY
- e molte altre…
Soluzione #1: Caching dei processi di reflow onerosi
La soluzione più ovvia sarebbe NON utilizzare quelle proprietà e funzioni. Tuttavia, PBS fa grande affidamento su di esse e non possiamo farne a meno.
L’unica soluzione rimasta consisteva nel ridurre il più possibile le chiamate a tali funzioni. La difficoltà stava nel fatto che usavamo quelle proprietà e funzioni innumerevoli volte in diverse parti di PBS e, nella maggior parte dei casi, le applicavamo più volte allo stesso elemento DOM. Per risolvere questo problema, abbiamo creato il nostro meccanismo di caching per ciascuna proprietà/funzione problematica.
Ad esempio, la funzione getBoundingClientRect ci fornisce le misurazioni attuali in pixel di top, left, width, height, right e bottom di un elemento DOM. Usiamo questa funzione continuamente per calcolare la posizione in cui tracciare i contorni degli elementi in PBS. Ecco la funzione di caching che abbiamo creato per questo:
// Cache: i nostri client rect sono conservati qui, possiamo usarlo per cancellarli in seguito.
var elemsWithBoundingRects = [];
window.pbsGetBoundingClientRect = function( element ) {
// Controlla se abbiamo già ottenuto il client rect in precedenza.
if ( ! element._boundingClientRect ) {
// In caso contrario, ottienilo e memorizzalo per un uso futuro.
element._boundingClientRect = element.getBoundingClientRect();
elemsWithBoundingRects.push( element );
}
return element._boundingClientRect;
};
Invece di chiamare getBoundingClientRect, ora usiamo pbsGetBoundingClientRect. In questo modo, ogni volta che viene richiesto il client rect, ne memorizziamo il valore per il futuro, limitando così il numero di trigger di reflow.
Ci serviva anche un modo per svuotare la cache quando i client rect memorizzati non erano più validi. I valori del client rect memorizzato cambiano quando l’utente scorre la pagina o aggiunge/modifica/rimuove elementi sullo schermo. Abbiamo quindi creato una funzione di svuotamento della cache richiamabile ogni volta che abbiamo bisogno di valori freschi per i client rect:
var clearClientRects = function() {
var i;
for ( i = 0; i < elemsWithBoundingRects.length; i++ ) {
if ( elemsWithBoundingRects[ i ] ) {
elemsWithBoundingRects[ i ]._boundingClientRect = null;
}
}
elemsWithBoundingRects = [];
};
Abbiamo applicato questo stesso meccanismo di caching anche ad altre chiamate di funzioni onerose che utilizziamo abitualmente.
Soluzione #2: Raggruppare letture e scritture utilizzando FastDom
L’implementazione della soluzione #1 ha migliorato le prestazioni di PBS, ma non era ancora abbastanza.
Cercando ancora su Google, ci siamo imbattuti in un articolo di Wilson Page sulla prevenzione del “layout thrashing”. Wilson afferma:
Il Layout Thrashing (reflow) si verifica quando JavaScript scrive e legge violentemente e ripetutamente dal DOM, provocando molteplici reflow del documento.
A quanto pare, non basta limitare le chiamate a specifiche proprietà e funzioni che innescano il reflow. L’esecuzione di molteplici “letture” e “scritture” consecutive sul DOM scatena a sua volta il reflow del browser. Ecco uno scenario tipico, tratto direttamente dal sito di Wilson:
// Lettura
var h1 = element1.clientHeight;
// Scrittura (invalida il layout)
element1.style.height = (h1 * 2) + 'px';
// Lettura (innesca il reflow)
var h2 = element2.clientHeight;
// Scrittura (invalida il layout)
element2.style.height = (h2 * 2) + 'px';
// Lettura (innesca il reflow)
var h3 = element3.clientHeight;
// Scrittura (invalida il layout)
element3.style.height = (h3 * 2) + 'px';
// Reflow del documento alla fine del frame
L’esempio sopra innesca 3 reflow perché le dimensioni dell’intera pagina vengono invalidate dopo ogni scrittura. La soluzione suggerita consiste nel riordinare il codice per raggruppare tutte le letture e le scritture insieme, in modo che l’intero blocco provochi solo 1 reflow nel browser:
// Lettura
var h1 = element1.clientHeight;
var h2 = element2.clientHeight;
var h3 = element3.clientHeight;
// Scrittura (invalida il layout)
element1.style.height = (h1 * 2) + 'px';
element2.style.height = (h2 * 2) + 'px';
element3.style.height = (h3 * 2) + 'px';
// Reflow del documento alla fine del frame
Lo snippet qui sopra fa sembrare semplice la risoluzione del problema. Nella realtà, però, in PBS la lettura e la scrittura di elementi che innescano il reflow avvengono in moltissimi punti diversi e in momenti differenti del ciclo di vita dell’applicazione. Riorganizzare migliaia di righe di codice Javascript non era fattibile.
Fortunatamente per noi, Wilson ha fornito una soluzione anche nel suo articolo. Ha creato un piccolo script di utilità per facilitare il raggruppamento di tali letture e scritture, chiamato FastDom.
Ecco un estratto di codice da Page Builder Sandwich che utilizza FastDom e include anche la nostra Soluzione #1:
// Raggruppa le letture dagli altri thread.
fastdom.measure( function() {
// Usa il nostro client rect in cache.
var rect = window.pbsGetBoundingClientRect( elem );
// Raggruppa le scritture dagli altri thread.
fastdom.mutate( function() {
elem.style.top = ( rect.top + window.pbsScrollY() ) + 'px';
elem.style.height = rect.height + 'px';
elem.style.left = rect.left + 'px';
elem.style.width = rect.width + 'px';
}.bind( this ) );
}.bind( this ) );
Ciò che fa FastDom è raggruppare tutte le chiamate fastdom.measure e tutte le chiamate fastdom.mutate, eseguendole in modo asincrono, raggruppando di fatto tutte le letture e le scritture per ridurre drasticamente i reflow nel browser.
Soluzione #3: L’accesso agli oggetti di stile è oneroso
Grazie a jsPerf abbiamo anche scoperto che alcune funzioni per ottenere gli stili erano più veloci di altre.
In PBS, abbiamo spesso bisogno di ottenere il valore corrente di uno stile specifico di un elemento. Ad esempio, normalmente usiamo questo per ottenere il margine superiore:
var styles = window.getComputedStyle( elem );
var marginTop = styles.marginTop;
Abbiamo tuttavia scoperto che fare questo è decisamente più veloce:
var marginTop = elem.style.marginTop;
Tuttavia, elem.style.marginTop e getComputedStyle sono molto diversi. Il primo restituisce solo lo stile inline dell’elemento e non tiene conto di eventuali fogli di stile; il secondo invece tiene conto sia degli stili inline che dei fogli di stile.
Inoltre, anche memorizzando nella cache il risultato di getComputedStyle, l’operazione risultava comunque molto lenta. A quanto pare, è l’accesso all’oggetto di stile in sé a costituire il processo lento.
Per nostra fortuna, PBS aggiunge stili inline agli elementi e, poiché gli stili inline hanno la precedenza (la maggior parte delle volte) sui fogli di stile, ci saranno casi in cui recuperare solo il valore dello stile inline sarà sufficiente. Combinando i due metodi descritti, possiamo quindi ottenere un codice dall’esecuzione molto più rapida:
var marginTop = elem.style.marginTop;
if ( ! marginTop ) {
var styles = window.getComputedStyle( elem );
marginTop = styles.marginTop;
}
I risultati delle ottimizzazioni del codice
Le nostre 3 soluzioni hanno richiesto molta ricerca, test e lavoro di implementazione effettiva (ritardando il rilascio della funzionalità Fonts), ma alla fine ne è valsa assolutamente la pena.
Tornando al nostro test iniziale di muovere il mouse sullo schermo per 2 secondi: ecco la vecchia timeline, poco efficiente e scattosa:
Ed ecco come appare la nostra nuova timeline dopo i miglioramenti prestazionali:
Come potete vedere, ci sono molte meno sezioni viola nel grafico. E c’è una netta differenza nella quantità di processi eseguiti dal browser. Anche i blocchi rettangolari in basso sono più sottili, il che significa che i processi vengono eseguiti più rapidamente rispetto a prima.
Confrontando l’entità delle aree viola alte e larghe tra il vecchio e il nuovo grafico, ci troviamo di fronte a un incredibile aumento delle prestazioni del 200%! Stiamo inoltre riscontrando miglioramenti complessivi su tutti i fronti. Niente male davvero!
Conclusioni e insegnamenti
In tutta onestà, non pensavamo che PBS avesse problemi di prestazioni prima che Ann lo facesse notare. Soprattutto perché utilizzavamo macchine dedicate allo sviluppo, dove i colli di bottiglia non erano così evidenti. Col senno di poi, verificare le prestazioni avrebbe dovuto essere una delle priorità assolute. Ma almeno ora, a partire dalla versione 4.3, la questione è stata risolta. 
Per chi desidera migliorare le prestazioni del proprio plugin WordPress, specialmente se fa uso intensivo di Javascript, ecco le mie considerazioni maturate da questa esperienza:
- Limita/memorizza nella cache le chiamate a proprietà e funzioni che innescano il reflow. Usa questa guida di riferimento: What forces layout/reflow. The comprehensive list.
- Raggruppa le “letture” e le “scritture” degli stili DOM. Usa FastDom per farlo al posto tuo.
- Usa il profiler Timeline di Chrome e assicurati di avere il minor numero possibile di reflow (aree viola).