Завантажэнне, чаканне, абяроны: як падходзяць межы ледачасці, напэўнення і абяроны ад памылак
З’ясавайце, як React.lazy, Suspense і Error Boundaries дзельнуюць код, прадстаўляюць альтернатывны варыянты і запобегаюць збоям, а таксама дзе трэба размістыць кожны з іх для кращага корыстнікаўскога досвяду.
Крыніцы: 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.