פיצול קוד באמצעות 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. כדאי לפצל כל תוכן אחר שלא מופיע במסך ולא קריטי למשתמש.