Hogyan növeltük bővítményünk teljesítményét 200%-kal

2017. február 2-án Ann Taylor megírta a 12 Things We Need to See From WordPress Page Builders in 2017 + Who Already Gets It Right című cikket a CodeInWP-n. Már több mint egy éve folyamatosan fejlesztjük a Page Builder Sandwichet: kutatunk, figyelünk az ügyfeleinkre, így azt gondoltuk, hogy a Page Builder Sandwich bekerül a CodeInWP „Who Got it Right” cikkébe. A teljesítmény miatt azonban lemaradtunk róla. ?

Így hát megkérdeztük erről Annt, és ő ezt válaszolta:

…Kipróbáltam egy kicsit az ingyenes verziót. Nagyon ígéretes: rengeteg modul, meglehetősen intuitív kezelőfelület, nagyszerű WYSIWYG élmény, mindent könnyű mozgatni. Amikor azonban már körülbelül 10 modul volt az oldalon, az alacsony szerkesztési sebesség miatt nehézkessé vált a munka folytatása…

Szóval szerinte a Page Builder Sandwichnek teljesítményproblémái vannak. És mivel mindig odafigyelünk a visszajelzésekre, utánajártunk a dolognak.

A teljesítmény tesztelése

Szükségünk volt egy módszerre a PBS teljesítményének ellenőrzésére; mivel a PBS többnyire a böngészőben fut JavaScripten keresztül, a Chrome Timeline profilozója volt a legmegfelelőbb eszköz erre a feladatra. Az idővonal-profilozó képes azonosítani és megmérni, hogy milyen folyamatok zajlanak egy weboldalon futás közben, ami tökéletes a sebességbeli szűk keresztmetszetek felderítésére.

Úgy gondoltuk, hogy először a legegyszerűbb lépéseket kellene tesztelnünk – végighúzni az egeret több elemen és soron. Egyszerű, nemde? Biztosak voltunk benne, hogy egy ilyen egyszerű dologgal nem ütközünk teljesítményproblémákba. Sajnos tévedtünk.

Miután rákattintottunk a felvétel gombra, körülbelül 2 másodpercig mozgattuk az egeret a képernyőn, majd megnyomtuk a leállítás gombot. Íme az eredményül kapott idővonal a profilozóból:

És láss csodát: tele van teljesítménybeli szűk keresztmetszetekkel!

Rengeteg dolog történik a grafikonon, de a legnyilvánvalóbatlanabb probléma az, hogy tele van lila területekkel (aminek nem kellene így lennie). Ráadásul ezek a lila területek láthatóan rendkívül szélesek, és úgy tűnik, mindegyik eléri a grafikon csúcsát. Azt is észreveheti, hogy a grafikon szinte mindig teljesen tele van, és a lila területek szinte sosem csökkennek nullára. Úgy tűnik, a böngésző mindig el van foglalva valamivel, és szinte sosem pihen.

De mit is jelentenek valójában ezek a lila területek?

A profilozó volt olyan kedves, és elárulta nekünk, hogy ezeket reflow-knak (újratördeléseknek) hívják. Íme a reflow definíciója a Google Fejlesztői oldaláról:

A reflow annak a webböngésző-folyamatnak a neve, amely újraszámolja a dokumentumban lévő elemek pozícióját és geometriáját.. a reflow a felhasználói műveleteket blokkoló folyamat a böngészőben.

Ez tehát azt jelenti, hogy az egér mozgatása a PBS-ben egy rakás reflow-t vált ki. Ami még rosszabb, hogy minden egyes reflow pillanatokra megakasztja a böngészőt, hogy befejezhesse a számításait. A több reflow több böngésző-megakadást jelent, az élmény pedig még szaggatottabbá válik. Egyszerűbben fogalmazva: a sok lila terület = rossz.

Kiderült tehát, hogy Ann-nek igaza volt: valóban hatalmas teljesítménybeli szűk keresztmetszettel rendelkezünk, így csapatunk azonnal munkához látott, hogy kijavítsa a hibát.

A reflow / szaggatási probléma megoldása

Mindenekelőtt kutatást kellett végeznünk, hogy kiderítsük, mi okozza ezt a reflow-t egyáltalán. Rájöttünk, hogy láthatóan számos gyakori JavaScript-művelet késztetheti a böngészőt reflow-ra.

Szinte bármelyik függvény, amellyel egy DOM-elem szélességét, magasságát vagy pozícióját kérdezzük le, reflow-t indíthat el. Paul Irish egy részletes listát írt az összes olyan JavaScript-tulajdonságról és függvényről, amely ezt eredményezheti. A rossz hír az, hogy a Page Builder Sandwich rengeteget használ ezekből a tulajdonságokból és függvényekből, és ezek mindegyike reflow-t okoz:

  • elem.getBoundingClientRect()
  • window.getComputedStyle()
  • window.scrollY
  • és még sok más…

1. javítás: A költséges reflow-folyamatok gyorsítótárazása

A legkézenfekvőbb megoldás az lenne, ha NEM használnánk ezeket a tulajdonságokat és függvényeket. A PBS azonban nagymértékben támaszkodik rájuk, és nem tudunk nélkülük működni.

Az egyetlen megmaradt megoldásunk az volt, hogy a lehető legjobban lecsökkentsük az ezekre a függvényekre irányuló hívások számát. A nehézséget az okozta, hogy ezeket a tulajdonságokat és függvényeket nagyon sokszor használtuk a PBS különböző részein, és legtöbbször ugyanazon a DOM-elemen többször is. Ennek orvoslására létrehoztuk a saját gyorsítótárazási mechanizmusunkat minden problematikus tulajdonsághoz/függvényhez.

Például a getBoundingClientRect függvény megadja egy DOM-elem aktuális felső, bal oldali, szélességi, magassági, jobb oldali és alsó képpontméreteit. Ezt a függvényt folyamatosan használjuk annak a pozíciónak a kiszámításához, ahová az elemek körvonalait rajzoljuk a PBS-ben. Íme az erre létrehozott gyorsítótárazó függvényünk:

// Gyorsítótár: Itt tároljuk a client rect-eket, ezt használhatjuk a későbbi törlésükhöz.
var elemsWithBoundingRects = [];

window.pbsGetBoundingClientRect = function( element ) {

	// Ellenőrizzük, hogy korábban lekértük-e már a client rectet.
	if ( ! element._boundingClientRect ) {

		// Ha nem, lekérjük, majd elmentjük későbbi használatra.
		element._boundingClientRect = element.getBoundingClientRect();
		elemsWithBoundingRects.push( element );
	}
	return element._boundingClientRect;
};
​

A getBoundingClientRect közvetlen meghívása helyett mostantól a pbsGetBoundingClientRect függvényt használtuk. Így minden alkalommal, amikor a client rect lekérdezésre kerül, eltároljuk ezt az értéket későbbi hivatkozás céljából, ezáltal korlátozva a reflow-k kiváltásának számát.

Szükségünk volt egy módszerre a gyorsítótár ürítésére is, amikor a tárolt client rect-ek érvénytelenné válnak. A tárolt client rect értékei megváltoznak, amikor a felhasználó görgeti az oldalt, vagy elemeket ad hozzá/módosít/távolít el a képernyőn. Ezért létrehoztunk egy gyorsítótár-törlő függvényt, amelyet bármikor meghívhatunk, ha friss client rect értékekre van szükségünk:

var clearClientRects = function() {
	var i;
	for ( i = 0; i < elemsWithBoundingRects.length; i++ ) {
		if ( elemsWithBoundingRects[ i ] ) {
			elemsWithBoundingRects[ i ]._boundingClientRect = null;
		}
	}
	elemsWithBoundingRects = [];
};​

Ugyanezt a gyorsítótárazási mechanizmust alkalmaztuk a többi gyakran használt, költséges függvényhívásunkra is.

2. javítás: Az olvasások és írások csoportosítása a FastDom segítségével

Az 1. javítás bevezetése növelte a PBS teljesítményét, de még nem volt elegendő.

Tovább keresgélve a Google-ben rábukkantunk Wilson Page cikkére a layout thrashing megelőzéséről. Wilson ezt írja:

A Layout Thrashing (reflow) akkor következik be, amikor a JavaScript drasztikusan ír a DOM-ba, majd olvas belőle egymás után többször is, ami dokumentum-reflow-kat okoz.

Úgy tűnik, nem elég csupán korlátozni a konkrét reflow-t kiváltó tulajdonságok és függvények meghívását. Amikor egymás után több „olvasást” és „írást” hajt végre a DOM-on, az szintén böngésző-reflow-t vált ki. Íme egy tipikus forgatókönyv, közvetlenül Wilson oldaláról:

// Olvasás
var h1 = element1.clientHeight;

// Írás (érvényteleníti az elrendezést)
element1.style.height = (h1 * 2) + 'px';

// Olvasás (reflow-t vált ki)
var h2 = element2.clientHeight;

// Írás (érvényteleníti az elrendezést)
element2.style.height = (h2 * 2) + 'px';

// Olvasás (reflow-t vált ki)
var h3 = element3.clientHeight;

// Írás (érvényteleníti az elrendezést)
element3.style.height = (h3 * 2) + 'px';

// Dokumentum-reflow a képkocka végén​

A fenti példa 3 reflow-t vált ki, mivel a teljes oldal méretei érvénytelenné válnak egy írás után. Erre a javasolt javítás a kód átrendezése úgy, hogy egy csoportba vonjuk az összes olvasást és írást, így az egész folyamat mindössze 1 böngésző-reflow-t indít el:

// Olvasás
var h1 = element1.clientHeight;
var h2 = element2.clientHeight;
var h3 = element3.clientHeight;

// Írás (érvényteleníti az elrendezést)
element1.style.height = (h1 * 2) + 'px';
element2.style.height = (h2 * 2) + 'px';
element3.style.height = (h3 * 2) + 'px';

// Dokumentum-reflow a képkocka végén​

A fenti kódrészlet alapján a probléma megoldása egyszerűnek tűnhet. A valóságban azonban a PBS-ben a reflow-t kiváltó adatok olvasása és írása rengeteg különböző helyen és a PBS életciklusa során számos különböző időpontban történik. Több ezer sornyi JavaScript-kód átrendezése nem volt kivitelezhető.

Szerencsénkre Wilson megoldást is javasolt a cikkében. Készített egy kis segédszkriptet az olvasások és írások csoportosításának megkönnyítésére, amely a FastDom nevet viseli.

Íme egy kódrészlet a Page Builder Sandwichből, amely a FastDomot használja, és magában foglalja a fenti 1. javításunkat is:

// Összegyűjtjük a más szálakból származó olvasásokat.
fastdom.measure( function() {

	// A gyorsítótárazott client rect használata.
	var rect = window.pbsGetBoundingClientRect( elem );

	// Összegyűjtjük a más szálakból származó írásokat.
	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 ) );​

A FastDom lényege, hogy egybefogja az összes fastdom.measure és fastdom.mutate hívást, és aszinkron módon hajtja végre őket, hatékonyan csoportosítva az összes olvasást és írást a böngészőbeli reflow-k minimalizálása érdekében.

3. javítás: A stílusobjektumok elérése költséges

A jsPerf segítségével arra is rájöttünk, hogy bizonyos stíluslekérdező függvények gyorsabbak másoknál.

A PBS-ben gyakran le kell kérnünk egy elem adott stílusának aktuális értékét. Például a felső margó lekéréséhez általában ezt használtuk:

var styles = window.getComputedStyle( elem );
var marginTop = styles.marginTop;​

Azonban rájöttünk, hogy ez a megoldás sokkal gyorsabb:

var marginTop = elem.style.marginTop;​

Azonban az elem.style.marginTop és a getComputedStyle nagyon eltérő módon működik. Az első csak az elem soron belüli (inline) stílusát adja vissza, és nem veszi figyelembe a stíluslapokat; a második viszont mind az inline stílusokat, mind a stíluslapokat figyelembe veszi.

Ráadásul még akkor is, amikor a getComputedStyle eredménye gyorsítótárazva volt, még mindig rendkívül lassúnak bizonyult. Mint kiderült, maga a stílusobjektum elérése a lassú folyamat.

Szerencsénkre a PBS inline stílusokat rendel az elemekhez, és mivel az inline stílusok (az esetek túlnyomó részében) elsőbbséget élveznek a stíluslapokkal szemben, lesznek olyan esetek, amikor az inline stílus értékének lekérése is bőven elegendő. Így ha kombináljuk a fenti két módszert, egy sokkal gyorsabban lefutó kódot hozhatunk létre:

var marginTop = elem.style.marginTop;
if ( ! marginTop ) {
	var styles = window.getComputedStyle( elem );
	marginTop = styles.marginTop;
}​

A kódfejlesztések eredményei

A fenti 3 javításunk rengeteg kutatást, tesztelést és tényleges megvalósítást igényelt (és késleltette a Fonts [Betűtípusok] funkciónk megjelenését), de a végén minden erőfeszítés megérte.

Visszatérve a kezdeti tesztünkhöz, amely során 2 másodpercig mozgattuk az egeret a képernyőn: Íme a régi, csúnya/szaggató idővonal:

És így néz ki az új idővonalunk a teljesítményjavítások után:

Amint látható, jóval kevesebb lila sáv szerepel a grafikonon. És észrevehető a különbség a böngésző által végrehajtott folyamatok mennyiségében is. Az alsó téglalapok is vékonyabbak lettek, ami azt jelenti, hogy a folyamatok gyorsabban futnak le, mint korábban.

Ha összehasonlítjuk a magas/széles lila területek mennyiségét a régi és az új grafikonokon, elképesztő, 200%-os teljesítménynövekedésről beszélhetünk! Emellett általános teljesítményjavulást tapasztalunk minden területen. Egyáltalán nem rossz!

Tanulságok és tapasztalatok

Őszintén szólva nem gondoltuk, hogy a PBS-nek teljesítményproblémái lennének, mielőtt Ann fel nem hívta rá a figyelmet. Főként azért, mert fejlesztésre szánt, erős gépeket használtunk, és a szűk keresztmetszetek nem voltak annyira nyilvánvalóak. Visszatekintve, a teljesítmény ellenőrzésének az egyik legfőbb prioritásnak kellett volna lennie. De legalább most, a 4.3-as verziótól kezdve ezt sikeresen megoldottuk. ?

Azok számára, akik szeretnék növelni saját WordPress-bővítményük teljesítményét – különösen, ha sok JavaScriptet használnak –, íme a tanulságaim ebből a tapasztalatból:

  1. Korlátozza/gyorsítótárazza a reflow-t kiváltó tulajdonságok/függvények hívását! Használja ezt útmutatóként: What forces layout/reflow. The comprehensive list.
  2. Csoportosítsa össze a DOM-stílusok „olvasásait” és „írásait”! Használja a FastDom segédprogramot erre a feladatra!
  3. Használja a Chrome Timeline profilozóját, és gondoskodjon arról, hogy a lehető legkevesebb reflow (lila elem) keletkezzen!