Op 2 februari 2017 schreef Ann Taylor het artikel 12 Things We Need to See From WordPress Page Builders in 2017 + Who Already Gets It Right op CodeInWP. We zijn al meer dan een jaar continu bezig met het verbeteren van Page Builder Sandwich: we doen onderzoek, luisteren naar onze klanten, dus we dachten dat Page Builder Sandwich het artikel “Who Got it Right” van CodeInWP wel zou halen. Dat gebeurde echter niet, vanwege de prestaties.
Dus vroegen we Ann ernaar, en ze antwoordde:
…Ik heb wat gespeeld met een gratis versie. Het is veelbelovend: veel modules, vrij intuïtieve gebruikersinterface, geweldige WYSIWYG-ervaring, alles kan eenvoudig worden versleept. Toen ik echter ongeveer 10 modules op de pagina had staan, werd het moeilijk om ermee verder te werken vanwege de lage bewerkingssnelheid…
Volgens haar heeft Page Builder Sandwich dus prestatieproblemen. En aangezien we altijd naar feedback luisteren, hebben we het onderzocht.
De prestaties testen
We hadden een manier nodig om de prestaties van PBS te controleren; aangezien PBS voornamelijk via JavaScript in de browser draait, was de Timeline-profiler van Chrome het juiste hulpmiddel voor deze taak. De timeline-profiler kan identificeren en meten welke processen plaatsvinden op een webpagina tijdens runtime, wat perfect is voor het vinden van knelpunten in de snelheid.
We dachten dat we eerst de eenvoudigste stappen moesten testen: met de muis over meerdere elementen en rijen bewegen. Simpel, toch? We hadden er alle vertrouwen in dat we bij zoiets eenvoudigs niet tegen prestatieproblemen zouden aanlopen. Helaas hadden we het mis.
Zodra we op de opnameknop drukten, bewogen we de muis ongeveer 2 seconden over het scherm en drukten daarna op de stopknop. Hier is de resulterende tijdlijn van de profiler:
En jawel hoor. Hij zit barstensvol prestatieknelpunten!
Er gebeurt veel in de grafiek, maar het meest voor de hand liggende probleem is dat er veel paarse gebieden in zitten (wat niet het geval zou moeten zijn). Bovendien zijn deze paarse gebieden zichtbaar erg breed, en ze lijken allemaal de piek van de grafiek te raken. Het zal je ook opvallen dat de grafiek altijd vol is en dat de paarse gebieden bijna nooit naar nul dalen. Het lijkt erop dat de browser altijd bezig is met iets en bijna nooit rust.
Maar wat betekenen al die paarse gebieden eigenlijk?
De profiler was zo vriendelijk ons te vertellen dat ze reflows worden genoemd. Hier is de definitie van wat een reflow is van de Google Developer-site:
Reflow is de naam van het webbrowserproces voor het herberekenen van de posities en geometrieën van elementen in het document.. reflow is een gebruikersblokkerende bewerking in de browser.
Dit betekent dus dat het bewegen van je muis in PBS een heleboel reflows triggert. Wat nog erger is, is dat elke reflow de browser even pauzeert zodat deze zijn berekeningen kan voltooien. Meer reflows betekenen meer browserpauzes, en de ervaring wordt daardoor nog schokkeriger (janky). Eenvoudiger gezegd: veel paarse gebieden = slecht.
Het bleek dus dat Ann gelijk had: we hebben inderdaad een enorm prestatieknelpunt, dus ging ons team direct aan de slag om het meteen op te lossen.
Ons reflow- / jank-probleem oplossen
Voordat we iets anders deden, moesten we onderzoek doen om erachter te komen wat deze reflow in de eerste plaats veroorzaakt. We ontdekten dat blijkbaar veel gangbare zaken in JavaScript de browser ertoe kunnen aanzetten een reflow te veroorzaken.
Bijna elke functie die je kunt gebruiken om de breedte, hoogte of positie van een DOM-element op te halen, kan een reflow activeren. Paul Irish schreef een uitgebreide lijst van alle eigenschappen en functies in JavaScript die dit kunnen doen. Het slechte nieuws is dat Page Builder Sandwich veel van deze eigenschappen en functies gebruikt, die allemaal reflow veroorzaken:
- elem.getBoundingClientRect()
- window.getComputedStyle()
- window.scrollY
- en nog veel meer…
Oplossing #1: Kostbare reflow-processen cachen
De meest voor de hand liggende oplossing is om die eigenschappen en functies NIET te gebruiken. PBS is er echter sterk van afhankelijk en we kunnen niet zonder.
De oplossing die ons restte, is om zoveel mogelijk aanroepen naar die functies te beperken. Het lastige hieraan was dat we die eigenschappen en functies op veel verschillende plekken in PBS gebruikten, en meestal gebruikten we ze meerdere keren op hetzelfde DOM-element. Om dit op te lossen, hebben we voor elke problematische eigenschap/functie ons eigen cachingmechanisme gemaakt.
De functie getBoundingClientRect geeft ons bijvoorbeeld de huidige pixelafmetingen voor boven, links, breedte, hoogte, rechts en onder van een DOM-element. We gebruiken deze functie voortdurend om de locatie te berekenen waar de elementcontouren in PBS moeten worden getekend. Hier is de cachingfunctie die we hiervoor hebben gemaakt:
// Cache: Onze client rects worden hier bewaard, we kunnen dit gebruiken om ze later te wissen.
var elemsWithBoundingRects = [];
window.pbsGetBoundingClientRect = function( element ) {
// Controleer of we de client rect al eerder hebben opgehaald.
if ( ! element._boundingClientRect ) {
// Zo niet, haal hem op en sla hem op voor toekomstig gebruik.
element._boundingClientRect = element.getBoundingClientRect();
elemsWithBoundingRects.push( element );
}
return element._boundingClientRect;
};
In plaats van getBoundingClientRect aan te roepen, gebruikten we nu pbsGetBoundingClientRect. Telkens wanneer er nu een client rect wordt opgevraagd, slaan we die waarde op voor toekomstig gebruik, waardoor het aantal reflow-triggers wordt beperkt.
We hadden ook een manier nodig om de cache te wissen wanneer de opgeslagen client rects ongeldig worden. De waarden van de opgeslagen client rect veranderen wanneer de gebruiker over de pagina scrolt of elementen op het scherm toevoegt/wijzigt/verwijdert. Dus hebben we een functie gemaakt om de cache te wissen, die we kunnen aanroepen wanneer we nieuwe client rect-waarden nodig hebben:
var clearClientRects = function() {
var i;
for ( i = 0; i < elemsWithBoundingRects.length; i++ ) {
if ( elemsWithBoundingRects[ i ] ) {
elemsWithBoundingRects[ i ]._boundingClientRect = null;
}
}
elemsWithBoundingRects = [];
};
We hebben ditzelfde cachingmechanisme ook toegepast op andere kostbare functie-aanroepen die we gewoonlijk gebruiken.
Oplossing #2: Lees- en schrijfbewerkingen groeperen met FastDom
Het implementeren van oplossing #1 verbeterde de prestaties van PBS, maar het was niet genoeg.
Toen we wat verder zochten op Google, kwamen we een artikel tegen van Wilson Page over het voorkomen van ‘layout thrashing’. Wilson zegt:
Layout Thrashing (reflow) treedt op wanneer JavaScript herhaaldelijk gewelddadig naar de DOM schrijft en er vervolgens uit leest, wat meerdere document-reflows veroorzaakt.
Blijkbaar is het niet genoeg om je aanroepen naar specifieke reflow-veroorzakende eigenschappen en functies te beperken. Wanneer je meerdere opeenvolgende “lees”- en “schrijf”-acties op de DOM uitvoert, activeert dit ook een browser-reflow. Hier is een typisch scenario, rechtstreeks van Wilsons site:
// Lezen var h1 = element1.clientHeight; // Schrijven (invalideert lay-out) element1.style.height = (h1 * 2) + 'px'; // Lezen (triggert reflow) var h2 = element2.clientHeight; // Schrijven (invalideert lay-out) element2.style.height = (h2 * 2) + 'px'; // Lezen (triggert reflow) var h3 = element3.clientHeight; // Schrijven (invalideert lay-out) element3.style.height = (h3 * 2) + 'px'; // Document reflowt aan einde van frame
Het bovenstaande voorbeeld activeert 3 reflows omdat de afmetingen van de hele pagina ongeldig worden gemaakt na een schrijfactie. De voorgestelde oplossing hiervoor is om de code te herschikken om alle lees- en schrijfacties te bundelen, zodat het geheel slechts 1 browser-reflow zou activeren:
// Lezen var h1 = element1.clientHeight; var h2 = element2.clientHeight; var h3 = element3.clientHeight; // Schrijven (invalideert lay-out) element1.style.height = (h1 * 2) + 'px'; element2.style.height = (h2 * 2) + 'px'; element3.style.height = (h3 * 2) + 'px'; // Document reflowt aan einde van frame
Het bovenstaande fragment laat het oplossen van het probleem er eenvoudig uitzien. Maar in werkelijkheid gebeurt het lezen en schrijven van reflow-triggerende zaken in PBS op veel verschillende plaatsen en op veel verschillende momenten tijdens de levenscyclus van PBS. Het reorganiseren van duizenden regels JavaScript-code is niet haalbaar.
Gelukkig voor ons bood Wilson in zijn artikel ook een oplossing. Hij creëerde een klein hulpscript om te helpen bij het bundelen van die lees- en schrijfbewerkingen, genaamd FastDom.
Hier is wat code uit Page Builder Sandwich die FastDom gebruikt en ook onze Oplossing #1 van hierboven bevat:
// Bundel de leesacties van andere threads.
fastdom.measure( function() {
// Gebruik onze gecachete client rect.
var rect = window.pbsGetBoundingClientRect( elem );
// Bundel de schrijfacties van andere threads.
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 ) );
Wat FastDom doet, is dat het alle fastdom.measure-aanroepen en alle fastdom.mutate-aanroepen groepeert en asynchroon uitvoert, waardoor alle lees- en schrijfbewerkingen effectief worden gebundeld om alle reflows in de browser te verminderen.
Oplossing #3: Toegang tot stijlobjecten is kostbaar
We ontdekten met behulp van jsPerf ook dat sommige functies voor het ophalen van stijlen sneller waren dan andere.
In PBS moeten we meestal de huidige waarde van een specifieke stijl van een element ophalen. Dit gebruiken we bijvoorbeeld normaal gesproken om de bovenmarge op te halen:
var styles = window.getComputedStyle( elem ); var marginTop = styles.marginTop;
Maar we ontdekten dat dit veel sneller is:
var marginTop = elem.style.marginTop;
Echter, elem.style.marginTop en getComputedStyle zijn heel verschillend. De eerste geeft ons alleen de inline styling van het element en houdt geen rekening met stylesheets; en de tweede houdt rekening met zowel inline stijlen als stylesheets.
Bovendien was het ophalen van het getComputedStyle-resultaat, zelfs wanneer het in de cache was opgeslagen, nog steeds erg traag. Het blijkt dat de toegang tot het stijlobject zelf het trage proces is.
Gelukkig voor ons voegt PBS inline stijlen toe aan elementen, en aangezien inline stijlen (meestal) voorrang hebben op stylesheets, zullen er gevallen zijn waarin het ophalen van de inline stijlwaarde voldoende is. Dus als we de twee bovenstaande methoden combineren, kunnen we komen tot een veel sneller werkende code:
var marginTop = elem.style.marginTop;
if ( ! marginTop ) {
var styles = window.getComputedStyle( elem );
marginTop = styles.marginTop;
}
De resultaten van de codeverbeteringen
Onze 3 bovenstaande oplossingen vergden veel onderzoek, testen en daadwerkelijke implementatie (en vertraagden de release van onze Fonts-functie), maar uiteindelijk was het het allemaal waard.
Terugkomend op onze initiële test waarbij je de muis gedurende 2 seconden over het scherm beweegt. Hier is de oude lelijke/schokkerige tijdlijn:
En dit is hoe onze nieuwe tijdlijn eruitziet na de prestatieverbeteringen:
Zoals je kunt zien, zit er minder paars in de grafiek. En er is een merkbaar verschil in het aantal processen dat door de browser wordt uitgevoerd. De rechthoekige elementen aan de onderkant zijn ook dunner, wat betekent dat processen sneller worden uitgevoerd dan voorheen.
Als je de hoeveelheid hoge/brede paarse gebieden in de oude en nieuwe grafieken vergelijkt, kijken we naar een verbluffende prestatieverbetering van 200%! We zien ook over de hele linie prestatieverbeteringen. Helemaal niet slecht!
Belangrijkste inzichten & lessen
Om eerlijk te zijn dachten we niet dat PBS prestatieproblemen had voordat Ann het ter sprake bracht. Vooral omdat we op ontwikkelingsgerichte machines werkten en de prestatieknelpunten niet zo zichtbaar waren. Terugkijkend had het controleren van de prestaties een van de topprioriteiten moeten zijn. Maar nu is het, vanaf versie 4.3, tenminste opgelost.
Voor degenen die de prestaties van hun eigen WordPress-plugin willen verbeteren, vooral als je ook veel JavaScript gebruikt, zijn hier mijn lessen uit deze ervaring:
- Beperk/cache je aanroepen naar eigenschappen/functies die reflow activeren. Gebruik dit als richtlijn: What forces layout/reflow. The comprehensive list.
- Groepeer “lees”- en “schrijf”-bewerkingen naar DOM-stijlen. Gebruik FastDom om dit voor je te doen.
- Gebruik de Timeline-profiler van Chrome en zorg ervoor dat je zo min mogelijk reflows (paarse zaken) hebt.