Am 2. Februar 2017 schrieb Ann Taylor auf CodeInWP den Artikel 12 Things We Need to See From WordPress Page Builders in 2017 + Who Already Gets It Right. Wir haben den Page Builder Sandwich nun schon seit mehr als einem Jahr kontinuierlich verbessert: Wir recherchieren, hören auf unsere Kunden und dachten daher, dass Page Builder Sandwich es in den „Who Got it Right“-Artikel von CodeInWP schaffen würde. Das taten wir jedoch nicht – wegen der Performance. 
Also fragten wir Ann danach, und sie antwortete:
… Ich habe ein wenig mit der kostenlosen Version herumprobiert. Sie wirkt sehr vielversprechend: viele Module, eine ziemlich intuitive Benutzeroberfläche, ein großartiges WYSIWYG-Erlebnis, alles lässt sich leicht verschieben. Als ich jedoch etwa 10 Module auf der Seite hatte, wurde es aufgrund der geringen Bearbeitungsgeschwindigkeit schwierig, weiter damit zu arbeiten …
Ihr zufolge hat Page Builder Sandwich also Performance-Probleme. Und da wir immer auf Feedback hören, sind wir der Sache nachgegangen.
Testen der Performance
Wir brauchten eine Möglichkeit, die Performance von PBS zu überprüfen; da PBS größtenteils über JavaScript im Browser läuft, war der Timeline-Profiler von Chrome das richtige Werkzeug dafür. Der Timeline-Profiler kann identifizieren und messen, welche Prozesse während der Laufzeit auf einer Webseite ablaufen, was perfekt ist, um Geschwindigkeitsengpässe aufzuspüren.
Wir dachten uns, wir sollten zuerst den einfachsten aller Schritte testen – mit der Maus über mehrere Elemente und Zeilen zu fahren. Klingt simpel, oder? Wir waren zuversichtlich, dass wir bei so etwas Einfachem nicht auf Performance-Probleme stoßen würden. Leider lagen wir falsch.
Sobald wir den Aufnahme-Button geklickt hatten, bewegten wir die Maus etwa 2 Sekunden lang über den Bildschirm und klickten dann auf Stopp. Hier ist die resultierende Timeline aus dem Profiler:
Und siehe da: Sie ist randvoll mit Performance-Engpässen!
Im Diagramm passiert eine ganze Menge, aber das offensichtlichste Problem ist, dass es sehr viele lila Bereiche gibt (was nicht der Fall sein sollte). Darüber hinaus sind diese lila Bereiche sichtbar sehr breit und scheinen alle den Höchstwert des Diagramms zu erreichen. Man bemerkt auch, dass das Diagramm durchgehend voll ist und die lila Bereiche fast nie auf null sinken. Es scheint, als sei der Browser ständig mit etwas beschäftigt und komme so gut wie nie zur Ruhe.
Aber was bedeuten all diese lila Bereiche eigentlich?
Der Profiler war so freundlich, uns zu verraten, dass sie Reflows genannt werden. Hier ist die Definition eines Reflows von der Google-Entwicklerseite:
Reflow ist die Bezeichnung für den Prozess eines Webbrowsers zur Neuberechnung der Positionen und Geometrien von Elementen im Dokument… Reflow ist ein benutzerblockierender Vorgang im Browser.
Das bedeutet also, dass das Bewegen der Maus in PBS eine Unmenge an Reflows auslöst. Was noch schlimmer ist: Jeder Reflow pausiert den Browser für einen kurzen Moment, damit er seine Berechnungen abschließen kann. Mehr Reflows bedeuten mehr Browser-Pausen, und das Benutzererlebnis wird noch ruckeliger (janky). Einfacher ausgedrückt: viele lila Bereiche = schlecht.
Es stellte sich also heraus, dass Ann recht hatte: Wir haben tatsächlich einen gewaltigen Performance-Engpass, weshalb sich unser Team sofort an die Arbeit machte, um ihn unverzüglich zu beheben.
Behebung unseres Reflow- / Ruckel-Problems
Zuallererst mussten wir recherchieren, was diesen Reflow überhaupt verursacht. Wir fanden heraus, dass anscheinend viele gängige Dinge in JavaScript den Browser dazu veranlassen können, einen Reflow auszulösen.
Fast jede Funktion, mit der man die Breite, Höhe oder Position eines DOM-Elements abrufen kann, kann einen Reflow auslösen. Paul Irish hat eine ausführliche Liste aller Eigenschaften und Funktionen in JavaScript verfasst, die dies tun können. Die schlechte Nachricht ist, dass Page Builder Sandwich viele dieser Eigenschaften und Funktionen nutzt, die allesamt Reflows verursachen:
- elem.getBoundingClientRect()
- window.getComputedStyle()
- window.scrollY
- und viele mehr …
Lösung 1: Caching aufwendiger Reflow-Prozesse
Die naheliegendste Lösung besteht darin, diese Eigenschaften und Funktionen NICHT zu verwenden. PBS ist jedoch stark auf sie angewiesen, und wir können nicht auf sie verzichten.
Die Lösung, die uns blieb, bestand darin, die Aufrufe dieser Funktionen so weit wie möglich zu reduzieren. Der schwierige Teil dabei war, dass wir diese Eigenschaften und Funktionen sehr oft an verschiedenen Stellen in PBS verwendet haben, und meistens haben wir sie mehrfach für dasselbe DOM-Element aufgerufen. Um dies zu beheben, haben wir unseren eigenen Caching-Mechanismus für jede problematische Eigenschaft/Funktion erstellt.
Beispielsweise liefert uns die Funktion getBoundingClientRect die aktuellen Pixelmaße für oben, links, Breite, Höhe, rechts und unten eines DOM-Elements. Wir verwenden diese Funktion ständig, um die Position zu berechnen, an der die Elementumrisse in PBS gezeichnet werden sollen. Hier ist die Caching-Funktion, die wir dafür erstellt haben:
// Cache: Hier werden unsere Client-Rects gespeichert, damit wir sie später löschen können.
var elemsWithBoundingRects = [];
window.pbsGetBoundingClientRect = function( element ) {
// Prüfen, ob wir das Client-Rect bereits zuvor abgerufen haben.
if ( ! element._boundingClientRect ) {
// Wenn nicht, abrufen und für die zukünftige Verwendung speichern.
element._boundingClientRect = element.getBoundingClientRect();
elemsWithBoundingRects.push( element );
}
return element._boundingClientRect;
};
Anstatt getBoundingClientRect aufzurufen, haben wir nun pbsGetBoundingClientRect verwendet. Jedes Mal, wenn das Client-Rect abgefragt wird, speichern wir diesen Wert nun für spätere Zugriffe und begrenzen so die Anzahl der Reflow-Auslöser.
Wir brauchten auch eine Möglichkeit, den Cache zu leeren, wenn die gespeicherten Client-Rects ungültig werden. Die Werte des gespeicherten Client-Rects ändern sich, wenn der Benutzer die Seite scrollt oder Elemente auf dem Bildschirm hinzufügt, ändert oder entfernt. Daher haben wir eine Funktion zum Leeren des Caches erstellt, die wir immer dann aufrufen können, wenn wir neue Client-Rect-Werte benötigen:
var clearClientRects = function() {
var i;
for ( i = 0; i < elemsWithBoundingRects.length; i++ ) {
if ( elemsWithBoundingRects[ i ] ) {
elemsWithBoundingRects[ i ]._boundingClientRect = null;
}
}
elemsWithBoundingRects = [];
};
Diesen Caching-Mechanismus haben wir auch für andere aufwendige Funktionsaufrufe implementiert, die wir üblicherweise verwenden.
Lösung 2: Zusammenfassen von Lese- und Schreibvorgängen mit FastDom
Die Implementierung von Lösung 1 steigerte die Performance von PBS, reichte aber noch nicht aus.
Nach weiteren Recherchen bei Google stießen wir auf einen Artikel von Wilson Page über das Verhindern von „Layout Thrashing“. Wilson schreibt:
Layout Thrashing (Reflow) tritt auf, wenn JavaScript heftig und wiederholt in das DOM schreibt und daraus liest, was zu Dokument-Reflows führt.
Anscheinend reicht es nicht aus, die Aufrufe bestimmter reflow-auslösender Eigenschaften und Funktionen zu begrenzen. Wenn man mehrere aufeinanderfolgende Lese- und Schreibvorgänge („reads“ und „writes“) am DOM durchführt, löst dies ebenfalls einen Browser-Reflow aus. Hier ist ein typisches Szenario, direkt von Wilsons Website:
// Lesen
var h1 = element1.clientHeight;
// Schreiben (macht Layout ungültig)
element1.style.height = (h1 * 2) + 'px';
// Lesen (löst Reflow aus)
var h2 = element2.clientHeight;
// Schreiben (macht Layout ungültig)
element2.style.height = (h2 * 2) + 'px';
// Lesen (löst Reflow aus)
var h3 = element3.clientHeight;
// Schreiben (macht Layout ungültig)
element3.style.height = (h3 * 2) + 'px';
// Dokument-Reflow am Ende des Frames
Das obige Beispiel löst 3 Reflows aus, da die Dimensionen der gesamten Seite nach einem Schreibvorgang ungültig werden. Die vorgeschlagene Lösung hierfür besteht darin, den Code so umzustrukturieren, dass alle Lese- und Schreibvorgänge zusammengefasst werden, sodass das Ganze nur noch 1 Browser-Reflow auslöst:
// Lesen
var h1 = element1.clientHeight;
var h2 = element2.clientHeight;
var h3 = element3.clientHeight;
// Schreiben (macht Layout ungültig)
element1.style.height = (h1 * 2) + 'px';
element2.style.height = (h2 * 2) + 'px';
element3.style.height = (h3 * 2) + 'px';
// Dokument-Reflow am Ende des Frames
Das obige Snippet lässt die Lösung des Problems einfach aussehen. In der Realität geschieht das Lesen und Schreiben von reflow-auslösenden Dingen in PBS jedoch an vielen verschiedenen Stellen und zu vielen verschiedenen Zeiten während des Lebenszyklus von PBS. Tausende Zeilen von JavaScript-Code umzustrukturieren, ist nicht machbar.
Zu unserem Glück lieferte Wilson in seinem Artikel auch eine Lösung. Er entwickelte ein kleines Hilfsskript namens FastDom, das dabei hilft, diese Lese- und Schreibvorgänge zu bündeln.
Hier ist etwas Code aus Page Builder Sandwich, der FastDom verwendet und auch unsere Lösung 1 von oben beinhaltet:
// Leseoperationen aus anderen Threads bündeln.
fastdom.measure( function() {
// Unser gecachtes Client-Rect verwenden.
var rect = window.pbsGetBoundingClientRect( elem );
// Schreiboperationen aus anderen Threads bündeln.
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 fasst alle fastdom.measure-Aufrufe und alle fastdom.mutate-Aufrufe zusammen und führt sie asynchron aus, wodurch alle Lese- und Schreibvorgänge effektiv gruppiert werden, um die Reflows im Browser zu reduzieren.
Lösung 3: Der Zugriff auf Style-Objekte ist kostspielig
Mithilfe von jsPerf fanden wir außerdem heraus, dass einige Funktionen zum Abrufen von Styles schneller waren als andere.
In PBS müssen wir üblicherweise den aktuellen Wert eines bestimmten Styles eines Elements abrufen. Normalerweise verwenden wir beispielsweise Folgendes, um den oberen Außenabstand (marginTop) zu ermitteln:
var styles = window.getComputedStyle( elem );
var marginTop = styles.marginTop;
Wir fanden jedoch heraus, dass Folgendes viel schneller ist:
var marginTop = elem.style.marginTop;
Allerdings unterscheiden sich elem.style.marginTop und getComputedStyle grundlegend. Das erste liefert uns nur das Inline-Styling des Elements und berücksichtigt keinerlei Stylesheets; das zweite berücksichtigt sowohl Inline-Styles als auch Stylesheets.
Darüber hinaus war das Ergebnis von getComputedStyle selbst dann noch sehr langsam, wenn es gecacht wurde. Wie sich herausstellte, ist der Zugriff auf das Style-Objekt selbst der langsame Prozess.
Zu unserem Glück hängt PBS Inline-Styles an Elemente an, und da Inline-Styles (meistens) Vorrang vor Stylesheets haben, wird es Fälle geben, in denen das Abrufen des Inline-Style-Wertes völlig ausreicht. Wenn wir also die beiden oben genannten Methoden kombinieren, erhalten wir einen viel schneller ausgeführten Code:
var marginTop = elem.style.marginTop;
if ( ! marginTop ) {
var styles = window.getComputedStyle( elem );
marginTop = styles.marginTop;
}
Die Ergebnisse der Code-Verbesserungen
Unsere 3 obigen Lösungen erforderten viel Recherche, Tests und tatsächliche Implementierung (und verzögerten die Veröffentlichung unseres Fonts-Features), aber am Ende hat es sich absolut gelohnt.
Kommen wir zurück zu unserem anfänglichen Test, bei dem die Maus 2 Sekunden lang über den Bildschirm bewegt wurde. Hier ist die alte, unschöne/ruckelige Timeline:
Und so sieht unsere neue Timeline nach den Performance-Verbesserungen aus:
Wie man sieht, gibt es weniger lila Bereiche im Diagramm. Zudem gibt es einen spürbaren Unterschied bei der Menge der Prozesse, die vom Browser ausgeführt werden. Die rechteckigen Elemente am unteren Rand sind ebenfalls dünner, was bedeutet, dass die Prozesse schneller als zuvor ausgeführt werden.
Vergleicht man die Menge der hohen/breiten lila Bereiche in der alten und neuen Grafik, so blicken wir auf eine satte Performance-Steigerung von 200 %! Wir verzeichnen auch durchweg Leistungssteigerungen auf breiter Front. Gar nicht mal so übel!
Fazit & Erkenntnisse
Um ehrlich zu sein, dachten wir nicht, dass PBS Performance-Probleme hätte, bevor Ann es ansprach. Hauptsächlich deshalb, weil wir entwicklungszentrierte Rechner nutzten und die Leistungsengpässe nicht so offensichtlich waren. Rückblickend hätte die Überprüfung der Performance eine der obersten Prioritäten sein müssen. Aber zumindest jetzt, ab Version 4.3, ist das Problem behoben. 
Für alle, die die Performance ihres eigenen WordPress-Plugins verbessern wollen, insbesondere bei starkem JavaScript-Einsatz, sind hier meine wichtigsten Erkenntnisse aus dieser Erfahrung:
- Aufrufe von Eigenschaften/Funktionen, die Reflows auslösen, begrenzen/cachen. Als Leitfaden dient: What forces layout/reflow. The comprehensive list.
- Lese- und Schreibzugriffe („reads“ und „writes“) auf DOM-Styles zusammenfassen. FastDom verwenden, um dies zu erledigen.
- Den Timeline-Profiler von Chrome nutzen und sicherstellen, dass so wenige Reflows (lila Bereiche) wie möglich auftreten.