מקרה לדוגמה מהעולם האמיתי של אופטימיזציה של ביצועים ב-React SPA.
ביצועי אתר לא משפיעים רק על זמן הטעינה. חשוב מאוד לספק למשתמשים חוויה מהירה ורספונסיבית, במיוחד באפליקציות פרודוקטיביות למחשב, שאנשים משתמשים בהן מדי יום. צוות המהנדסים ב-Recruit Technologies השלים פרויקט של טיוב קוד כדי לשפר את אחת מאפליקציות האינטרנט שלהם, AirSHIFT, ולשפר את הביצועים של קלט המשתמש. כך הם עשו זאת.
תגובה איטית, פחות פרודוקטיביות
AirSHIFT היא אפליקציית אינטרנט למחשב שעוזרת לבעלי חנויות, כמו מסעדות ובתי קפה, לנהל את העבודה המגוונת של חברי הצוות שלהם. האפליקציה הזו, שנוצרה באמצעות React, היא אפליקציה עם דף יחיד שמספקת תכונות לקוח עשירות, כולל טבלאות שונות של לוחות זמנים של משמרות שמאורגנות לפי יום, שבוע, חודש ועוד.
כשצוות המהנדסים של Recruit Technologies הוסיף תכונות חדשות לאפליקציית AirSHIFT, הם התחילו לקבל יותר משוב על ביצועים איטיים. מנהל ההנדסה של AirSHIFT, יוסוקה פורוקאווה, אמר:
במחקר שנערך על ידי משתמשים, הופתענו כשאחת מבעלי החנות אמרה שהיא יוצאת מהמושב שלה כדי להכין קפה אחרי שלחץ על לחצן, רק כדי לחסוך זמן בהמתנה עד שטבלת המשמרת תיטען.
אחרי שבחנו את המחקר, צוות המהנדסים הבין שרבים מהמשתמשים מנסים לטעון טבלאות שינוי עצומות במחשבים עם מפרט נמוך, כמו מחשב נייד עם מעבד Celeron M של 1GHz מלפני 10 שנים.
אפליקציית AirSHIFT חסמה את השרשור הראשי באמצעות סקריפטים יקרים, אבל צוות המהנדסים לא הבין כמה הסקריפטים יקרים כי הם פיתחו ובדקו אותם במחשבים עם מפרט עשיר וחיבורי Wi-Fi מהירים.
אחרי יצירת פרופיל של הביצועים בכלי הפיתוח ל-Chrome שבהם הופעלה ויסות נתונים (throttle) במעבד (CPU) וברשת, ברור היה צורך באופטימיזציה של הביצועים. AirSHIFT יצר כוח משימה כדי להתמודד עם הבעיה הזו. ריכזנו כאן 5 דברים שהם התמקדו בהם כדי לשפר את התגובה של האפליקציה שלהם לקלט של המשתמשים.
1. וירטואליזציה של טבלאות גדולות
כדי להציג את טבלת המשמרות נדרשו כמה שלבים יקרים: בניית ה-DOM הווירטואלי והצגתו במסך בהתאם למספר חברי הצוות ולתורים. לדוגמה, אם במסעדה יש 50 עובדים ורוצים לבדוק את לוח המשמרות החודשי שלהם, זה יהיה טבלה של 50 (עובדים) כפול 30 (ימים), וכתוצאה מכך יידרשו 1,500 רכיבי תאים לעיבוד. זו פעולה יקרה מאוד, במיוחד במכשירים עם מפרט דל. בפועל, המצב היה גרוע יותר. מהמחקר הם הבינו שיש חנויות שמנהלות 200 עובדים, ושהן צריכות כ-6,000 רכיבי תאים בטבלה חודשית אחת.
כדי לצמצם את העלות של הפעולה הזו, AirSHIFT הווירטואליזציה את טבלת המשמרות. האפליקציה מעכשיו מחברת רק את הרכיבים שנמצאים באזור התצוגה ומנתקת את הרכיבים שנמצאים מחוץ למסך.
במקרה הזה, ב-AirSHIFT השתמשו ב-react-virtualized כי היו דרישות בנוגע להפעלת טבלאות רשת דו-מימדיות מורכבות. הם גם בודקים דרכים להמיר את ההטמעה כך שתשתמש ב-react-window הקל בעתיד.
תוצאות
הווירטואליזציה של הטבלה לבדה צמצמה את זמן הכתיבה של הסקריפט ב-6 שניות (בסביבה של Macbook Pro עם האטה פי 4 במהירות התגובה של המעבד ועם 3G מהיר עם הגבלת קצב העברת נתונים). זה היה השיפור המשמעותי ביותר בביצועים בפרויקט הארגון מחדש.
2. ביקורת באמצעות User Timing API
בשלב הבא, צוות AirSHIFT ארגן מחדש את הסקריפטים שרצים על קלט של משתמשים. תרשים הלהבות של כלי הפיתוח ל-Chrome מאפשר לנתח מה קורה בפועל ב-thread הראשי. אבל לצוות AirSHIFT היה קל יותר לנתח את פעילות האפליקציה על סמך מחזור החיים של React.
ב-React 16 אפשר לראות את מעקב הביצועים באמצעות User Timing API, וניתן להציג אותו בקטע Timings בכלי הפיתוח ל-Chrome. ב-AirSHIFT השתמשו בקטע Timings כדי למצוא לוגיקה מיותרת שפועלת באירועים של מחזור החיים של React.
תוצאות
צוות AirSHIFT גילה שReact Tree Reconciliation מתבצע באופן מיותר ממש לפני כל ניווט בנתיב. פירוש הדבר הוא ש-React עדכן את טבלת השינויים שלא לצורך לפני הניווטים. הבעיה נגרמה כתוצאה מעדכון מיותר של מצב Redux. התיקון חסך כ-750 אלפיות השנייה של זמן כתיבת סקריפטים. כמו כן, AirSHIFT ביצעו אופטימיזציות אחרות ברמת המיקרו, שבסופו של דבר הובילו לירידה של שנייה אחת בזמן הכולל של כתיבת הסקריפטים.
3. טעינה מדורגת של רכיבים והעברת לוגיקה יקרה ל-web workers
ל-AirSHIFT יש אפליקציית צ'אט מובנית. בעלי חנויות רבים מתכתבים עם חברי הצוות שלהם בצ'אט בזמן שהם צופים בטבלת המשמרות. לכן, יכול להיות שמשתמש יתקליד הודעה בזמן שהטבלה נטענת. אם ה-thread הראשי מכיל סקריפטים שמבצעים עיבוד של הטבלה, הקלט של המשתמשים עלול להיות משובש.
כדי לשפר את החוויה הזו, AirSHIFT משתמש עכשיו ב-React.lazy וב-Suspense כדי להציג תוכן placeholder לתוכן הטבלה בזמן הטעינה האיטית של הרכיבים בפועל.
צוות AirSHIFT העביר גם חלק מהלוגיקה העסקית היקרה בתוך הרכיבים שנטענים באיטרציה לWeb Workers. כך פתרנו את הבעיה של קפיצות בקלט של המשתמשים על ידי שחרור של שרשור הראשי כדי שיוכל להתמקד בתגובה לקלט של המשתמשים.
בדרך כלל, מפתחים נתקלים בבעיות מורכבות כשהם משתמשים ב-workers, אבל הפעם Comlink עשתה את העבודה הקשה בשבילם. בהמשך מופיע פסאודו-קוד שמראה איך AirSHIFT הגדירה כוח עבודה לאחת מהפעולות היקרות ביותר שהיו לה: חישוב עלויות העבודה הכוללות.
ב-App.js, משתמשים ב-React.lazy וב-Suspense כדי להציג תוכן חלופי בזמן הטעינה
/** App.js */
import React, { lazy, Suspense } from 'react'
// Lazily loading the Cost component with React.lazy
const Hello = lazy(() => import('./Cost'))
const Loading = () => (
<div>Some fallback content to show while loading</div>
)
// Showing the fallback content while loading the Cost component by Suspense
export default function App({ userInfo }) {
return (
<div>
<Suspense fallback={<Loading />}>
<Cost />
</Suspense>
</div>
)
}
ברכיב Cost, משתמשים ב-comlink כדי להפעיל את לוגיקת החישוב
/** Cost.js */
import React from 'react';
import { proxy } from 'comlink';
// import the workerlized calc function with comlink
const WorkerlizedCostCalc = proxy(new Worker('./WorkerlizedCostCalc.js'));
export default async function Cost({ userInfo }) {
// execute the calculation in the worker
const instance = await new WorkerlizedCostCalc();
const cost = await instance.calc(userInfo);
return <p>{cost}</p>;
}
הטמעת הלוגיקה של החישוב שפועלת ב-worker וחשיפה שלה באמצעות comlink
// WorkerlizedCostCalc.js
import { expose } from 'comlink'
import { someExpensiveCalculation } from './CostCalc.js'
// Expose the new workerlized calc function with comlink
expose({
calc(userInfo) {
// run existing (expensive) function in the worker
return someExpensiveCalculation(userInfo);
}
}, self);
תוצאות
למרות הכמות המוגבלת של הלוגיקה שהם העבירו ל-worker בתור ניסיון, ב-AirSHIFT העבירו כ-100 אלפיות השנייה של JavaScript מהשרשור הראשי לשרשור ה-worker (בסימולציה עם צמצום של 4x ב-CPU).
ב-AirSHIFT בודקים כרגע אם אפשר להשתמש בטעינה איטית של רכיבים אחרים ולהעביר יותר לוגיקה ל-web workers כדי לצמצם עוד יותר את התנודות.
4. הגדרת תקציב ביצועים
אחרי שהטמענו את כל פעולות האופטימיזציה האלה, היה חשוב לוודא שהאפליקציה תמשיך לפעול בצורה טובה לאורך זמן. עכשיו AirSHIFT משתמש במאפיין bundlesize כדי שלא לחרוג מהגודל הנוכחי של קובץ JavaScript ו-CSS. מעבר להגדרת התקציבים הבסיסיים, הם יצרו לוח בקרה שמציג אחוזונים שונים של זמן הטעינה של טבלת השינוי, כדי לבדוק אם האפליקציה מניבה ביצועים גם בתנאים לא אידיאליים.
- עכשיו מתבצעת מדידה של זמן השלמת הסקריפט של כל אירוע Redux
- נתוני הביצועים נאספים ב-Elasticsearch
- נתוני הביצועים של כל אירוע באחוזון ה-10, ה-25, ה-50 וה-75 מוצגים באופן חזותי באמצעות Kibana
AirSHIFT עוקב עכשיו אחרי אירוע הטעינה של טבלת המשמרות כדי לוודא שהוא מסתיים תוך 3 שניות אצל 75% מהמשתמשים. בשלב הזה התקציב לא נאכף, אבל הם שוקלים אם לשלוח להם התראות אוטומטיות דרך Elasticsearch אם הם חורגים מהתקציב.
תוצאות
לפי התרשים שלמעלה, אפשר לראות ש-AirSHIFT משיג עכשיו ברוב המקרים את התקציב של 3 שניות למשתמשים באחוזון ה-75, וגם טבלת המעבר נטענת תוך שנייה למשתמשים באחוזון ה-25. בעזרת תיעוד נתוני הביצועים של RUM בתנאים ובמכשירים שונים, AirSHIFT יכול עכשיו לבדוק אם השקת תכונה חדשה משפיעה בפועל על ביצועי האפליקציה או לא.
5. האקתונים בנושא ביצועים
כל המאמצים האלה לשיפור הביצועים היו חשובים ומשמעותיים, אבל לא תמיד קל לגרום לצוותים של מהנדסים ושל עסקים לתת עדיפות לפיתוח לא פונקציונלי. חלק מהאתגר הוא שאי אפשר לתכנן חלק מהשיפורים האלה בביצועים. הם מצריכים ניסויים ומצב רוח של ניסוי וטעיה.
ב-AirSHIFT מתקיימים עכשיו אירועי האקתון פנימיים של יום אחד בנושא ביצועים, כדי לאפשר למהנדסים להתמקד רק בעבודה שקשורה לביצועים. באירועי ה-hackathon האלה הם מסירים את כל האילוצים ומכבדים את היצירתיות של המהנדסים, כלומר כדאי לשקול כל הטמעה שתורמת למהירות. כדי לזרז את ההתקדמות ב-hackathon, צוות AirSHIFT מפצל את הקבוצה לצוותים קטנים, וכל צוות מתחרה על השיפור הגדול ביותר בציון הביצועים ב-Lighthouse. התחרות בין הקבוצות הולכת ומתחממת! 🔥
תוצאות
גישת ההאקאתון עובדת טוב בשבילם.
- כדי לזהות בקלות צווארי בקבוק בביצועים, אפשר לנסות כמה גישות במהלך ה-hackathon ולמדוד כל אחת מהן באמצעות Lighthouse.
- אחרי ההאקתון, קל למדי לשכנע את הצוות באילו שיפורים צריך לתת עדיפות לקראת השקת המוצר בסביבת הייצור.
- זו גם דרך יעילה להדגיש את החשיבות של המהירות. כל אחד מהמשתתפים יכול להבין את הקשר בין אופן התכנות לבין הביצועים שלו.
תופעת לוואי חיובית הייתה שצוותי מהנדסים רבים אחרים ב-Recruit התעניינו בגישה המעשית הזו, וצוות AirSHIFT מפעיל עכשיו כמה אירועי speed hackathon בחברה.
סיכום
לא הייתה זו דרך קלה עבור AirSHIFT לבצע את האופטימיזציות האלה, אבל התוצאות בהחלט הצדיקו את המאמץ. עכשיו AirSHIFT טוענת את טבלת המשמרות תוך 1.5 שניות בממוצע, שיפור של פי 6 לעומת הביצועים שלה לפני הפרויקט.
לאחר השקת האופטימיזציה של הביצועים, משתמש אחד אמר:
תודה רבה שעזרת לנו לטעון את טבלת השינויים במהירות. עכשיו הרבה יותר קל לתזמן את משמרות העבודה.