פיצול קוד באמצעות React.lazy ו-Suspense

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

השיטה React.lazy מאפשרת לפצל בקלות את הקוד של אפליקציית React ברמת הרכיב באמצעות ייבוא דינמי.

import React, { lazy } from 'react';

const AvatarComponent = lazy(() => import('./AvatarComponent'));

const DetailsComponent = () => (
  <div>
    <AvatarComponent />
  </div>
)

למה המידע הזה מועיל?

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

הפונקציה React.lazy מספקת דרך מובנית להפריד רכיבים באפליקציה לקטעי JavaScript נפרדים, בלי הרבה עבודה. לאחר מכן תוכלו לטפל בעומס המצבים כשאתם משלבים אותו עם הרכיב Suspense.

מתח

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

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

import React, { lazy, Suspense } from 'react';

const AvatarComponent = lazy(() => import('./AvatarComponent'));

const renderLoader = () => <p>Loading</p>;

const DetailsComponent = () => (
  <Suspense fallback={renderLoader()}>
    <AvatarComponent />
  </Suspense>
)

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

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

כדי להמחיש טוב יותר איך זה עובד:

  • כדי לראות תצוגה מקדימה של האתר, לוחצים על הצגת האפליקציה. לאחר מכן לוחצים על מסך מלא מסך מלא.
  • מקישים על Control+Shift+J (או על Command+Option+J ב-Mac) כדי לפתוח את DevTools.
  • לוחצים על הכרטיסייה רשתות.
  • לוחצים על התפריט הנפתח Throttling (ויסות נתונים). כברירת מחדל, האפשרות שמוגדרת בתפריט היא No throttling (ללא ויסות נתונים). בוחרים באפשרות 3G מהיר.
  • לוחצים על הלחצן Click Me באפליקציה.

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

חלונית הרשת של DevTools שבה מוצג קובץ chunk.js אחד שהורד

השעיה של כמה רכיבים

תכונה נוספת של Suspense היא האפשרות להשעות את הטעינה של כמה רכיבים, גם אם כולם נטענים באיטרציה.

לדוגמה:

import React, { lazy, Suspense } from 'react';

const AvatarComponent = lazy(() => import('./AvatarComponent'));
const InfoComponent = lazy(() => import('./InfoComponent'));
const MoreInfoComponent = lazy(() => import('./MoreInfoComponent'));

const renderLoader = () => <p>Loading</p>;

const DetailsComponent = () => (
  <Suspense fallback={renderLoader()}>
    <AvatarComponent />
    <InfoComponent />
    <MoreInfoComponent />
  </Suspense>
)

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

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

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

טיפול בכשלים בטעינת הנתונים

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

ל-React יש תבנית רגילה לטיפול בצורה חלקה בסוגים האלה של כשלים בחיבור: שימוש בגבול שגיאה. כפי שמתואר במסמכי העזרה, כל רכיב React יכול לשמש כגבול שגיאה אם הוא מטמיע את אחת (או את שתיהן) משיטות מחזור החיים static getDerivedStateFromError() או componentDidCatch().

כדי לזהות כשלים בטעינת פריטים בזמן אמת ולטפל בהם, אפשר לעטוף את הרכיב Suspense ברכיבי הורה שמשמש כגבול שגיאה. בתוך השיטה render() של גבול השגיאה, אפשר להציג את הצאצאים כפי שהם אם אין שגיאה, או להציג הודעת שגיאה בהתאמה אישית אם משהו משתבש:

import React, { lazy, Suspense } from 'react';

const AvatarComponent = lazy(() => import('./AvatarComponent'));
const InfoComponent = lazy(() => import('./InfoComponent'));
const MoreInfoComponent = lazy(() => import('./MoreInfoComponent'));

const renderLoader = () => <p>Loading</p>;

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = {hasError: false};
  }

  static getDerivedStateFromError(error) {
    return {hasError: true};
  }

  render() {
    if (this.state.hasError) {
      return <p>Loading failed! Please reload.</p>;
    }

    return this.props.children;
  }
}

const DetailsComponent = () => (
  <ErrorBoundary>
    <Suspense fallback={renderLoader()}>
      <AvatarComponent />
      <InfoComponent />
      <MoreInfoComponent />
    </Suspense>
  </ErrorBoundary>
)

סיכום

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

  1. מתחילים ברמת המסלול. נתיבים הם הדרך הפשוטה ביותר לזהות נקודות באפליקציה שאפשר לפצל. במסמכי העזרה של React מוסבר איך משתמשים ב-Suspense יחד עם react-router.
  2. זיהוי רכיבים גדולים בדף באתר שנעשים זמינים רק באינטראקציות מסוימות של משתמשים (למשל, לחיצה על לחצן). פיצול הרכיבים האלה יעזור לצמצם את המטען הייעודי (payload) של JavaScript.
  3. כדאי לפצל כל תוכן אחר שלא מופיע במסך ולא קריטי למשתמש.