React.lazy – 为什么
How to split your code with React lazy
该lazy
函数允许您动态导入组件。您可能希望这样做以减小用户为了在屏幕上看到内容而必须下载的初始包大小。
假设您的应用程序分为几条路线。你有
/home
路线和/large-page
路线。该/large-page
路由导入并使用一些您不在/home
页面上使用的大型库。如果用户访问您的/home
页面,您不希望他们必须下载大型库才能在屏幕上呈现内容,毕竟 – 他们甚至可能不会访问您的/large-page
路线,这样会很浪费。
你会发生的是加载足够的 javascript 来渲染/home
路由以进行快速初始渲染,然后如果用户导航到该/large-page
路由,你将显示一个加载微调器以通知用户一些转换即将发生并加载到/large-page
路由所需的 javascript 块。
互联网上的大多数人习惯于在页面之间导航时不得不等待转换。对于我们的用户来说,更糟糕的用户体验是长时间看一个白色的空白屏幕。
那么让我们看看如何React.lazy
帮助我们处理这个问题。
示例
让我们创建一个反应应用程序:
npx create-react-app react-lazy --template typescript cd react-lazy npm install react-router-dom npm install moment npm install --save-dev @types/react-router-dom npm start
你只需要index.tsx
和App.tsx
文件,你可以删除.css
和
.test
文件。
我们来看看内容src/index.tsx
// src/index.tsx import React from 'react'; import ReactDOM from 'react-dom'; import {App} from './App'; ReactDOM.render( <React.StrictMode> <App /> </React.StrictMode>, document.getElementById('root'), );
和内容src/App.tsx
:
// src/App.tsx import {BrowserRouter as Router, Link, Route, Switch} from 'react-router-dom'; import Home from './Home'; import LargePage from './LargePage'; export function App() { return ( <Router> <div> <Link to="/">Home</Link> <hr /> <Link to="/large-page">Large Page</Link> <hr /> </div> <Switch> <Route exact path="/"> <Home /> </Route> <Route exact path="/large-page"> <LargePage /> </Route> </Switch> </Router> ); }
首先,我们从中导入一些组件react-router-dom
,然后导入一些我们尚未编写的本地组件,然后我们有一个简单的导航,其中有 2 个链接和我们的组件在/
和/large-page
路由。现在让我们添加组件。首先让我们创建src/Home.tsx
组件:
// src/Home.tsx export default function Home() { return <h1>This is the home page...</h1>; }
和src/LargePage.tsx
组件:
// src/LargePage.tsx import * as moment from 'moment'; export default function LargePage() { const a = moment.duration(-1, 'week').humanize(true, {d: 7, w: 4}); // a week ago return ( <div> <h1>{a}</h1> </div> ); }
在我们的LargePage
组件中,我们导入库并使用它,但是我们在路由中moment
不需要该moment
库。Home
现在我们可以在路线之间导航,但即使用户从未去过LargePage
路线,他们仍然必须moment
在初始渲染时下载库。
LargePage
现在让我们改变这种行为,我们只希望用户在导航到路线时下载时刻库和路线的组件代码。让我们编辑我们src/App.tsx
的:
// src/App.tsx - import Home from './Home'; - import LargePage from './LargePage'; + import {lazy} from 'react'; + const Home = lazy(() => import('./Home')); + const LargePage = lazy(() => import('./LargePage'));
添加悬念边界
如果您现在查看浏览器,您应该会看到一个大错误。
错误:渲染时挂起的 React 组件,但未指定回退 UI。在树的更高层添加一个 Suspense fallback= 组件,以提供要显示的加载指示器或占位符。
所以 React 告诉我们组件“已暂停”,但我们没有提供加载组件来在该组件暂停时进行渲染。暂停意味着组件尚未准备好呈现,因为它尚未满足要求。转到Home
路由,打开 devtools,选择
network
选项卡并过滤JS
文件,你可以看到我们有一个单独的 JS
块用于Home
路由,因为我们正在延迟导入Home
组件。React 尝试渲染它,但它尚未加载,因此组件
暂停并且必须显示加载状态回退组件,但我们没有提供。
附带说明一下,主页组件很小,我们不应该延迟加载它,对 1Kb 大小的模块进行额外的网络请求是一种浪费,我们这样做只是为了示例。
Suspense 让你的组件在渲染之前等待一些东西,在等待时显示回退。让我们看看它是如何工作的,src/App.tsx
再次编辑您的页面并将其更改为:
// src/App.tsx import {lazy, Suspense} from 'react'; import {BrowserRouter as Router, Link, Route, Switch} from 'react-router-dom'; const Home = lazy(() => import('./Home')); const LargePage = lazy(() => import('./LargePage')); export function App() { return ( <Router> <div> <Link to="/">Home</Link> <hr /> <Link to="/large-page">Large Page</Link> <hr /> </div> {/* Now wrapping our components in Suspense passing in a fallback */} <Suspense fallback={<h1>Loading...</h1>}> <Switch> <Route exact path="/"> <Home /> </Route> <Route exact path="/large-page"> <LargePage /> </Route> </Switch> </Suspense> </Router> ); }
我们所要做的就是将我们的组件包装在Suspense
边界中,并传入加载状态的回退。
如果您在开发者工具中打开您的网络选项卡,将网络速度设置为Slow 3G
并刷新页面,您应该能够看到我们的回退被渲染到页面,同时正在加载 Home 路由的JS 块。
或者,如果您的浏览器中安装了 React devtools 扩展,您可以手动挂起该组件。点击Components
react extension中的tab,选中Home
组件,点击右上角的秒表图标,即可挂起选中的组件。
到目前为止一切顺利,现在让我们再次打开网络选项卡,按JS文件过滤并导航到Large Page
路由。您会看到我们加载了 2 个 JS 块,一个用于组件本身,另一个用于moment
库。
moment
在这种状态下,当我们导航到Large Page
路线时,我们的应用程序会延迟加载库。如果用户进入我们的Home
页面并且从未访问过我们的页面,Large Page
他们甚至不必加载moment
库或Large Page
组件代码。
作为旁注,用户只需加载一次 JS 块。如果他们来回导航,浏览器将已经缓存了文件,我们将看不到回退加载微调器,组件将不必暂停。
添加错误边界
我们的应用程序似乎处于良好状态,但是我们正在使用网络请求我们已拆分的 JS 文件,因此如果用户加载我们的
Home
,失去与互联网的连接并导航到LargePage
路线会发生什么。
要对此进行测试,请转到Home
页面,刷新,打开“网络”选项卡并将网络状态设置为offline
。现在导航到/large-page
路线,您应该看到一个空白的白色屏幕,这绝不是一件好事。
在我们的控制台中,我们收到以下错误:
未捕获的 ChunkLoadError:加载块 2 失败。
所以我们尝试加载 JS 块,但我们失败了,整个应用程序崩溃了。为了向用户提供一些反馈并记录错误以便我们修复它,我们必须包装可能会抛出一个
组件的ErrorBoundary
组件。
ErrorBoundary 就像一个 try{} catch(){},用于在它下面的组件的渲染方法中抛出的错误。
考虑它的一个好方法是:当它的孩子还没有准备好渲染时,Suspense
边界显示 a
– 它处理加载状态,而处理组件渲染方法中抛出的错误。FallbackComponent
ErrorBoundary
让我们在以下位置添加一个ErrorBoundary
组件src/ErrorBoundary.tsx
:
// src/ErrorBoundary.tsx import React from 'react'; export class ErrorBoundary extends React.Component< {children?: React.ReactNode}, {error: unknown; hasError: boolean} > { state = {hasError: false, error: undefined}; componentDidCatch(error: any, errorInfo: any) { this.setState({hasError: true, error}); } render() { if (this.state.hasError) { return <h1>An error has occurred. {JSON.stringify(this.state.error)}</h1>; } return this.props.children; } }
让我们在我们的src/App.tsx
组件中使用它:
// ... other imports import {ErrorBoundary} from './ErrorBoundary'; // ... export function App() { return ( <Router> <div> <Link to="/">Home</Link> <hr /> <Link to="/large-page">Large Page</Link> <hr /> </div> {/* Now wrapping our components in Suspense passing in a fallback */} <ErrorBoundary> <Suspense fallback={<h1>Loading...</h1>}> <Switch> <Route exact path="/"> <Home /> </Route> <Route exact path="/large-page"> <LargePage /> </Route> </Switch> </Suspense> </ErrorBoundary> </Router> ); }
现在我们已经包装了可能会抛出
ErrorBoundary
. 让我们重复测试:刷新Home
页面,打开网络选项卡,将网络设置设置为离线并导航至
/large-page
路线。您将看到错误被打印到屏幕上,这比看到空白屏幕更好。
限制
- 该
ErrorBoundary
组件是一个类,在撰写本文时,您只能使用类来实现错误边界。 - 我们延迟加载的组件是默认导出 –
React.lazy
目前仅支持默认导出。
摘要
React.lazy允许我们将代码分成块。为了提高大型应用程序的性能,我们不想强迫用户下载包含我们整个应用程序的单个 JS 文件,因为他们很可能不会使用我们的整个应用程序,他们不会访问我们网站的每条路线。
由于互联网上的大多数人习惯于在页面转换之间等待,因此提供加载指示器并在用户导航到它时按需加载组件代码比让所有用户加载他们可能不需要的代码要好。