Στις 2 Φεβρουαρίου 2017, η Ann Taylor έγραψε το άρθρο 12 Things We Need to See From WordPress Page Builders in 2017 + Who Already Gets It Right στο CodeInWP. Βελτιώνουμε συνεχώς το Page Builder Sandwich για περισσότερο από έναν χρόνο τώρα: κάνουμε την έρευνά μας, ακούμε τους πελάτες μας, οπότε νομίζαμε ότι το Page Builder Sandwich θα συμπεριλαμβανόταν στο άρθρο «Who Got it Right» του CodeInWP. Ωστόσο, δεν τα καταφέραμε, λόγω απόδοσης.
Έτσι ρωτήσαμε την Ann σχετικά με αυτό, και εκείνη απάντησε:
…Δοκίμασα λίγο τη δωρεάν έκδοση. Δείχνει πολλά υποσχόμενη: πολλά modules, αρκετά διαισθητικό περιβάλλον χρήστη (UI), εξαιρετική εμπειρία WYSIWYG, όλα μετακινούνται εύκολα με drag and drop. Ωστόσο, όταν είχα περίπου 10 modules στη σελίδα, έγινε δύσκολο να συνεχίσω να εργάζομαι με αυτό λόγω της χαμηλής ταχύτητας επεξεργασίας…
Σύμφωνα λοιπόν με εκείνη, το Page Builder Sandwich έχει προβλήματα απόδοσης. Και καθώς πάντα ακούμε τα σχόλια, το ελέγξαμε.
Δοκιμάζοντας την Απόδοση
Χρειαζόμασταν έναν τρόπο να ελέγξουμε την απόδοση του PBS· εφόσον το PBS εκτελείται κυρίως στο πρόγραμμα περιήγησης μέσω Javascript, το Timeline profiler του Chrome ήταν το κατάλληλο εργαλείο για αυτή τη δουλειά. Το timeline profiler μπορεί να εντοπίσει και να μετρήσει ποιες διεργασίες λαμβάνουν χώρα σε μια ιστοσελίδα κατά τον χρόνο εκτέλεσης, κάτι που είναι ιδανικό για την εύρεση σημείων συμφόρησης (bottlenecks) στην ταχύτητα.
Σκεφτήκαμε ότι θα έπρεπε πρώτα να δοκιμάσουμε το πιο απλό από τα βήματα – να σύρουμε το ποντίκι μας πάνω από πολλαπλά στοιχεία και σειρές. Απλό, σωστά; Ήμασταν σίγουροι ότι δεν θα συναντούσαμε προβλήματα απόδοσης με κάτι τόσο απλό. Δυστυχώς, κάναμε λάθος.
Μόλις πατήσαμε το κουμπί εγγραφής, κινήσαμε το ποντίκι για περίπου 2 δευτερόλεπτα στην οθόνη και μετά πατήσαμε το κουμπί διακοπής. Ιδού το χρονολόγιο (timeline) που προέκυψε από το profiler:
Και ιδού. Είναι γεμάτο ασφυκτικά με σημεία συμφόρησης απόδοσης!
Συμβαίνουν πολλά πράγματα στο γράφημα, αλλά το πιο προφανές πρόβλημα είναι ότι υπάρχουν πολλές μοβ περιοχές σε αυτό (κάτι που δεν θα έπρεπε να συμβαίνει). Επιπλέον, αυτές οι μοβ περιοχές είναι ορατά πολύ πλατιές και όλες φαίνεται να φτάνουν στην κορυφή του γραφήματος. Θα παρατηρήσετε επίσης ότι το γράφημα είναι πάντα γεμάτο, και ότι οι μοβ περιοχές σχεδόν ποτέ δεν πέφτουν στο μηδέν. Φαίνεται σαν το πρόγραμμα περιήγησης να είναι πάντα απασχολημένο κάνοντας κάτι, και σχεδόν ποτέ να μην ξεκουράζεται.
Τι σημαίνουν όμως πραγματικά όλες αυτές οι μοβ περιοχές;
Το profiler είχε την ευγένεια να μας δείξει ότι ονομάζονται reflows. Ορίστε ο ορισμός του τι είναι το reflow από τον ιστότοπο Google Developer:
Το reflow είναι το όνομα της διαδικασίας του προγράμματος περιήγησης ιστού για τον επανυπολογισμό των θέσεων και των γεωμετριών των στοιχείων στο έγγραφο.. το reflow είναι μια λειτουργία που μπλοκάρει τον χρήστη (user-blocking operation) στο πρόγραμμα περιήγησης.
Αυτό σημαίνει λοιπόν ότι η κίνηση του ποντικιού σας μέσα στο PBS πυροδοτεί έναν τεράστιο αριθμό από reflows. Το χειρότερο είναι ότι κάθε reflow παγώνει στιγμιαία το πρόγραμμα περιήγησης ώστε να μπορέσει να ολοκληρώσει τους υπολογισμούς του. Περισσότερα reflows σημαίνουν περισσότερα παγώματα του προγράμματος περιήγησης, και η εμπειρία γίνεται ακόμα πιο προβληματική με κολλήματα (janky). Με απλούστερους όρους, πολλές μοβ περιοχές = κακό αποτέλεσμα.
Αποδείχθηκε λοιπόν ότι η Ann είχε δίκιο: έχουμε όντως ένα τεράστιο πρόβλημα απόδοσης, οπότε η ομάδα μας στρώθηκε αμέσως στη δουλειά για να το διορθώσει.
Διορθώνοντας το Πρόβλημα των Reflow / Jank
Πριν από οτιδήποτε άλλο, έπρεπε να κάνουμε κάποια έρευνα για να μάθουμε τι ακριβώς προκαλεί αυτό το reflow εξαρχής. Ανακαλύψαμε ότι προφανώς, πολλά συνηθισμένα πράγματα στη Javascript μπορούν να ωθήσουν το πρόγραμμα περιήγησης να προκαλέσει reflow.
Σχεδόν κάθε συνάρτηση που μπορείτε να χρησιμοποιήσετε για να λάβετε το πλάτος, το ύψος ή τη θέση ενός στοιχείου DOM μπορεί να ενεργοποιήσει ένα reflow. Ο Paul Irish έγραψε μια εκτενή λίστα με όλες τις ιδιότητες και συναρτήσεις στη Javascript που μπορούν να το κάνουν αυτό. Τα κακά νέα είναι ότι το Page Builder Sandwich χρησιμοποιεί πολλές από αυτές τις ιδιότητες και συναρτήσεις, οι οποίες προκαλούν reflow:
- elem.getBoundingClientRect()
- window.getComputedStyle()
- window.scrollY
- και πολλά άλλα…
Διόρθωση #1: Προσωρινή Αποθήκευση (Caching) των Κοστοβόρων Διαδικασιών Reflow
Η πιο προφανής λύση είναι να ΜΗΝ χρησιμοποιούνται αυτές οι ιδιότητες και συναρτήσεις. Ωστόσο, το PBS βασίζεται σε μεγάλο βαθμό σε αυτές και δεν μπορούμε να λειτουργήσουμε χωρίς αυτές.
Η λύση που μας απομένει είναι να περικόψουμε όσο το δυνατόν περισσότερες κλήσεις προς αυτές τις συναρτήσεις. Το δύσκολο κομμάτι εδώ ήταν ότι χρησιμοποιούσαμε αυτές τις ιδιότητες και συναρτήσεις πολλές φορές σε διαφορετικά μέρη του PBS, και τις περισσότερες φορές, τις χρησιμοποιούσαμε στο ίδιο στοιχείο DOM πολλαπλές φορές. Για να το διορθώσουμε αυτό, δημιουργήσαμε τον δικό μας μηχανισμό προσωρινής αποθήκευσης (caching) για κάθε προβληματική ιδιότητα/συνάρτηση.
Για παράδειγμα, η συνάρτηση getBoundingClientRect μας δίνει τις τρέχουσες μετρήσεις σε pixel για top, left, width, height, right, και bottom ενός στοιχείου DOM. Χρησιμοποιούμε αυτή τη συνάρτηση συνεχώς για να υπολογίσουμε τη θέση στην οποία θα σχεδιαστούν τα περιγράμματα των στοιχείων στο PBS. Ορίστε η συνάρτηση caching που δημιουργήσαμε για αυτό:
// Cache: Our client rects are kept here, we can use this to clear them later.
var elemsWithBoundingRects = [];
window.pbsGetBoundingClientRect = function( element ) {
// Check if we already got the client rect before.
if ( ! element._boundingClientRect ) {
// If not, get it then store it for future use.
element._boundingClientRect = element.getBoundingClientRect();
elemsWithBoundingRects.push( element );
}
return element._boundingClientRect;
};
Αντί να καλούμε το getBoundingClientRect, χρησιμοποιούσαμε πλέον το pbsGetBoundingClientRect. Τώρα, κάθε φορά που γίνεται ερώτημα για το client rect, αποθηκεύουμε αυτήν την τιμή για μελλοντική αναφορά, περιορίζοντας έτσι τον αριθμό των ενεργοποιήσεων reflow.
Χρειαζόμασταν επίσης έναν τρόπο να καθαρίζουμε την cache όταν τα αποθηκευμένα client rects καθίστανται μη έγκυρα. Οι τιμές του αποθηκευμένου client rect αλλάζουν όταν ο χρήστης κάνει κύλιση στη σελίδα ή προσθέτει/αλλάζει/αφαιρεί στοιχεία στην οθόνη. Δημιουργήσαμε λοιπόν μια συνάρτηση εκκαθάρισης της cache την οποία μπορούμε να καλούμε όποτε χρειαζόμαστε νέες τιμές client rect:
var clearClientRects = function() {
var i;
for ( i = 0; i < elemsWithBoundingRects.length; i++ ) {
if ( elemsWithBoundingRects[ i ] ) {
elemsWithBoundingRects[ i ]._boundingClientRect = null;
}
}
elemsWithBoundingRects = [];
};
Εφαρμόσαμε επίσης αυτόν τον ίδιο μηχανισμό caching και για άλλες κοστοβόρες κλήσεις συναρτήσεων που χρησιμοποιούμε συνήθως.
Διόρθωση #2: Ομαδοποίηση Αναγνώσεων & Εγγραφών Χρησιμοποιώντας το FastDom
Η εφαρμογή της διόρθωσης #1 αύξησε την απόδοση του PBS, αλλά δεν ήταν αρκετή.
Ψάχνοντας λίγο περισσότερο στο Google, συναντήσαμε ένα άρθρο του Wilson Page σχετικά με την Αποτροπή του «layout thrashing». Ο Wilson αναφέρει:
Το Layout Thrashing (reflow) συμβαίνει όταν η JavaScript γράφει βίαια, και στη συνέχεια διαβάζει, από το DOM, πολλαπλές φορές προκαλώντας reflows στο έγγραφο.
Προφανώς, δεν αρκεί να περιορίσετε τις κλήσεις σας σε συγκεκριμένες ιδιότητες και συναρτήσεις που προκαλούν reflow. Όταν εκτελείτε πολλαπλές διαδοχικές «αναγνώσεις» (reads) και «εγγραφές» (writes) στο DOM, αυτό ενεργοποιεί επίσης reflow στο πρόγραμμα περιήγησης. Ορίστε ένα τυπικό σενάριο, κατευθείαν από τον ιστότοπο του Wilson:
// 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
Το παραπάνω παράδειγμα προκαλεί 3 reflows επειδή οι διαστάσεις ολόκληρης της σελίδας ακυρώνονται μετά από μια εγγραφή. Η προτεινόμενη λύση για αυτό είναι η αναδιάταξη του κώδικα ώστε να ομαδοποιηθούν όλες οι αναγνώσεις και οι εγγραφές μαζί έτσι ώστε το όλο ζήτημα να ενεργοποιήσει μόνο 1 reflow στο πρόγραμμα περιήγησης:
// 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
Το παραπάνω απόσπασμα κάνει τη διόρθωση του προβλήματος να φαίνεται εύκολη. Στην πραγματικότητα όμως, στο PBS, η ανάγνωση και η εγγραφή πραγμάτων που προκαλούν reflow συμβαίνει σε πολλά διαφορετικά μέρη σε πολλές διαφορετικές χρονικές στιγμές κατά τον κύκλο ζωής του PBS. Η αναδιοργάνωση χιλιάδων γραμμών κώδικα Javascript δεν είναι εφικτή.
Για καλή μας τύχη, ο Wilson παρείχε επίσης μια λύση στο άρθρο του. Δημιούργησε ένα μικρό βοηθητικό script για να βοηθήσει στην ομαδοποίηση αυτών των αναγνώσεων και εγγραφών, που ονομάζεται FastDom.
Ορίστε κώδικας από το Page Builder Sandwich που χρησιμοποιεί το FastDom και περιλαμβάνει επίσης τη Διόρθωση #1 παραπάνω:
// Bunch together the reads from other threads.
fastdom.measure( function() {
// Use our cached client rect.
var rect = window.pbsGetBoundingClientRect( elem );
// Bunch together the writes from other 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 ) );
Αυτό που κάνει το FastDom είναι ότι ομαδοποιεί όλες τις κλήσεις fastdom.measure και όλες τις κλήσεις fastdom.mutate και τις εκτελεί ασύγχρονα, ομαδοποιώντας αποτελεσματικά όλες τις αναγνώσεις και τις εγγραφές μαζί για να μειώσει όλα τα reflows στο πρόγραμμα περιήγησης.
Διόρθωση #3: Η Πρόσβαση σε Αντικείμενα Style Είναι Δαπανηρή
Ανακαλύψαμε επίσης με τη βοήθεια του jsPerf ότι ορισμένες συναρτήσεις λήψης στυλ ήταν ταχύτερες από άλλες.
Στο PBS, συνήθως χρειάζεται να λάβουμε την τρέχουσα τιμή ενός συγκεκριμένου στυλ ενός στοιχείου. Για παράδειγμα, συνήθως χρησιμοποιούμε αυτό για να λάβουμε το επάνω περιθώριο (top margin):
var styles = window.getComputedStyle( elem ); var marginTop = styles.marginTop;
Όμως, ανακαλύψαμε ότι το να κάνουμε αυτό είναι πολύ πιο γρήγορο:
var marginTop = elem.style.marginTop;
Ωστόσο, το elem.style.marginTop και το getComputedStyle είναι πολύ διαφορετικά. Το πρώτο μας δίνει μόνο το inline styling του στοιχείου και δεν λαμβάνει υπόψη κανένα stylesheet· και το δεύτερο λαμβάνει υπόψη τόσο τα inline στυλ όσο και τα stylesheets.
Επιπλέον, ακόμη και όταν το αποτέλεσμα του getComputedStyle ήταν αποθηκευμένο στην cache, παρέμενε πραγματικά αργό. Όπως αποδείχθηκε, η πρόσβαση στο ίδιο το αντικείμενο style είναι η αργή διαδικασία.
Για καλή μας τύχη, το PBS προσαρτά inline στυλ στα στοιχεία, και δεδομένου ότι τα inline στυλ έχουν προτεραιότητα (τις περισσότερες φορές) έναντι των stylesheets, θα υπάρξουν περιπτώσεις όπου η λήψη της τιμής του inline στυλ θα ήταν αρκετή. Έτσι, εάν συνδυάσουμε τις δύο παραπάνω μεθόδους, μπορούμε να καταλήξουμε σε έναν πολύ ταχύτερο κώδικα εκτέλεσης:
var marginTop = elem.style.marginTop;
if ( ! marginTop ) {
var styles = window.getComputedStyle( elem );
marginTop = styles.marginTop;
}
Τα Αποτελέσματα των Βελτιώσεων του Κώδικα
Οι 3 παραπάνω διορθώσεις μας απαίτησαν πολλή έρευνα, δοκιμές και πραγματική υλοποίηση (και καθυστέρησαν την κυκλοφορία της λειτουργίας Γραμματοσειρών μας), αλλά στο τέλος άξιζε τον κόπο.
Επιστρέφοντας στην αρχική μας δοκιμή μετακίνησης του ποντικιού στην οθόνη για 2 δευτερόλεπτα. Ορίστε το παλιό άσχημο/προβληματικό timeline:
Και ορίστε πώς μοιάζει το νέο μας timeline μετά τις βελτιώσεις απόδοσης:
Όπως μπορείτε να δείτε, υπάρχουν λιγότερα μοβ στοιχεία στο γράφημα. Και υπάρχει μια αξιοσημείωτη διαφορά στην ποσότητα των διεργασιών που εκτελούνται από το πρόγραμμα περιήγησης. Τα ορθογώνια στοιχεία στο κάτω μέρος είναι επίσης πιο λεπτά, πράγμα που σημαίνει ότι οι διεργασίες εκτελούνται ταχύτερα από πριν.
Αν συγκρίνετε τον αριθμό των ψηλών/πλατιών μοβ περιοχών στα παλιά και νέα γραφήματα, βλέπουμε μια τεράστια αύξηση απόδοσης κατά 200%! Βλέπουμε επίσης αυξήσεις απόδοσης σε όλα τα επίπεδα. Καθόλου άσχημα!
Συμπεράσματα & Διδάγματα
Για να είμαστε ειλικρινείς, δεν πιστεύαμε ότι το PBS είχε προβλήματα απόδοσης πριν το αναφέρει η Ann. Κυρίως επειδή χρησιμοποιούσαμε μηχανήματα ανάπτυξης υψηλών προδιαγραφών και τα σημεία συμφόρησης στην απόδοση δεν ήταν τόσο εμφανή. Κοιτάζοντας πίσω, ο έλεγχος της απόδοσης θα έπρεπε να ήταν μία από τις κορυφαίες προτεραιότητες. Αλλά τουλάχιστον τώρα, από την έκδοση 4.3, έχει διευθετηθεί.
Για όσους θέλουν να βελτιώσουν την απόδοση του δικού τους πρόσθετου WordPress, ειδικά αν χρησιμοποιείτε επίσης βαριά Javascript, ορίστε τα διδάγματά μου από αυτήν την εμπειρία:
- Περιορίστε/αποθηκεύστε στην cache τις κλήσεις σας σε ιδιότητες/συναρτήσεις που ενεργοποιούν reflow. Χρησιμοποιήστε αυτό ως οδηγό: What forces layout/reflow. The comprehensive list.
- Ομαδοποιήστε τις «αναγνώσεις» και τις «εγγραφές» στα στυλ του DOM. Χρησιμοποιήστε το FastDom για να το κάνει αυτό για εσάς.
- Χρησιμοποιήστε το Timeline profiler του Chrome και βεβαιωθείτε ότι έχετε όσο το δυνατόν λιγότερα reflows (μοβ στοιχεία).