Dana 2. veljače 2017. Ann Taylor napisala je članak 12 stvari koje trebamo vidjeti kod WordPress page buildera u 2017. + tko to već radi kako treba na CodeInWP-u. Page Builder Sandwich kontinuirano unaprjeđujemo već više od godinu dana: istražujemo, slušamo naše korisnike, pa smo mislili da će Page Builder Sandwich ući u CodeInWP-ov članak „Tko to radi kako treba”. Međutim, nismo uspjeli zbog performansi.
Pitali smo Ann o tome, a ona je odgovorila:
…Malo sam isprobavala besplatnu verziju. Djeluje vrlo obećavajuće: puno modula, prilično intuitivno sučelje, odlično WYSIWYG iskustvo, sve se lako povlači. Međutim, kada sam na stranici imala oko 10 modula, postalo je teško nastaviti raditi zbog niske brzine uređivanja…
Dakle, prema njoj, Page Builder Sandwich ima problema s performansama. A budući da uvijek slušamo povratne informacije, odlučili smo to provjeriti.
Testiranje performansi
Trebao nam je način za provjeru performansi PBS-a; budući da PBS uglavnom radi u pregledniku putem Javascripta, Chromeov Timeline profiler bio je pravi alat za taj posao. Timeline profiler može identificirati i izmjeriti koji se procesi odvijaju na web-stranici tijekom izvođenja, što je savršeno za pronalaženje uskih grla u brzini.
Zaključili smo da bismo najprije trebali testirati najjednostavnije korake – prelaženje mišem preko više elemenata i redaka. Jednostavno, zar ne? Bili smo uvjereni da nećemo naići na probleme s performansama s nečim tako jednostavnim. Nažalost, bili smo u krivu.
Nakon što smo pritisnuli gumb za snimanje, pomicali smo miša oko 2 sekunde po ekranu, a zatim pritisnuli gumb za zaustavljanje. Evo rezultirajuće vremenske trake iz profilera:
I gle čuda. Prepuna je uskih grla u izvedbi!
Na grafikonu se događa mnogo toga, no najočitiji problem je što na njemu ima jako puno ljubičastih područja (što ne bi smio biti slučaj). Povrh toga, ta ljubičasta područja su vidljivo vrlo široka i čini se da sva dosežu sam vrh grafikona. Također ćete primijetiti da je grafikon uvijek popunjen te da ljubičasta područja gotovo nikada ne padaju na nulu. Čini se kao da je preglednik uvijek zauzet nečim i da se gotovo nikada ne odmara.
Ali što sva ta ljubičasta područja zapravo znače?
Profiler nam je ljubazno rekao da se zovu reflows (ponovna iscrtavanja izgleda). Evo definicije što je reflow sa stranice Google Developer:
Reflow je naziv procesa web-preglednika za ponovno izračunavanje položaja i geometrije elemenata u dokumentu.. reflow je operacija koja blokira korisnika u pregledniku.
To znači da pomicanje miša unutar PBS-a pokreće mnoštvo reflowova. Što je još gore, svaki reflow trenutno pauzira preglednik kako bi mogao dovršiti svoje izračune. Više reflowova znači više pauza u pregledniku, a iskustvo postaje još više trzavo (janky). Jednostavnije rečeno, puno ljubičastih područja = loše.
Ispostavilo se da je Ann bila u pravu: mi uistinu imamo golemo usko grlo u performansama, pa se naš tim odmah bacio na posao kako bi to smjesta popravio.
Rješavanje našeg problema s reflowom i trzanjem
Prije svega, morali smo provesti istraživanje kako bismo otkrili što uopće uzrokuje ovaj reflow. Otkrili smo da, očigledno, mnoge uobičajene stvari u Javascriptu mogu navesti preglednik na pokretanje reflowa.
Gotovo svaka funkcija kojom možete dobiti širinu, visinu ili položaj DOM elementa može izazvati reflow. Paul Irish napisao je opsežan popis svih svojstava i funkcija u Javascriptu koje to mogu učiniti. Loša vijest je da Page Builder Sandwich koristi mnoga od tih svojstava i funkcija, a sve one uzrokuju reflow:
- elem.getBoundingClientRect()
- window.getComputedStyle()
- window.scrollY
- i još mnogo toga…
Rješenje #1: Predmemoriranje zahtjevnih reflow procesa
Najočitije rješenje je NE koristiti ta svojstva i funkcije. Međutim, PBS se uvelike oslanja na njih i bez njih ne možemo funkcionirati.
Rješenje koje nam preostaje jest srezati broj poziva tih funkcija što je više moguće. Težak dio bio je u tome što smo ta svojstva i funkcije koristili mnogo puta u različitim dijelovima PBS-a, a većinu vremena smo ih primjenjivali na istom DOM elementu više puta. Kako bismo to riješili, stvorili smo vlastiti mehanizam predmemoriranja za svako problematično svojstvo/funkciju.
Na primjer, funkcija getBoundingClientRect daje nam trenutne vrijednosti položaja vrha, lijevog ruba, širine, visine, desnog i donjeg ruba u pikselima za DOM element. Ovu funkciju stalno koristimo za izračunavanje lokacije na kojoj treba iscrtati obrube elemenata u PBS-u. Evo funkcije predmemoriranja koju smo za to stvorili:
// Cache: Ovdje spremamo client rects, možemo ih kasnije isprazniti.
var elemsWithBoundingRects = [];
window.pbsGetBoundingClientRect = function( element ) {
// Provjeri jesmo li već ranije dohvatili client rect.
if ( ! element._boundingClientRect ) {
// Ako nismo, dohvati ga i spremi za buduću upotrebu.
element._boundingClientRect = element.getBoundingClientRect();
elemsWithBoundingRects.push( element );
}
return element._boundingClientRect;
};
Umjesto pozivanja getBoundingClientRect , sada smo koristili pbsGetBoundingClientRect . Sada svaki put kada se zatraži client rect, spremamo tu vrijednost za buduće potrebe, čime ograničavamo broj okidača za reflow.
Također nam je trebao način za pražnjenje predmemorije kada pohranjeni pravokutnici postanu nevažeći. Vrijednosti pohranjenog client recta mijenjaju se kada korisnik pomiče stranicu ili dodaje/mijenja/uklanja elemente na zaslonu. Stoga smo stvorili funkciju za brisanje predmemorije koju možemo pozvati kad god zatrebamo svježe vrijednosti client recta:
var clearClientRects = function() {
var i;
for ( i = 0; i < elemsWithBoundingRects.length; i++ ) {
if ( elemsWithBoundingRects[ i ] ) {
elemsWithBoundingRects[ i ]._boundingClientRect = null;
}
}
elemsWithBoundingRects = [];
};
Također smo implementirali isti mehanizam predmemoriranja i za druge zahtjevne pozive funkcija koje inače koristimo.
Rješenje #2: Grupiranje operacija čitanja i pisanja pomoću biblioteke FastDom
Implementacija rješenja br. 1 poboljšala je performanse PBS-a, ali to nije bilo dovoljno.
Pretražujući dalje, naišli smo na članak koji je napisao Wilson Page o sprečavanju ‘layout thrashinga’. Wilson kaže:
Do ‘layout thrashinga’ (reflowa) dolazi kada JavaScript intenzivno uzastopno piše, a zatim čita iz DOM-a, više puta uzrokujući reflowove dokumenta.
Očito nije dovoljno samo ograničiti pozive specifičnim svojstvima i funkcijama koje pokreću reflow. Kada izvodite višestruka uzastopna „čitanja” i „pisanja” po DOM-u, to također izaziva reflow preglednika. Evo tipičnog scenarija izravno s Wilsonove stranice:
// Čitanje var h1 = element1.clientHeight; // Pisanje (poništava raspored) element1.style.height = (h1 * 2) + 'px'; // Čitanje (pokreće reflow) var h2 = element2.clientHeight; // Pisanje (poništava raspored) element2.style.height = (h2 * 2) + 'px'; // Čitanje (pokreće reflow) var h3 = element3.clientHeight; // Pisanje (poništava raspored) element3.style.height = (h3 * 2) + 'px'; // Reflow dokumenta na kraju okvira
Gornji primjer pokreće 3 reflowa jer se dimenzije cijele stranice poništavaju nakon pisanja. Predloženo rješenje za ovo jest preslagivanje koda kako bi se sva čitanja i pisanja grupirala zajedno tako da cijela stvar pokrene samo 1 reflow preglednika:
// Čitanje var h1 = element1.clientHeight; var h2 = element2.clientHeight; var h3 = element3.clientHeight; // Pisanje (poništava raspored) element1.style.height = (h1 * 2) + 'px'; element2.style.height = (h2 * 2) + 'px'; element3.style.height = (h3 * 2) + 'px'; // Reflow dokumenta na kraju okvira
Gornji isječak prikazuje rješavanje problema jednostavnim. No u stvarnosti se u PBS-u čitanje i pisanje stvari koje pokreću reflow događa na mnogo različitih mjesta i u mnogo različitih trenutaka tijekom životnog ciklusa PBS-a. Reorganizacija tisuća redaka Javascript koda jednostavno nije izvediva.
Srećom po nas, Wilson je u svom članku ponudio i rješenje. Stvorio je malu pomoćnu skriptu koja pomaže u grupiranju tih čitanja i pisanja, nazvanu FastDom.
Evo dijela koda iz dodatka Page Builder Sandwich koji koristi FastDom i također uključuje naše Rješenje #1 odozgo:
// Grupiraj čitanja iz drugih niti.
fastdom.measure( function() {
// Upotrijebi naš predmemorirani client rect.
var rect = window.pbsGetBoundingClientRect( elem );
// Grupiraj pisanja iz drugih niti.
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 ) );
Ono što FastDom radi jest to da grupira sve pozive fastdom.measure i sve pozive fastdom.mutate te ih izvršava asinkrono, učinkovito grupirajući sva čitanja i pisanja zajedno kako bi se smanjio broj reflowova u pregledniku.
Rješenje #3: Pristupanje objektima stilova je skupo
Uz pomoć jsPerfa otkrili smo i da su neke funkcije za dohvaćanje stila brže od drugih.
U PBS-u obično moramo dohvatiti trenutnu vrijednost određenog stila elementa. Na primjer, inače ovo koristimo za dohvaćanje gornje margine:
var styles = window.getComputedStyle( elem ); var marginTop = styles.marginTop;
No, otkrili smo da je ovaj način znatno brži:
var marginTop = elem.style.marginTop;
Međutim, elem.style.marginTop i getComputedStyle vrlo su različiti. Prvi nam daje samo ugrađeni (inline) stil elementa i ne uzima u obzir stilove iz CSS datoteka; a drugi uzima u obzir i ugrađene stilove i stilove iz CSS datoteka.
Štoviše, čak i kada je rezultat funkcije getComputedStyle bio predmemoriran, proces je i dalje bio vrlo spor. Kako se ispostavilo, sam pristup objektu stila predstavlja spori proces.
Srećom po nas, PBS dodaje inline stilove elementima, a budući da inline stilovi (većinu vremena) imaju prednost nad stilovima iz CSS tablica, bit će slučajeva u kojima je dohvaćanje vrijednosti inline stila sasvim dovoljno. Stoga, ako kombiniramo gornje dvije metode, možemo dobiti kod koji se izvodi znatno brže:
var marginTop = elem.style.marginTop;
if ( ! marginTop ) {
var styles = window.getComputedStyle( elem );
marginTop = styles.marginTop;
}
Rezultati poboljšanja koda
Naša tri gornja rješenja zahtijevala su mnogo istraživanja, testiranja i stvarne implementacije (te odgodila objavu značajke Fontova), ali na kraju se sve isplatilo.
Vratimo se na naš početni test pomicanja miša preko zaslona tijekom 2 sekunde. Evo stare, trzave vremenske trake:
A evo kako izgleda naša nova vremenska traka nakon poboljšanja performansi:
Kao što vidite, na grafikonu ima znatno manje ljubičastih dijelova. Primjetna je i razlika u količini procesa koje preglednik izvodi. Pravokutni elementi na dnu također su tanji, što znači da se procesi izvode brže nego prije.
Ako usporedite količinu visokih/širokih ljubičastih područja na starom i novom grafikonu, vidimo nevjerojatno povećanje performansi od 200%! Također bilježimo poboljšanja performansi u svim segmentima. Uopće nije loše!
Zaključci i naučene lekcije
Iskreno, nismo mislili da PBS ima problema s performansama prije nego što je Ann to spomenula. Uglavnom zato što smo koristili strojeve namijenjene razvoju softvera na kojima ta uska grla nisu bila toliko uočljiva. Gledajući unatrag, provjera performansi trebala je biti jedan od glavnih prioriteta. No barem je sada, od verzije 4.3, to riješeno.
Za one koji žele poboljšati performanse vlastitog WordPress dodatka, posebno ako također uvelike koristite Javascript, evo mojih zaključaka iz ovog iskustva:
- Ograničite/predmemorirajte pozive svojstvima/funkcijama koje pokreću reflow. Koristite ovo kao vodič: Što prisiljava layout/reflow. Sveobuhvatan popis.
- Grupirajte „čitanja” i „pisanja” DOM stilova. Koristite FastDom da to odradi umjesto vas.
- Koristite Chromeov Timeline profiler i osigurajte da imate što manje reflowova (ljubičastih područja).