ב-2 בפברואר 2017, אן טיילור כתבה את המאמר 12 Things We Need to See From WordPress Page Builders in 2017 + Who Already Gets It Right ב-CodeInWP. אנחנו משפרים באופן מתמיד את Page Builder Sandwich כבר יותר משנה: אנחנו חוקרים, מקשיבים ללקוחות שלנו, ולכן חשבנו ש-Page Builder Sandwich ייכנס למאמר "מי שעושה את זה נכון" של CodeInWP. אבל לא נכנסנו, בגלל ביצועים.
אז שאלנו את אן על כך, והיא ענתה:
…שיחקתי קצת עם גרסה חינמית. היא מראה המון פוטנציאל: שפע של מודולים, ממשק משתמש די אינטואיטיבי, חוויית WYSIWYG נהדרת, הכל נגרר בקלות. עם זאת, כאשר היו לי כ-10 מודולים בעמוד, הפך קשה להמשיך לעבוד איתה בגלל מהירות עריכה איטית…
אז לדבריה, ל-Page Builder Sandwich יש בעיות ביצועים. ומכיוון שאנחנו תמיד מקשיבים למשוב, בדקנו את זה.
בדיקת הביצועים
היינו צריכים דרך לבדוק את הביצועים של PBS; מכיוון ש-PBS פועל בעיקר בדפדפן באמצעות Javascript, ה-Timeline profiler של Chrome היה הכלי המתאים לעבודה. ה-timeline profiler יכול לזהות ולמדוד אילו תהליכים מתרחשים בדף אינטרנט בזמן ריצה, מה שמושלם לאיתור צווארי בקבוק במהירות.
חשבנו שכדאי לבדוק קודם את הפעולה הפשוטה ביותר – הזזת העכבר על פני מספר אלמנטים ושורות. פשוט, נכון? היינו בטוחים שלא ניתקל בבעיות ביצועים במשהו פשוט כל כך. לרוע המזל, טעינו.
ברגע שלחצנו על כפתור ההקלטה, הזזנו את העכבר במשך כ-2 שניות על המסך, ולאחר מכן לחצנו על כפתור העצירה. הנה ציר הזמן שהתקבל מה-profiler:
והנה, לא פחות ולא יותר: הוא עמוס כולו בצווארי בקבוק בביצועים!
הרבה דברים קורים בגרף, אבל הבעיה הברורה ביותר היא שיש בו הרבה אזורים סגולים (מה שלא אמור לקרות). בנוסף לכך, האזורים הסגולים הללו רחבים מאוד באופן ניכר לעין, ונראה שכולם מגיעים לשיא של הגרף. תבחינו גם שהגרף תמיד מלא, ושהאזורים הסגולים כמעט אף פעם לא יורדים לאפס. נראה שהדפדפן תמיד עסוק בעשיית משהו, וכמעט אף פעם לא נח.
אבל מה בעצם אומרים כל האזורים הסגולים האלה?
ה-profiler היה אדיב מספיק כדי לומר לנו שהם נקראים reflows. הנה ההגדרה של מהו reflow מתוך אתר Google Developer:
Reflow הוא שמו של תהליך דפדפן האינטרנט לחישוב מחדש של המיקומים והגאומטריה של אלמנטים במסמך.. reflow הוא פעולה החוסמת את המשתמש בדפדפן.
אז המשמעות היא שהזזת העכבר בתוך PBS מפעילה טונות של reflows. ומה שגרוע יותר הוא שכל reflow משהה את הדפדפן לרגע כדי שיוכל לסיים את החישובים שלו. יותר reflows משמעותם יותר השהיות של הדפדפן, והחוויה הופכת לעוד יותר מקרטעת (janky). במילים פשוטות יותר, הרבה אזורים סגולים = רע.
אז מסתבר שאן צדקה: אכן יש לנו צוואר בקבוק ענקי בביצועים, ולכן הצוות שלנו ניגש מיד לעבודה כדי לתקן זאת בהקדם.
תיקון בעיית ה-Reflow / קרטוע שלנו
לפני כל דבר אחר, היינו צריכים לעשות מחקר כדי לגלות מה בכלל גורם ל-reflow הזה. גילינו שככל הנראה, הרבה דברים נפוצים ב-Javascript יכולים לגרום לדפדפן לבצע reflow.
כמעט כל פונקציה שניתן להשתמש בה כדי לקבל את הרוחב, הגובה או המיקום של אלמנט DOM יכולה להפעיל reflow. פול איריש (Paul Irish) כתב רשימה מקיפה של כל המאפיינים והפונקציות ב-Javascript שיכולים לעשות זאת. החדשות הרעות הן ש-Page Builder Sandwich משתמש בהרבה מהמאפיינים והפונקציות האלה, שכולם גורמים ל-reflow:
- elem.getBoundingClientRect()
- window.getComputedStyle()
- window.scrollY
- ועוד הרבה…
תיקון מס' 1: שמירה במטמון (Caching) של תהליכי Reflow יקרים
הפתרון הברור ביותר הוא לא להשתמש במאפיינים ובפונקציות הללו. עם זאת, PBS מסתמך עליהם רבות ואיננו יכולים להסתדר בלעדיהם.
הפתרון שנותר לנו הוא לצמצם ככל האפשר את הקריאות לפונקציות הללו. החלק הקשה כאן היה שהשתמשנו במאפיינים ובפונקציות האלה פעמים רבות בחלקים שונים של PBS, וברוב הפעמים השתמשנו בהם על אותו אלמנט DOM מספר פעמים. כדי לתקן זאת, יצרנו מנגנון שמירה במטמון משלנו עבור כל מאפיין/פונקציה בעייתיים.
לדוגמה, הפונקציה getBoundingClientRect מספקת לנו את מידות הפיקסלים הנוכחיות של top, left, width, height, right ו-bottom של אלמנט DOM. אנחנו משתמשים בפונקציה זו כל הזמן כדי לחשב את המיקום שבו יש לצייר את קווי המתאר של האלמנט ב-PBS. הנה פונקציית השמירה במטמון שיצרנו עבור זה:
// 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.
היינו צריכים גם דרך לנקות את המטמון כאשר ה-client rects השמורים מאבדים מתוקפם. הערכים של ה-client rect השמור משתנים כאשר המשתמש גולל את העמוד או מוסיף/משנה/מסיר אלמנטים במסך. אז יצרנו פונקציה לניקוי המטמון שאותה נוכל להפעיל בכל פעם שנזדקק לערכי client rect רעננים:
var clearClientRects = function() {
var i;
for ( i = 0; i < elemsWithBoundingRects.length; i++ ) {
if ( elemsWithBoundingRects[ i ] ) {
elemsWithBoundingRects[ i ]._boundingClientRect = null;
}
}
elemsWithBoundingRects = [];
};
יישמנו את אותו מנגנון שמירה במטמון גם עבור קריאות לפונקציות יקרות אחרות שבהן אנו משתמשים בדרך כלל.
תיקון מס' 2: קיבוץ יחד של קריאות וכתיבות באמצעות FastDom
הטמעת תיקון מס' 1 שיפרה את הביצועים של PBS, אך זה לא הספיק.
בחיפוש נוסף בגוגל, נתקלנו במאמר מאת וילסון פייג' בנושא מניעת 'layout thrashing'. וילסון אומר:
Layout Thrashing (reflow) מתרחש כאשר JavaScript כותב וקורא באגרסיביות מה-DOM מספר פעמים, מה שגורם ל-reflows של המסמך.
מתברר שלא מספיק להגביל את הקריאות שלך למאפיינים ולפונקציות ספציפיים המפעילים reflow. כשמבצעים מספר "קריאות" ו"כתיבות" רצופות ב-DOM, הדבר מפעיל גם כן reflow של הדפדפן. הנה תרחיש טיפוסי, ישירות מהאתר של וילסון:
// 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 מכיוון שהמידות של העמוד כולו נפסלות לאחר פעולת כתיבה. התיקון המוצע לכך הוא לסדר מחדש את הקוד כדי לקבץ את כל הקריאות והכתיבות יחד, כך שכל התהליך יפעיל רק 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 אינו מעשי.
למזלנו, וילסון סיפק גם פתרון במאמרו. הוא יצר סקריפט שירות קטן שעוזר לקבץ יחד את אותן קריאות וכתיבות, בשם 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) של האלמנט, ואינו לוקח בחשבון גליונות סגנון; והשני לוקח בחשבון הן סגנונות מוטבעים והן גליונות סגנון.
יתרה מכך, גם כאשר התוצאה של getComputedStyle נשמרה במטמון, היא עדיין הייתה איטית מאוד. כפי שמסתבר, הגישה לאובייקט ה-style עצמו היא התהליך האיטי.
למזלנו, PBS מצרף סגנונות inline לאלמנטים, ומכיוון שלסגנונות inline יש עדיפות (ברוב המקרים) על פני גליונות סגנון, יהיו מקרים שבהם קבלת ערך סגנון ה-inline תספיק. לכן, אם נשלב את שתי השיטות לעיל, נוכל להגיע לקוד שרץ הרבה יותר מהר:
var marginTop = elem.style.marginTop;
if ( ! marginTop ) {
var styles = window.getComputedStyle( elem );
marginTop = styles.marginTop;
}
תוצאות שיפורי הקוד
שלושת התיקונים שלנו לעיל דרשו מחקר רב, בדיקות והטמעה בפועל (ועיכבו את השקת תכונת הגופנים שלנו), אבל בסופו של דבר הכל היה שווה את זה.
בחזרה לבדיקה הראשונית שלנו של הזזת העכבר על פני המסך במשך 2 שניות. הנה ציר הזמן הישן, המכוער והמקרטע:
וכך נראה ציר הזמן החדש שלנו לאחר שיפורי הביצועים:
כפי שניתן לראות, יש פחות דברים סגולים בגרף. ויש הבדל ניכר בכמות התהליכים שמבוצעים על ידי הדפדפן. המלבנים בתחתית דקים יותר גם כן, מה שאומר שתהליכים רצים מהר יותר מבעבר.
אם משווים את כמות האזורים הסגולים הגבוהים/רחבים בגרפים הישן והחדש, אנו רואים עלייה מדהימה של 200% בביצועים! אנו רואים גם עליות בביצועים בכל התחומים. לא רע בכלל!
מסקנות ולקחים
למען האמת, לא חשבנו של-PBS יש בעיות ביצועים לפני שאן העלתה את זה. בעיקר כי השתמשנו במחשבים המיועדים לפיתוח וצווארי הבקבוק בביצועים לא היו כה בולטים. במבט לאחור, בדיקת הביצועים הייתה צריכה להיות אחת מהעדיפויות העליונות. אבל לפחות עכשיו, החל מגרסה 4.3, העניין טופל.
עבור מי שרוצה לשפר את ביצועי תוסף הוורדפרס שלו, במיוחד אם אתם משתמשים גם ב-Javascript כבד, הנה המסקנות שלי מהחוויה הזו:
- הגבילו/שמרו במטמון את הקריאות שלכם למאפיינים/פונקציות המפעילים reflow. השתמשו בזה כמדריך: What forces layout/reflow. The comprehensive list.
- קבצו יחד "קריאות" ו"כתיבות" לסגנונות DOM. השתמשו ב-FastDom כדי לעשות זאת עבורכם.
- השתמשו ב-Timeline profiler של Chrome, וודאו שיש לכם כמה שפחות reflows (החלקים הסגולים).