חמש דרכים שבהן AirSHIFT שיפרה את הביצועים בזמן הריצה של אפליקציית React

מקרה לדוגמה מהעולם האמיתי של אופטימיזציית הביצועים של React SPA.

Kento Tsuji
Kento Tsuji
Satoshi Arai
Satoshi Arai
Yusuke Utsunomiya
Yusuke Utsunomiya
Yosuke Furukawa
Yosuke Furukawa

ביצועי אתרים אינם תלויים רק בזמן טעינה. חשוב מאוד לספק למשתמשים חוויה מהירה ורספונסיבית, במיוחד באפליקציות לשיפור הפרודוקטיביות במחשב שמשתמשים בהן מדי יום. צוות מהנדסי התוכנה ב-Recruit Technologies ביצע פרויקט של ארגון מחדש כדי לשפר את אחת מאפליקציות האינטרנט שלהם, AirSHIFT, כדי לשפר את ביצועי הקלט של המשתמשים. זה מה שהם עשו.

תגובה איטית, פחות פרודוקטיביות

AirSHIFT היא אפליקציית אינטרנט למחשבים שולחניים שעוזרת לבעלי חנויות, כמו מסעדות ובתי קפה, לנהל את עבודת המשמרות של חברי הצוות שלהם. האפליקציה של דף יחיד, שנוצרת באמצעות React, מספקת תכונות לקוח עשירות, כולל טבלאות רשת שונות של לוחות זמנים למשמרות המאורגנים לפי יום, שבוע, חודש ועוד.

צילום מסך של אפליקציית האינטרנט AirSHIFT.

כשצוות המהנדסים של Recruit Technologies הוסיף תכונות חדשות לאפליקציית AirSHIFT, הם התחילו לקבל יותר משובים לגבי ביצועים איטיים. יוסוקה פורוקאווה, מנהל ההנדסה של AirSHIFT, אמר:

במחקר על התנהגות משתמשים, הופתענו כשאחת מבעלי החנות אמרה שהיא תעזוב את המושב כדי להכין קפה אחרי לחיצה על לחצן, רק כדי לקצר את הזמן בהמתנה לטעינה של שולחן המשמרת.

לאחר שעברו את המחקר, צוות מהנדסי התוכנה הבין שרבים מהמשתמשים שלהם ניסו לטעון לפני 10 שנים טבלאות ניידות מסיביות במחשבים עם מפרטים נמוכים, כמו מחשב נייד מסוג Celeron M בהספק של 1 GHz.

מסתובב אינסופי במכשירים פשוטים.

אפליקציית AirSHIFT חסמה את ה-thread הראשי באמצעות סקריפטים יקרים, אבל צוות מהנדסי התוכנה לא הבינו את עלות הסקריפטים היקרים כי הם בפיתוח ובדיקה במחשבים עם מפרטים עשירים וחיבורי Wi-Fi מהירים.

תרשים שמציג את הפעילות של האפליקציה בזמן הריצה.
בעת טעינת טבלת Shift, הרצת סקריפטים צורכה כ-80% מזמן הטעינה.

אחרי שליצור פרופיל של הביצועים שלהם בכלי הפיתוח ל-Chrome ולהפעיל את ויסות המעבד (CPU) והרשת, התברר שצריך לבצע אופטימיזציה של הביצועים. AirSHIFT הקים צוות ייעודי להתמודדות עם הבעיה הזו. ריכזנו 5 דברים שהם התמקדו בהם כדי שהאפליקציה תהיה יותר תגובה לקלט של משתמשים.

1. הפיכת טבלאות גדולות לווירטואליות

הצגת טבלת המשמרות דרשה מספר שלבים יקרים: בניית ה-DOM הווירטואלי ורינדור שלו על המסך באופן יחסי למספר חברי הצוות ומשבצות הזמן. לדוגמה, אם במסעדה היו 50 עובדים ואתם רוצים לבדוק את לוח הזמנים של המשמרת החודשית, תוכלו להציג טבלה של 50 (חברים) כפול 30 (ימים). כך ייווצרו 1,500 רכיבים של תאים. זוהי פעולה יקרה מאוד, במיוחד למכשירים עם מפרטים נמוכים. במציאות, המצב היה גרוע יותר. מהמחקר עולה כי חנויות שמנהלות 200 חברי צוות, נדרשו כ-6,000 רכיבים של תאים בטבלה חודשית אחת.

כדי להפחית את עלות הפעולה הזו, AirSHIFT ביצע וירטואליזציה של טבלת המשמרות. האפליקציה טוענת עכשיו רק את הרכיבים בתוך אזור התצוגה ומנתקת את הרכיבים מחוץ למסך.

צילום מסך עם הערות שמראה שהשימוש ב-AirSHIFT לעיבוד תוכן מתבצע מחוץ לאזור התצוגה.
לפני: רינדור כל התאים בטבלת Shift.
צילום מסך עם הערות שמראה שעכשיו AirSHIFT מעבד רק תוכן שמופיע באזור התצוגה.
אחרי: רינדור רק של התאים בתוך אזור התצוגה.

במקרה הזה, AirSHIFT השתמשה בתגובה וירטואלית כי היו דרישות לגבי הפעלת טבלאות רשת דו-ממדיות מורכבות. הם גם בוחנים דרכים להמיר את ההטמעה לשימוש ב-react-window בעתיד.

תוצאות

וירטואליזציה של הטבלה בלבד הפחיתה את זמן הסקריפט ב-6 שניות (בסביבת האטה פי 4 של יחידת העיבוד המרכזית) + בסביבת Macbook Pro מהירה שמווסתת על ידי 3G). זה היה השיפור המשמעותי ביותר בביצועים בפרויקט של ארגון הקוד מחדש (Refactoring).

צילום מסך עם הערות של הקלטה בלוח הביצועים של כלי הפיתוח ל-Chrome.
לפני: כ-10 שניות של כתיבת סקריפטים לאחר קלט של משתמשים.
צילום מסך נוסף עם הערות של הקלטה בלוח הביצועים של כלי הפיתוח ל-Chrome.
לאחר: 4 שניות של כתיבת סקריפטים לאחר קלט של משתמשים.

2. ביקורת באמצעות User Timing API

בשלב הבא, צוות AirSHIFT ארגן מחדש את הסקריפטים שפועלים בקלט של משתמשים. תרשים הלהבות של כלי הפיתוח ל-Chrome מאפשר לנתח את מה שקורה ב-thread הראשי. אבל לצוות AirSHIFT קל יותר לנתח את הפעילות באפליקציות על סמך מחזור החיים של React.

React 16 מספק את דוח הביצועים שלו דרך User Timing API, שאותו אפשר להמחיש דרך הקטע Timings בכלי הפיתוח ל-Chrome. חברת AirSHIFT השתמשה בקטע 'תזמונים' כדי למצוא לוגיקה מיותרת שרצה באירועי מחזור החיים של React.

הקטע 'תזמונים' בחלונית 'ביצועים' בכלי הפיתוח ל-Chrome.
האירועים של React User Timing.

תוצאות

צוות AirSHIFT גילה שהתאמה של עץ התגובה לא נעשתה ממש לפני כל ניווט במסלול. כלומר, בוצע עדכון של טבלת החלפת הנתונים ב-React שלא לצורך לפני הניווטים. עדכון מיותר של מצב Redux גרם לבעיה הזו. תיקון הבעיה חסך כ-750 אלפיות השנייה של זמן כתיבת סקריפטים. בנוסף, AirSHIFT ביצעה גם אופטימיזציות מיקרו נוספות שבסופו של דבר הובילו לקצר שנייה אחת במשך הזמן הכולל של כתיבת הסקריפטים.

3. רכיבי טעינה עצלה והעברת לוגיקה יקרה לעובדי אינטרנט

AirSHIFT כולל אפליקציית צ'אט מובנית. בעלי חנויות רבים מתקשרים עם חברי הצוות שלהם באמצעות הצ'אט בזמן שמעיינים בטבלת המשמרות. פירוש הדבר הוא שמשתמש עשוי להקליד הודעה בזמן שהטבלה נטענת. אם ה-thread הראשי מלא בסקריפטים שמעבדים את הטבלה, יכול להיות שקלט המשתמשים יהיה איטי.

כדי לשפר את החוויה, AirSHIFT משתמש עכשיו ב-React.lazy ו-Suspense כדי להציג placeholders לתוכן של טבלה בזמן הטעינה של הרכיבים עצמם.

צוות AirSHIFT גם העביר חלק מהלוגיקה העסקית היקרה של הרכיבים שנטענים באופן מדורג לעובדי אינטרנט. הפעולה הזו פתרה את הבעיה של בעיות jank בקלט של משתמשים וששחררה את ה-thread הראשי, כך שיוכל להתמקד בתגובה לקלט של משתמשים.

בדרך כלל מפתחים מתמודדים עם מורכבות בשימוש של עובדים, אבל הפעם 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>;
}

הטמעה של לוגיקת החישוב שפועלת בעובד וחשיפה שלו באמצעות 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);

תוצאות

למרות הכמות המוגבלת של הלוגיקה שבה הם עבדו כניסוי, AirSHIFT הזיז את ה-JavaScript בערך 100 אלפיות השנייה מה-thread הראשי ל-thread של ה-worker (עם הדמיה של ויסות נתונים (throttle) פי 4 של המעבד (CPU).

צילום מסך של הקלטה בחלונית הביצועים של כלי הפיתוח ל-Chrome, שמראה שהסקריפט מתרחש עכשיו ב-Web worker ולא ב-thread הראשי.

בשלב זה, AirSHIFT בודקת אם הם יכולים לטעון רכיבים אחרים באופן הדרגתי ולבטל את עומס הלוגיקה של עובדי האינטרנט כדי לצמצם עוד יותר את ה-jank.

4. הגדרת תקציב ביצועים

לאחר יישום כל האופטימיזציות האלה, היה חשוב מאוד לוודא שהאפליקציה תמשיך לפעול לאורך זמן. עכשיו AirSHIFT משתמש ב-bundlesize כדי לא לחרוג מהגודל הנוכחי של קובץ ה-JavaScript וה-CSS. מלבד הגדרת התקציבים הבסיסיים האלה, הם בנו מרכז בקרה שמציג אחוזים שונים של זמן הטעינה של טבלת המשמרות, כדי לבדוק אם האפליקציה מניבה ביצועים גם בתנאים לא אידיאליים.

  • זמן ההשלמה של הסקריפט עבור כל אירוע Redux נמדד עכשיו
  • נתוני הביצועים נאספים ב-Elasticsearch
  • הביצועים של כל אירוע באחוזון ה-10, ה-25, ה-50 וה-75 של כל אירוע מוצגים באופן ויזואלי באמצעות Kibana

AirSHIFT עוקב עכשיו אחרי אירוע הטעינה של טבלת המשמרת כדי לוודא שהוא יושלם תוך 3 שניות עבור המשתמשים באחוזון ה-75. התקציב הזה לא נאכף בשלב הזה, אבל הם שוקלים לקבל התראות אוטומטיות דרך Elasticsearch כשהם חורגים מהתקציב.

תרשים שמראה שהמאון ה-75 מסתיים בערך ב-2,500 אלפיות השנייה, האחוזון ה-50 ב-1,250 אלפיות השנייה, המאון ה-25 ב-750 אלפיות השנייה בערך ומאון ה-10 בסביבות 500 אלפיות השנייה.
לוח הבקרה של Kibana שבו מוצגים נתוני ביצועים יומיים לפי אחוזים.

תוצאות

מהתרשים שלמעלה, אפשר לראות ש-AirSHIFT מגיעה עכשיו בעיקר לתקציב של 3 שניות למשתמשים באחוזון ה-75. בנוסף, היא טוענת את טבלת ההתאמות בתוך שנייה עבור משתמשים באחוזון ה-25. באמצעות תיעוד נתוני הביצועים של RUM מתנאים וממכשירים שונים, AirSHIFT יכול לבדוק עכשיו אם גרסת תכונה חדשה משפיעה בפועל על ביצועי האפליקציה או לא.

5. אירועי האקאתון

למרות שכל המאמצים האלה לאופטימיזציית הביצועים היו חשובים ואפקטיביים, לא תמיד קל לבקש מצוותי ההנדסה והעסקים לתת עדיפות לפיתוח לא פעיל. חלק מהאתגר הוא שחלק מהאופטימיזציות האלה של הביצועים לא ניתנות לתכנון. הם דורשים חשיבה של ניסוי וטעייה.

AirSHIFT מקיים עכשיו אירועי האקאתון פנימיים של יום אחד כדי לאפשר למהנדסים להתמקד רק בעבודה הקשורה לביצועים. באירועי ההאקאתון האלה, הם מסירים את כל האילוצים ומכבדים את היצירתיות של המהנדסים. כלומר, כדאי לשקול כל הטמעה שמשפיעה על המהירות. כדי להאיץ את ההאקאתון, AirSHIFT מפצל את הקבוצה לצוותים קטנים, וכל צוות מתחרה כדי לבדוק מי יכול להשיג את השיפור הגדול ביותר בציון הביצועים של Lighthouse. הקבוצות יהיו תחרותיות מאוד! 🔥

תמונות של האקאתון.

תוצאות

הגישה של ההאקאתון עובדת היטב עבורם.

  • כדי לזהות בקלות צווארי בקבוק בביצועים, מנסים כמה גישות במהלך ההאקאתון ומודדים כל אחת מהן באמצעות Lighthouse.
  • אחרי ההאקאתון, קל יותר לשכנע את הצוות איזו אופטימיזציה כדאי לתת לו עדיפות בגרסת הייצור.
  • זו גם דרך יעילה לקדם את חשיבות המהירות. כל משתתף יכול להבין את הקשר בין אופן הקוד לבין האופן שבו הוא מניב ביצועים.

הרבה צוותי הנדסה אחרים ב-Recruit התעניינו בגישה המעשית הזו, וצוות AirSHIFT מאפשר עכשיו מספר רב של אירועי האקאתון במסגרת החברה.

סיכום

זה בהחלט לא היה המסע הקל ביותר של AirSHIFT לעבוד על האופטימיזציות האלה, אבל זה בהחלט השתלם. עכשיו AirSHIFT טוענת את טבלת המשמרות בחציון של 1.5 שניות בחציון, שיפור פי 6 בהשוואה לביצועים שלה לפני הפרויקט.

לאחר השקת האופטימיזציות של ביצועים, משתמש אחד אמר:

תודה רבה על המהירות של טבלת המשמרת. עכשיו סידור המשמרות הרבה יותר יעיל.