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

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

  • כדי לראות תצוגה מקדימה של האתר, מקישים על View App ואז על Fullscreen מסך מלא.
  • לוחצים על 'Control+Shift+J' (או 'Command+Option+J' ב-Mac) כדי לפתוח את כלי הפיתוח.
  • לוחצים על הכרטיסייה רשתות.
  • לוחצים על התפריט הנפתח ויסות נתונים, שמוגדר כברירת מחדל כללא ויסות נתונים. בוחרים באפשרות Fast 3G.
  • לוחצים על הלחצן אני רוצה ללחוץ באפליקציה.

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

חלונית הרשת של כלי הפיתוח שבה מוצג קובץ 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. התחל ברמת המסלול. הנתיבים הם הדרך הפשוטה ביותר לזהות נקודות באפליקציה שאפשר לפצל. במסמכי התגובה מוסבר איך אפשר להשתמש ב-Suspense יחד עם react-router.
  2. מזהים בדף באתר רכיבים גדולים שמעובדים רק באינטראקציות מסוימות של המשתמש (כמו לחיצה על לחצן). פיצול הרכיבים האלה יצמצם את המטען הייעודי (payloads) של JavaScript.
  3. כדאי לפצל כל דבר אחר שאינו מועיל במסך ולא קריטי למשתמש.