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

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

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

import React, { lazy } from 'react';

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

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

למה זה שימושי?

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

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

מתח

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

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

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