Loni 2. února 2017 napsala Ann Taylor na webu CodeInWP článek 12 Things We Need to See From WordPress Page Builders in 2017 + Who Already Gets It Right. Page Builder Sandwich neustále vylepšujeme už více než rok: provádíme průzkumy, nasloucháme zákazníkům, a tak jsme si mysleli, že se Page Builder Sandwich dostane do článku „Kdo to dělá správně“ na CodeInWP. Jenže nedostal, a to kvůli výkonu. 
Tak jsme se na to Ann zeptali a ona odpověděla:
…trochu jsem si hrála s bezplatnou verzí. Vypadá to velmi slibně: spousta modulů, poměrně intuitivní uživatelské rozhraní, skvělý WYSIWYG zážitek, vše se dá snadno přetahovat. Když jsem však měla na stránce kolem 10 modulů, začalo být obtížné s ním dál pracovat kvůli nízké rychlosti editace…
Podle ní má tedy Page Builder Sandwich problémy s výkonem. A protože zpětné vazbě vždy věnujeme pozornost, šli jsme to prověřit.
Testování výkonu
Potřebovali jsme způsob, jak výkon PBS zkontrolovat. Vzhledem k tomu, že PBS běží převážně v prohlížeči přes JavaScript, byl správným nástrojem pro tento úkol Timeline profiler v Chromu. Profiler časové osy dokáže identifikovat a měřit procesy, které na webové stránce probíhají za běhu, což je ideální pro odhalení úzkých míst rychlosti.
Řekli jsme si, že bychom měli nejprve otestovat ten nejjednodušší krok – přejetí myší přes více prvků a řádků. Jednoduché, že? Byli jsme si jisti, že u něčeho tak jednoduchého na problémy s výkonem nenarazíme. Bohužel jsme se mýlili.
Jakmile jsme stiskli tlačítko pro záznam, pohybovali jsme myší po obrazovce asi 2 sekundy a pak stiskli tlačítko stop. Zde je výsledná časová osa z profileru:
A hle. Je doslova plná úzkých míst ve výkonu!
V grafu se děje spousta věcí, ale tím nejviditelnějším problémem je velké množství fialových oblastí (což by být nemělo). Navíc jsou tyto fialové oblasti viditelně velmi široké a zdá se, že všechny dosahují vrcholu grafu. Všimnete si také, že graf je stále plný a že fialové oblasti téměř nikdy neklesnou k nule. Vypadá to, že prohlížeč neustále něco dělá a téměř nikdy neodpočívá.
Co ale všechny ty fialové oblasti vlastně znamenají?
Profiler byl tak laskav a prozradil nám, že se jim říká reflows (překreslování / přepočítávání rozvržení). Zde je definice toho, co je reflow, z webu Google Developers:
Reflow je název procesu webového prohlížeče pro přepočítávání pozic a geometrií prvků v dokumentu… reflow je operace, která v prohlížeči blokuje uživatele.
To znamená, že pouhý pohyb myší v PBS spouští hromadu reflow operací. Horší je, že každý reflow prohlížeč na okamžik pozastaví, aby mohl dokončit své výpočty. Více reflow znamená více záseků prohlížeče a celý zážitek se stává ještě více trhaným. Zjednodušeně řečeno: hodně fialových oblastí = špatně.
Ukázalo se tedy, že Ann měla pravdu: obrovský problém s výkonem opravdu máme, a tak se náš tým okamžitě pustil do práce, aby to hned napravil.
Řešení našeho problému s reflow a zasekáváním
Nejprve jsme museli udělat průzkum, abychom zjistili, co vlastně tyto reflow operace v první řadě způsobuje. Zjistili jsme, že reflow v prohlížeči může zjevně vyvolat celá řada běžných věcí v JavaScriptu.
Téměř jakákoli funkce, kterou můžete použít k získání šířky, výšky nebo pozice prvku DOM, může reflow vyvolat. Paul Irish sepsal rozsáhlý seznam všech vlastností a funkcí v JavaScriptu, které to dělají. Špatnou zprávou je, že Page Builder Sandwich používá spoustu těchto vlastností a funkcí, z nichž všechny způsobují reflow:
- elem.getBoundingClientRect()
- window.getComputedStyle()
- window.scrollY
- a mnoho dalších…
Oprava č. 1: Cachování náročných reflow procesů
Nejjednodušším řešením je tyto vlastnosti a funkce NEPOUŽÍVAT. Jenže PBS na nich silně závisí a my se bez nich neobejdeme.
Řešením, které nám zbylo, bylo omezit volání těchto funkcí na minimum. Obtížná část spočívala v tom, že jsme tyto vlastnosti a funkce používali mnohokrát na různých místech PBS, a většinou jsme je volali na stejném prvku DOM opakovaně. Abychom to vyřešili, vytvořili jsme pro každou problematickou vlastnost/funkci vlastní mechanismus cachování.
Například funkce getBoundingClientRect nám dává aktuální rozměry prvku DOM v pixelech (top, left, width, height, right a bottom). Tuto funkci používáme neustále k výpočtu místa, kde se mají v PBS vykreslit obrysy prvků. Zde je funkce pro cachování, kterou jsme pro to vytvořili:
// Cache: Zde se ukládají naše obdélníky prvků (client rects), později je odsud můžeme vymazat.
var elemsWithBoundingRects = [];
window.pbsGetBoundingClientRect = function( element ) {
// Zkontrolujeme, zda jsme již dříve obdélník prvku nezískali.
if ( ! element._boundingClientRect ) {
// Pokud ne, získáme jej a uložíme pro budoucí použití.
element._boundingClientRect = element.getBoundingClientRect();
elemsWithBoundingRects.push( element );
}
return element._boundingClientRect;
};
Místo volání getBoundingClientRect nyní používáme pbsGetBoundingClientRect. Pokaždé, když je dotázán obdélník prvku, uložíme tuto hodnotu pro budoucí použití, čímž omezíme počet spouštěčů reflow.
Potřebovali jsme také způsob, jak mezipaměť vymazat, když uložené obdélníky prvků pozbydou platnosti. Hodnoty uloženého obdélníku se mění, když uživatel posouvá stránku nebo přidává/upravuje/odstraňuje prvky na obrazovce. Vytvořili jsme proto funkci pro vymazání mezipaměti, kterou můžeme zavolat, kdykoli potřebujeme čerstvé hodnoty:
var clearClientRects = function() {
var i;
for ( i = 0; i < elemsWithBoundingRects.length; i++ ) {
if ( elemsWithBoundingRects[ i ] ) {
elemsWithBoundingRects[ i ]._boundingClientRect = null;
}
}
elemsWithBoundingRects = [];
};
Tento stejný mechanismus cachování jsme použili i pro další náročná volání funkcí, která běžně využíváme.
Oprava č. 2: Seskupování čtení a zápisů pomocí FastDom
Implementace opravy č. 1 sice výkon PBS zvýšila, ale nestačilo to.
Při dalším googlení jsme narazili na článek, jehož autorem je Wilson Page: Preventing ‘layout thrashing’. Wilson píše:
K zahlcení rozvržení (layout thrashing / reflow) dochází, když JavaScript prudce střídavě zapisuje a čte z DOMu, což opakovaně způsobuje reflow dokumentu.
Očividně nestačí jen omezit volání konkrétních vlastností a funkcí, které reflow spouštějí. Když provádíte více po sobě jdoucích „čtení“ a „zápisů“ do DOMu, vyvolává to reflow prohlížeče také. Zde je typický scénář přímo z Wilsonova webu:
// Read
var h1 = element1.clientHeight;
// Write (invalidates layout)
element1.style.height = (h1 * 2) + 'px';
// Read (triggers reflow)
var h2 = element2.clientHeight;
// Write (invalidates layout)
element2.style.height = (h2 * 2) + 'px';
// Read (triggers reflow)
var h3 = element3.clientHeight;
// Write (invalidates layout)
element3.style.height = (h3 * 2) + 'px';
// Document reflows at end of frame
Výše uvedený příklad vyvolá 3 reflow operace, protože rozměry celé stránky se po zápisu zneplatní. Doporučenou opravou je přeuspořádat kód tak, aby se všechna čtení a zápisy seskupily k sobě, takže celá operace vyvolá pouze 1 reflow prohlížeče:
// Read
var h1 = element1.clientHeight;
var h2 = element2.clientHeight;
var h3 = element3.clientHeight;
// Write (invalidates layout)
element1.style.height = (h1 * 2) + 'px';
element2.style.height = (h2 * 2) + 'px';
element3.style.height = (h3 * 2) + 'px';
// Document reflows at end of frame
Díky výše uvedené ukázce vypadá vyřešení problému jednoduše. Ve skutečnosti však v PBS probíhá čtení a zápis věcí vyvolávajících reflow na mnoha různých místech a v mnoha různých okamžicích životního cyklu PBS. Přeorganizovat tisíce řádků javascriptového kódu nebylo proveditelné.
Naštěstí pro nás Wilson ve svém článku nabídl i řešení. Vytvořil malý pomocný skript, který pomáhá toto čtení a zápisy seskupovat dohromady, nazvaný FastDom.
Zde je ukázka kódu z Page Builder Sandwich, která používá FastDom a zahrnuje také naši opravu č. 1 výše:
// Seskupení čtení z jiných vláken.
fastdom.measure( function() {
// Použijeme náš nacachovaný obdélník prvku.
var rect = window.pbsGetBoundingClientRect( elem );
// Seskupení zápisů z jiných vláken.
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 ) );
FastDom funguje tak, že seskupí všechna volání fastdom.measure a všechna volání fastdom.mutate a provede je asynchronně, čímž efektivně spojí všechna čtení a zápisy dohromady a omezí veškeré reflow operace v prohlížeči.
Oprava č. 3: Přístup k objektům stylů je náročný
S pomocí nástroje jsPerf jsme také zjistili, že některé funkce pro získávání stylů jsou rychlejší než jiné.
V PBS obvykle potřebujeme získat aktuální hodnotu konkrétního stylu prvku. K získání horního okraje (top margin) běžně používáme například toto:
var styles = window.getComputedStyle( elem );
var marginTop = styles.marginTop;
Zjistili jsme však, že tento postup je mnohem rychlejší:
var marginTop = elem.style.marginTop;
Jenže elem.style.marginTop a getComputedStyle jsou velmi odlišné věci. První z nich nám dává pouze inline styl prvku a nebere v úvahu žádné externí styly (stylesheets); druhý bere v úvahu jak inline styly, tak šablony stylů.
Navíc i když byl výsledek getComputedStyle nacachován, stále to bylo velmi pomalé. Jak se ukázalo, pomalým procesem je samotný přístup k objektu stylů.
Naštěstí pro nás PBS přidává prvkům inline styly, a protože inline styly mají (většinou) přednost před externími styly, nastávají případy, kdy získání hodnoty inline stylu bohatě stačí. Pokud tedy zkombinujeme obě výše uvedené metody, můžeme dospět k mnohem rychleji běžícímu kódu:
var marginTop = elem.style.marginTop;
if ( ! marginTop ) {
var styles = window.getComputedStyle( elem );
marginTop = styles.marginTop;
}
Výsledky vylepšení kódu
Naše 3 výše uvedené opravy si vyžádaly spoustu průzkumu, testování a samotné implementace (a zpozdily vydání funkce Písma), ale nakonec to všechno stálo za to.
Vraťme se k našemu původnímu testu pohybu myší po obrazovce po dobu 2 sekund. Zde je stará nevzhledná/zasekaná časová osa:
A takto vypadá naše nová časová osa po provedení optimalizací výkonu:
Jak vidíte, v grafu je mnohem méně fialových ploch. A je zde patrný rozdíl v množství procesů prováděných prohlížečem. Obdélníkové úseky ve spodní části jsou také tenčí, což znamená, že procesy probíhají rychleji než dříve.
Pokud porovnáte množství vysokých/širokých fialových oblastí ve starém a novém grafu, díváme se na neuvěřitelný nárůst výkonu o 200 %! Zaznamenáváme také celkové zrychlení napříč celým systémem. Vůbec to není špatné!
Poznatky a poučení
Abychom byli upřímní, předtím, než to Ann zmínila, jsme si nemysleli, že PBS má problémy s výkonem. Hlavně proto, že jsme používali stroje optimalizované pro vývoj, a tam úzká místa výkonu nebyla tak zřejmá. Když se ohlédneme zpět, kontrola výkonu měla patřit mezi nejvyšší priority. Ale teď už je to alespoň od verze 4.3 vyřešeno. 
Pro ty, kteří chtějí zlepšit výkon vlastního pluginu pro WordPress, zvláště pokud také hojně využíváte JavaScript, uvádím své hlavní poznatky z této zkušenosti:
- Omezte/cachujte svá volání vlastností a funkcí, které spouštějí reflow. Jako vodítko použijte: What forces layout/reflow. The comprehensive list.
- Seskupujte „čtení“ a „zápisy“ stylů DOMu. Nechte to za vás udělat knihovnu FastDom.
- Používejte Chrome Timeline profiler a ujistěte se, že máte co nejméně reflow operací (fialových ploch).