2 lutego 2017 r. Ann Taylor napisała w CodeInWP artykuł 12 Things We Need to See From WordPress Page Builders in 2017 + Who Already Gets It Right. Nieustannie ulepszamy Page Builder Sandwich już od ponad roku: prowadzimy badania, słuchamy naszych klientów, więc myśleliśmy, że Page Builder Sandwich trafi do artykułu CodeInWP „Who Got it Right”. Jednak tak się nie stało z powodu wydajności. 
Zapytaliśmy więc o to Ann, a ona odpowiedziała:
…Pobawiłam się trochę darmową wersją. Zapowiada się bardzo obiecująco: mnóstwo modułów, całkiem intuicyjny interfejs użytkownika, świetne doświadczenie WYSIWYG, wszystko łatwo się przeciąga. Jednak gdy na stronie miałam około 10 modułów, dalsza praca stała się trudna ze względu na niską prędkość edycji…
Według niej Page Builder Sandwich ma więc problemy z wydajnością. A ponieważ zawsze słuchamy opinii, postanowiliśmy to sprawdzić.
Testowanie wydajności
Potrzebowaliśmy sposobu na sprawdzenie wydajności PBS; ponieważ PBS działa głównie w przeglądarce za pośrednictwem JavaScriptu, odpowiednim narzędziem do tego zadania był profiler osi czasu w Chrome. Profiler osi czasu potrafi zidentyfikować i zmierzyć procesy zachodzące na stronie w trakcie jej działania, co idealnie nadaje się do wyszukiwania wąskich gardeł wydajności.
Uznaliśmy, że najpierw powinniśmy przetestować najprostsze czynności – przesuwanie myszy nad wieloma elementami i wierszami. Proste, prawda? Byliśmy pewni, że przy czymś tak prostym nie napotkamy żadnych problemów z wydajnością. Niestety, myliliśmy się.
Po kliknięciu przycisku nagrywania przesuwaliśmy mysz po ekranie przez około 2 sekundy, a następnie kliknęliśmy przycisk zatrzymania. Oto wynikowa oś czasu z profilera:
I proszę bardzo. Jest po brzegi wypełniona wąskimi gardłami wydajności!
Na wykresie dzieje się bardzo dużo, ale najbardziej oczywistym problemem jest obecność wielu fioletowych obszarów (co nie powinno mieć miejsca). Co więcej, te fioletowe obszary są widocznie bardzo szerokie i wszystkie zdają się sięgać szczytu wykresu. Zauważysz również, że wykres jest stale zapełniony, a fioletowe obszary prawie nigdy nie spadają do zera. Wygląda na to, że przeglądarka jest cały czas czymś zajęta i niemal w ogóle nie odpoczywa.
Ale co właściwie oznaczają te wszystkie fioletowe obszary?
Profiler uprzejmie poinformował nas, że nazywają się one reflow. Oto definicja reflow ze strony Google Developer:
Reflow to nazwa procesu przeglądarki internetowej służącego do ponownego obliczania pozycji i geometrii elementów w dokumencie… Reflow jest operacją blokującą użytkownika w przeglądarce.
Oznacza to zatem, że poruszanie myszą w PBS wywołuje mnóstwo operacji reflow. Co gorsza, każdy reflow na chwilę zawiesza przeglądarkę, aby ta mogła dokończyć obliczenia. Więcej operacji reflow oznacza więcej przestojów przeglądarki, a działanie staje się jeszcze bardziej szarpane (janky). Mówiąc prościej: dużo fioletowych obszarów = źle.
Okazało się więc, że Ann miała rację: rzeczywiście mamy ogromne wąskie gardło wydajnościowe, dlatego nasz zespół natychmiast zabrał się do pracy, aby to naprawić.
Naprawa naszego problemu z reflow / zacinaniem
Przede wszystkim musieliśmy przeprowadzić analizę, aby dowiedzieć się, co w ogóle powoduje ten reflow. Odkryliśmy, że najwyraźniej wiele powszechnych operacji w JavaScripcie może sprowokować przeglądarkę do wywołania reflow.
Niemal każda funkcja, której można użyć do pobrania szerokości, wysokości lub pozycji elementu DOM, może wywołać reflow. Paul Irish sporządził wyczerpującą listę wszystkich właściwości i funkcji w JavaScripcie, które mogą do tego doprowadzić. Zła wiadomość jest taka, że Page Builder Sandwich używa wielu z tych właściwości i funkcji, a wszystkie one powodują reflow:
- elem.getBoundingClientRect()
- window.getComputedStyle()
- window.scrollY
- i wiele innych…
Poprawka nr 1: Buforowanie kosztownych procesów reflow
Najbardziej oczywistym rozwiązaniem jest NIEUŻYWANIE tych właściwości i funkcji. Jednak PBS w dużym stopniu na nich polega i nie możemy się bez nich obejść.
Rozwiązaniem, które nam pozostało, jest maksymalne ograniczenie wywołań tych funkcji. Trudność polegała na tym, że używaliśmy tych właściwości i funkcji wielokrotnie w różnych częściach PBS, a przez większość czasu odwoływaliśmy się do tego samego elementu DOM wielokrotnie. Aby to naprawić, stworzyliśmy własny mechanizm buforowania dla każdej problematycznej właściwości/funkcji.
Na przykład funkcja getBoundingClientRect daje nam bieżące wymiary pikselowe elementu DOM: górną i lewą pozycję, szerokość, wysokość, prawą oraz dolną krawędź. Używamy tej funkcji nieustannie do obliczania pozycji, w której należy narysować obrysy elementów w PBS. Oto funkcja buforująca, którą do tego stworzyliśmy:
// Pamięć podręczna: tutaj przechowywane są nasze client rects, możemy tego użyć do ich wyczyszczenia później.
var elemsWithBoundingRects = [];
window.pbsGetBoundingClientRect = function( element ) {
// Sprawdź, czy pobraliśmy już wcześniej client rect.
if ( ! element._boundingClientRect ) {
// Jeśli nie, pobierz go i zapisz do późniejszego użycia.
element._boundingClientRect = element.getBoundingClientRect();
elemsWithBoundingRects.push( element );
}
return element._boundingClientRect;
};
Zamiast wywoływać getBoundingClientRect , użyliśmy pbsGetBoundingClientRect . Teraz za każdym razem, gdy następuje zapytanie o client rect, zapisujemy tę wartość do późniejszego wglądu, ograniczając tym samym liczbę wywołań reflow.
Potrzebowaliśmy również sposobu na wyczyszczenie pamięci podręcznej, gdy zapisane client rects staną się nieaktualne. Wartości zapisanego client rect zmieniają się, gdy użytkownik przewija stronę lub dodaje/zmienia/usuwa elementy na ekranie. Stworzyliśmy więc funkcję czyszczącą pamięć podręczną, którą możemy wywołać za każdym razem, gdy potrzebujemy świeżych wartości client rect:
var clearClientRects = function() {
var i;
for ( i = 0; i < elemsWithBoundingRects.length; i++ ) {
if ( elemsWithBoundingRects[ i ] ) {
elemsWithBoundingRects[ i ]._boundingClientRect = null;
}
}
elemsWithBoundingRects = [];
};
Zastosowaliśmy ten sam mechanizm buforowania także dla innych kosztownych wywołań funkcji, z których zwykle korzystamy.
Poprawka nr 2: Grupowanie operacji odczytu i zapisu za pomocą FastDom
Wdrożenie poprawki nr 1 poprawiło wydajność PBS, ale to nie wystarczyło.
Szukając dalej w Google, natknęliśmy się na artykuł autorstwa Wilsona Page’a na temat zapobiegania „layout thrashing”. Wilson pisze:
Layout Thrashing (reflow) ma miejsce wtedy, gdy JavaScript gwałtownie zapisuje, a następnie odczytuje dane z modelu DOM wielokrotnie, powodując przeliczanie układu dokumentu.
Okazuje się, że nie wystarczy ograniczyć wywołań określonych właściwości i funkcji powodujących reflow. Wykonywanie wielu następujących po sobie „odczytów” i „zapisów” w drzewie DOM również powoduje reflow w przeglądarce. Oto typowy scenariusz, prosto ze strony Wilsona:
// Odczyt
var h1 = element1.clientHeight;
// Zapis (unieważnia układ)
element1.style.height = (h1 * 2) + 'px';
// Odczyt (wywołuje reflow)
var h2 = element2.clientHeight;
// Zapis (unieważnia układ)
element2.style.height = (h2 * 2) + 'px';
// Odczyt (wywołuje reflow)
var h3 = element3.clientHeight;
// Zapis (unieważnia układ)
element3.style.height = (h3 * 2) + 'px';
// Reflow dokumentu na końcu klatki
Powyższy przykład wywołuje 3 operacje reflow, ponieważ wymiary całej strony ulegają unieważnieniu po zapisie. Sugerowanym rozwiązaniem jest zmiana kolejności kodu, aby zgrupować wszystkie odczyty i zapisy razem, dzięki czemu całość wywoła tylko 1 reflow w przeglądarce:
// Odczyt
var h1 = element1.clientHeight;
var h2 = element2.clientHeight;
var h3 = element3.clientHeight;
// Zapis (unieważnia układ)
element1.style.height = (h1 * 2) + 'px';
element2.style.height = (h2 * 2) + 'px';
element3.style.height = (h3 * 2) + 'px';
// Reflow dokumentu na końcu klatki
Powyższy fragment sprawia, że rozwiązanie problemu wygląda na proste. Jednak w rzeczywistości w PBS odczytywanie i zapisywanie rzeczy wywołujących reflow ma miejsce w wielu różnych miejscach i w różnym czasie cyklu życia PBS. Reorganizacja tysięcy linii kodu JavaScript jest po prostu niewykonalna.
Na szczęście dla nas, Wilson przedstawił w swoim artykule również rozwiązanie. Stworzył mały skrypt pomocniczy ułatwiający grupowanie tych odczytów i zapisów, nazwany FastDom.
Oto fragment kodu z Page Builder Sandwich, który korzysta z FastDom i zawiera również naszą Poprawkę nr 1 opisaną powyżej:
// Zgrupuj odczyty z innych wątków.
fastdom.measure( function() {
// Użyj naszego zbuforowanego client rect.
var rect = window.pbsGetBoundingClientRect( elem );
// Zgrupuj zapisy z innych wątków.
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 ) );
Działanie FastDom polega na tym, że grupuje wszystkie wywołania fastdom.measure oraz wszystkie wywołania fastdom.mutate i wykonuje je asynchronicznie, efektywnie łącząc ze sobą wszystkie odczyty i zapisy, aby ograniczyć liczbę operacji reflow w przeglądarce.
Poprawka nr 3: Odwoływanie się do obiektów stylów jest kosztowne
Dzięki jsPerf odkryliśmy również, że niektóre funkcje pobierania stylów są szybsze od innych.
W PBS zazwyczaj musimy pobrać bieżącą wartość konkretnego stylu danego elementu. Zwykle używamy do tego na przykład takiego kodu, aby uzyskać margines górny:
var styles = window.getComputedStyle( elem );
var marginTop = styles.marginTop;
Okazało się jednak, że zrobienie tego w ten sposób jest znacznie szybsze:
var marginTop = elem.style.marginTop;
Jednak elem.style.marginTop oraz getComputedStyle znacząco się różnią. Pierwszy sposób zwraca tylko style liniowe elementu i nie uwzględnia żadnych arkuszy stylów, natomiast drugi uwzględnia zarówno style liniowe, jak i arkusze stylów.
Co więcej, nawet gdy wynik getComputedStyle był buforowany, nadal działało to bardzo wolno. Jak się okazuje, powolnym procesem jest sam dostęp do obiektu stylów.
Na nasze szczęście PBS dołącza do elementów style liniowe, a ponieważ style liniowe mają (w większości przypadków) pierwszeństwo przed arkuszami stylów, zdarzają się sytuacje, w których pobranie wartości stylu liniowego w zupełności wystarczy. Łącząc więc obie powyższe metody, możemy uzyskać kod działający znacznie szybciej:
var marginTop = elem.style.marginTop;
if ( ! marginTop ) {
var styles = window.getComputedStyle( elem );
marginTop = styles.marginTop;
}
Rezultaty ulepszeń w kodzie
Wprowadzenie powyższych 3 poprawek wymagało wielu badań, testów i faktycznej implementacji (oraz opóźniło wydanie naszej funkcji czcionek), ale ostatecznie było warto.
Wracając do naszego początkowego testu polegającego na poruszaniu myszą po ekranie przez 2 sekundy. Oto stara, brzydka i zacinająca się oś czasu:
A oto jak wygląda nasza nowa oś czasu po ulepszeniach wydajności:
Jak widać, na wykresie jest znacznie mniej fioletowych elementów. Widoczna jest także wyraźna różnica w liczbie procesów wykonywanych przez przeglądarkę. Prostokątne bloki na dole są również cieńsze, co oznacza, że procesy wykonują się szybciej niż wcześniej.
Porównując liczbę wysokich/szerokich fioletowych obszarów na starym i nowym wykresie, mamy do czynienia z zawrotnym 200% wzrostem wydajności! Widzimy również poprawę wydajności na wszystkich polach. Wcale nie najgorzej!
Wnioski i lekcje
Mówiąc szczerze, dopóki Ann o tym nie wspomniała, nie sądziliśmy, że PBS ma problemy z wydajnością. Wynikało to głównie z tego, że korzystaliśmy z maszyn typowo deweloperskich i wąskie gardła wydajności nie były tak widoczne. Patrząc wstecz, sprawdzanie wydajności powinno być jednym z najwyższych priorytetów. Ale przynajmniej teraz, od wersji 4.3, zostało to załatwione. 
Dla osób chcących poprawić wydajność własnych wtyczek do WordPressa, zwłaszcza jeśli intensywnie korzystacie z JavaScriptu, oto moje wnioski z tego doświadczenia:
- Ograniczaj/buforuj odwołania do właściwości/funkcji wywołujących reflow. Skorzystaj z tego przewodnika: What forces layout/reflow. The comprehensive list.
- Grupuj „odczyty” i „zapisy” stylów DOM. Użyj FastDom, aby zrobił to za Ciebie.
- Korzystaj z profilera osi czasu w Chrome i upewnij się, że masz jak najmniej operacji reflow (fioletowych elementów).