Галоўная / Артыкулы

Завантажэнне, чаканне, абяроны: як падходзяць межы ледачасці, напэўнення і абяроны ад памылак

З’ясавайце, як React.lazy, Suspense і Error Boundaries дзельнуюць код, прадстаўляюць альтернатывны варыянты і запобегаюць збоям, а таксама дзе трэба размістыць кожны з іх для кращага корыстнікаўскога досвяду.

Выдавец: REACTAPP.TOP9 хв чытання1968 слоў

Крыніцы: React Suspense, Lazy Loading & Error Boundaries — A Practical Guide

Экран у React можа знаходзіцца ў трох вадозлежных станах: код не быў завантажаны, контэнт не готавы, або выйшла памылка пад час відраслівання. React.lazy адкладае завантажэння коду, Suspense паказвае заместнік пакуль ўсё не будзе готава, а межа памылак заменяе зламаны паддрэс на альтернатыўны варіант. Калі вы зрозумеете, як усе гэта взаімаўпрацоўвае, вы зможаце розумна раздзеляць пакеты коду і ставіць межы, якія будуць прабавляць залишак сторанкі.

Адкладэнне завантажэння коду за дапамою React.lazy

«Леніва загрузка» означае, што код компаненту запрашываецца толькі тады, калі React пачаткова павінен яго атрыбутаваць, а не як частка пачатковага пакета. Разлік видны ў імпорце: статычны import Dashboard from "./Dashboard" мгновэна запрашывае модуль, тады як дынамічны імпорт, пакаваны ў lazy(), дазволяе інструменту пакавання выдаты окольны чанк. (У атласе другі імпорт паказан толькі для порэвняння.)

import { lazy } from "react";

const Dashboard = lazy(() => import("./Dashboard"));

Instead of:

import Dashboard from "./Dashboard";

Што дае «леніва загрузка»

Уявіцеся прыклад аплікацыі з калькама разных рэгіонаў:

Application
├── Home
├── Products
├── Dashboard
├── Analytics
├── Admin
└── Payment

Ўжохадзячы на старонцу «Галоўная», корыстнік яшчо не мае патрэбы ў іх:

Analytics
Admin
Payment

Їх загрузка ў будзь-якім случае павялічвае размер пачатковага завантажэння. За дапамогою «ленівай загрузкі» іншыя чанкі запрашываюцца толькі пад час патрэбы:

Initial Load
     ↓
Home + common code
     ↓
User opens Dashboard
     ↓
Download Dashboard chunk
     ↓
Render Dashboard

Як функціонуе «леніва загрузка» занутра

lazy() прыймае функцыю, яка вяртае праміс, зазвычай дынамічны import(). Цэльны модуль павінен матыць стандартны экспорт, які ўжо є компонентам. Працэс выглядае так (дыяграма ўявная, а не код):

const Dashboard = lazy(() => import("./Dashboard"));

Conceptually:

lazy()
  ↓
dynamic import()
  ↓
JavaScript chunk requested
  ↓
Promise pending
  ↓
Component becomes available
  ↓
React renders component

Існуюць два практычныя правіла. Компоненты, якія працюють у режыме lazy, трэба адзначаць на верхнім роўні модуля, ніколі не ўсередзіні іншага компонента, іначы з кожным перзапісам будуе стварацца новы тип компонента і скасывацца яго стан. А так как даныя прыходзяць асінхронна, трэба, каб хтось вялікі рашыў, што будзе паказана корыстніку тым часам. Гэта заведома Suspense.

Паказанне падместа з вялікай Suspense

Suspense паказвае альтэрнатыўны элемент, паколькі які-небудзь з наследнікаў застаецца у стане паузы, тое ўсё ещэ не можа быць адрасаваны. Трэба загорнуць компонент, які працюе у режыме lazy, і перадаць елемент-замену:

import { lazy, Suspense } from "react";

const Dashboard = lazy(() => import("./Dashboard"));

function App() {
  return (
    <Suspense fallback={<p>Loading dashboard...</p>}>
      <Dashboard />
    </Suspense>
  );
}

Калі частка завантажаецца, React падыходзіць да найбліжэйшага межовага і атрыбутуе свой запасны варыянт:

Dashboard
    ↓
Not ready
    ↓
Suspense
    ↓
"Loading dashboard..."

Калі запаведзанне вырашваецца, React зноў прабуе атрыбутаваць і справжній кантэнт заменяе запасны варыянт:

Dashboard loaded
      ↓
React retries rendering
      ↓
Dashboard displayed

Suspense не адпавядае за завантажэнне; lazy() (альбо бібліятэка дадзеных з падтрымкай Suspense) запускае гэты процес. Suspense толькі пазначае, дзе можа з’явіцца стан чакання і як ён выглядае.

Іспользованне lazy і Suspense разам

Большасць рэальных кодаў спалюе гэтыя два элементы, зазвычай з болей насыценым запасным варыянтам, чым простае рэченне:

const Dashboard = lazy(() => import("./Dashboard"));

function App() {
  return (
    <Suspense fallback={<DashboardSkeleton />}>
      <Dashboard />
    </Suspense>
  );
}

Полны жыцёвы цикл першага адвізу, знову ў вигляде дыяграмы:

User opens Dashboard
        ↓
React tries to render Dashboard
        ↓
Dashboard chunk isn't available
        ↓
Dynamic import starts
        ↓
Suspense fallback shown
        ↓
Dashboard chunk loads
        ↓
React retries rendering
        ↓
Dashboard displayed

Скелеты заместо індыкатора завантажэння

Просты текстовы запасны варыянт працюе, але ён нічога не паведамляе корыстніку пра тое, што будзе практычна:

<Suspense fallback={<p>Loading...</p>}>

Компанент-скелет, які імітуюць гэты распадак, зазвычай яўляецца кращым выборам:

<Suspense fallback={<DashboardSkeleton />}>

Ён звільняе прыблізна правы прастор, зменшаючы зсув распадака, і робіць час чакання менш дзяўгім:

┌─────────────────────────────┐
│ Dashboard                   │
├─────────────────────────────┤
│ ░░░░░░░   ░░░░░░░           │
│ ░░░░░░░   ░░░░░░░           │
│                             │
│ ░░░░░░░░░░░░░░░░░░          │
└─────────────────────────────┘

Вкладанне меж Suspense

Одна межа для всей аплікацыі рэдкая раз калі ўсё, чаго вы хочаце. Межы можна вкладваць, і кожны компанент у стане Suspense падпадае пад найбліжэю межу вышэй:

<Suspense fallback={<PageSkeleton />}>
  <Header />

  <Suspense fallback={<ProductsSkeleton />}>
    <Products />
  </Suspense>

  <Suspense fallback={<RecommendationsSkeleton />}>
    <Recommendations />
  </Suspense>
</Suspense>

У замен таго, каб весь старонік ператворыўся на аднае паведамленне пра завантажэння,

Entire page → Loading...

кожны раздзіл можа апавесціцца самастоятельна:

Header                  ✅

Products                Loading...
[Product Skeleton]

Recommendations         Loading...
[Recommendation Skeleton]

Ураджэнне адказоў за бяды з межамі Error

Чаканне — гэта адна з можлівых ситуацыяў; бяда — іншая. Падазроўваю, што компанент кідае адказ пад час рэндарывання:

function Dashboard() {
  throw new Error("Dashboard crashed!");
}

Неконтрольваныя памылкі пад час адраджэння выключаюць усю коранавую структуру React, залишаючы порожню стораніцу. Межа памылак — это компонент, який перехопляе памылкі, якія выкалываюць його наследнікі, і паказвае альтернатыўны інтерфейс:

Dashboard
    ↓
Rendering Error
    ↓
Error Boundary
    ↓
Fallback UI

Їе выкарыстоўванне адначынаеся да таго, што яна стае часткай будь-якога іншага компонента, і корыстувач бачыць паведамленне „Осё пайшло не так“ заместо порожньяго экрана:

<ErrorBoundary>
  <Dashboard />
</ErrorBoundary>

Стварэнне межы памылак

Межы памылак яшчэ ўважаюцца класовымі компонентамі; у самам React няма эквівалента для гакоў. Важныя два методы: статычны getDerivedStateFromError, які выконваецца пад час адраджэння і вяртае стан, які пераключаеся на альтернатыўны варыянт, і componentDidCatch, які выконваецца пасля таго з самай памылкай і info.componentStack, які служыць для логавання або надсылкі звястак.

import React from "react";

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      hasError: false,
    };
  }

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

  componentDidCatch(error, info) {
    console.error(error);
    console.error(info.componentStack);
  }

  render() {
    if (this.state.hasError) {
      return <h2>Something went wrong.</h2>;
    }

    return this.props.children;
  }
}

Їе выкарыстоўваюць абсолютна так сама, як у паказаным раней прыкладзе:

<ErrorBoundary>
  <Dashboard />
</ErrorBoundary>

У гэтай мінімальной версіі няма функцыі перазначэння стану; кантактны кранц «Пракастаць знову» павінен вярнуць значэнне hasError у false.

Як React знаходзіць межу

Неабяжнае межа або кантактны кранц абоўшчы компонента з профілем,

<ErrorBoundary>
  <Profile />
</ErrorBoundary>

дзе компонент выклікае адказ:

function Profile() {
  throw new Error("Profile failed");
}

React продовжае атрыбутавацыю элементаў дакладна да момента падчас якога выйшла адказ:

ErrorBoundary
      ↓
Profile
      ↓
💥 Error

Пасля чаго ён шукае вышэй за межу, каліяцуючы найбліжэйшую межу з адказамі:

Profile
   ↑
ErrorBoundary

Гэтая межа перайходзіць у стан з адказамі,

{
  hasError: true
}

і атрыбутавае элемент з альтэрнатыўным контэнтам. Межа не можа падхапіць своия сабеўласныя адказы, толькі адказы своіх дзецячых элементаў; адказ, які выйшоў у элементе з альтэрнатыўным контэнтам, пераходзіць да наступнай межы вышэй.

Адказы, якія падхапляе межа

Межы караюць аберанціях, які выходзяць пад час рэндараўвання React, у методах жыцёвага циклу і ў канстрактарах компонентаў-наследнікаў. Класычны прыклад — чытанне атрыбута undefined пад час рэндараўвання:

function Profile() {
  const user = undefined;

  return <h1>{user.name}</h1>;
}

Яны таксама ловяць аберанціі, якія выходзяць пад час атрыбутаўвання з запазджэннем. Якщо, напрыклад, пасля аплыву стары файл быў выдален, і чанк не можа быть завантажаны, аберанція праказуецца ў найбліжэйшай межы аберанцый, а не ў Suspense.

Аберанціі, якія межы не бачаць

Часта запытваюць у інтэрв’ю: межы не бачаць аберанцый у працэсарах змаганняў, таму што яны выконваюцца за межамі рэндараўвання.

function Button() {
  const handleClick = () => {
    throw new Error("Click failed");
  };

  return (
    <button onClick={handleClick}>
      Click
    </button>
  );
}

Караце іх там, дзе яны выходзяць, за дапамою звычайнага карання аберанцый у працэсары:

try {
  // operation
} catch (error) {
  // handle error
}

То ж самае стосуецца асінхронных калбэкаў, такіх як таймеры або ланцюгі promise, якія выходзяць пасля завершэння рэндараўвання:

setTimeout(() => {
  throw new Error("Async error");
}, 1000);

Якщо такая памяць выкарыстоўваецца для адображэння запасной версіі на межы, трэба яе падхапіць і зберагчы ў стане, а пасля выклікці яе падчас наступнага атрыбутавання.

Аднаўленне ўсіх трохоў разам

У практычным падходзе Suspense абгортаецца ў межу для адлучэнняя:

<ErrorBoundary>
  <Suspense fallback={<DashboardSkeleton />}>
    <Dashboard />
  </Suspense>
</ErrorBoundary>

Калі панель керування задаецца ляжна,

const Dashboard = lazy(() => import("./Dashboard"));

сумарны тэкст пакрывае ўсі можлывыя результаты. Першую гілку можна прачытаць як «код не знайдзены»: якщо так, React пачынае імпорт і адображае шкелет; пасля чаго атрыбутаванне адбываецца успешна або доходзіць да межы для адлучэнняя.

                    Dashboard
                        │
                        ▼
                  Need the code?
                        │
               ┌────────┴────────┐
               │                 │
              YES                NO
               │                 │
          Render normally    dynamic import()
                                 │
                              Pending
                                 │
                                 ▼
                             Suspense
                                 │
                                 ▼
                            Skeleton UI
                                 │
                              Loaded
                                 │
                                 ▼
                           Render again
                                 │
                         ┌───────┴───────┐
                         │               │
                      Success          Error
                         │               │
                         ▼               ▼
                     Dashboard     Error Boundary
                                         │
                                         ▼
                                      Error UI

Раздзелэнне па маршрутах

Маршруты ўсё чыстае месца для раздзелэння, адколі кожная сторанка ёсць самастоятельны экран. Кожную сторанку трэба задаць ляжна:

const Home = lazy(() => import("./pages/Home"));
const Products = lazy(() => import("./pages/Products"));
const Dashboard = lazy(() => import("./pages/Dashboard"));

пасля чаго рутэр трэба абгортаць у межу, каб навігацыя да незагрузленай сторанкі адображала індыкатор загрузкі:

<Suspense fallback={<PageLoader />}>
  <Routes>
    <Route path="/" element={<Home />} />
    <Route path="/products" element={<Products />} />
    <Route path="/dashboard" element={<Dashboard />} />
  </Routes>
</Suspense>

Ведаць, чаго не трэба раздзеляць

Актыўнае заваносць маленькага компонента ёсць контрапродуктыва:

const Button = lazy(() => import("./Button"));

Кожны компонент, які заваносяць актыўна, выкалічвае запит і стан заваносу, чаго кнопкі ніколі не можа компенсаваць. Хорашымі кандыдатамі ў такі падход являюцца большы панелі керування, аналітыка і адміністрацыйныя роздзелы, редактары багатага тексту, карты, складныя дыяграмы чыў бібліятэкі трохцях сторон, процесы платежаў і рэдка адвідуемыя маршруты. Неабходна раздзеляць код, які ўскладнюе роботу чыў які выкалічвае больш заўсёды, а не ўсё разам.

Цялесапраўеднае размешчэнне меж

Размешчэнне элемента адлагоджання вялічына таго, што зникае

Межа, распавешчаная высока ў структуре, значыць, што будь-які повольны компонент заменяе вялікую плошчу на свой альтэрнатыўны варіант:

<Suspense fallback={<FullPageLoader />}>
  <EntireApplication />
</Suspense>

Размешчэнне такой межы навакол повольной часткі дапамагае заставіць заголовак і футер виднымі чыў прыдатнымі падчас заваносу:

<Header />

<Suspense fallback={<DashboardSkeleton />}>
  <Dashboard />
</Suspense>

<Footer />

Размешчэнне межы для адлагоджання бядаў вялічына таго, што застаецца

Тая ж самая логіка прыменяецца і да некальканняў. Адна межа на роўні корэню ператварае будзь-якую памылку на экран памылак для всій сторанкі:

<ErrorBoundary>
  <EntireApplication />
</ErrorBoundary>

Абгортаванне незалежных частак дапамагае захаваць крах у межах адзінаго регіону:

<App>
  <Header />

  <ErrorBoundary>
    <Dashboard />
  </ErrorBoundary>

  <ErrorBoundary>
    <Recommendations />
  </ErrorBoundary>

  <Footer />
</App>

Якщо рэкамендаціі не выканаюцца, усе інша продовжвае працаваць:

Header              ✅
Dashboard           ✅
Recommendations     ❌
Footer              ✅

Межа на роўні корэню застаецца карысной як апошняя мера ў пару з болей дробнымі межамі.

Ментальная модель у аднай картынцы

             Lazy Loading
                  │
                  ▼
             Code needed?
                  │
                  ▼
             Suspense
                  │
           ┌──────┴──────┐
           │             │
        Loading         Ready
           │             │
        Fallback         ▼
                       Render
                         │
                   ┌─────┴─────┐
                   │           │
                Success       Error
                   │           │
                   ▼           ▼
                   UI    Error Boundary
                              │
                              ▼
                           Error UI

Ключовыя выводы

  • React.lazy з дынамічным import() дзеліць код на часткі, якія завантажаюцца пад час першага адрасавання; модулю неабходны стандартны экспорт.
  • Suspense не завантажае нічога; ён вызначае, дзе будзе показана альтернатыва, калі наследнікі знаходзяцца у стане паузы.
  • Межы памылак ловяць памылкі адрасавання, жыцёвага циклу і канстрактара, укладаючы ў сябе некальканняя завантажэння з викорыстаннем lazy, але не памылкі обробні запускаў чы пазнейшыя асінхронныя памылкі.
  • Suspense не караце бягі, таму яго трэба выкорыстоўваць разам з Error Boundary.
  • Раздзеляйце структуру на рэвэль-роўні і ў абласцях з важкімі, рэдка выкарыстоўванымі функцыямі, а не ў маленькіх компонентах.
  • Месца размешчэння Boundary — это рашэнне, звязанае з UX: размешчайце оба вида аблакоў навакол значымых, самастоятельных частэй структуры.