פיצול קוד באמצעות 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) כדי לפתוח את כלי הפיתוח.
  • לוחצים על הכרטיסייה רשת.
  • לוחצים על התפריט הנפתח Throttling (ויסות נתונים), שמוגדר כברירת מחדל לNo throttling (ללא ויסות נתונים). בוחרים באפשרות Fast 3G (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. כדאי לפצל כל תוכן אחר שלא מוצג במסך ולא קריטי למשתמש.