React 渲染调优
React 渲染调优
React.Suspense 异步渲染
React.Suspense
使得组件可以“等待”某些操作结束后,再进行渲染。可以通过 fallback
属性指定加载指示器(loading indicator)。
目前,React.Suspense
仅支持的使用场景是:通过 React.lazy
动态加载组件。它将在未来支持其它使用场景,如数据获取等。
最佳实践是将 React.Suspense
置于需要展示加载指示器(loading indicator)的位置,而 React.lazy
则可被放置于任何想要做代码分割的地方。
// 该组件是动态加载的
const OtherComponent = React.lazy(() => import('./OtherComponent'))
function MyComponent() {
return (
// 显示 <Spinner> 组件直至 OtherComponent 加载完成
<React.Suspense fallback={<Spinner />}>
<div>
<OtherComponent />
</div>
</React.Suspense>
)
}
React.lazy 懒加载
const OtherComponent = React.lazy(() => import('./OtherComponent'))
React.lazy
接受一个函数,这个函数需要动态调用import()
。它必须返回一个Promise
,该Promise
需要resolve
一个default export
的 React 组件。然后,应在
React.Suspense
组件中渲染 lazy 组件,则可以使用在等待加载 lazy 组件时,做优雅降级(如 loading 指示器等)。可以将React.Suspense
组件置于懒加载组件之上的任何位置。甚至可以用一个React.Suspense
组件包裹多个懒加载组件。
const OtherComponent = React.lazy(() => import('./OtherComponent'))
const AnotherComponent = React.lazy(() => import('./AnotherComponent'))
function MyComponent() {
return (
<div>
<React.Suspense fallback={<div>Loading...</div>}>
<section>
<OtherComponent />
<AnotherComponent />
</section>
</React.Suspense>
</div>
)
}
在如下标签切换示例中,如果标签从 <Photos />
切换为 <Comments />
,但 <Comments />
会暂停,用户会看到屏幕闪烁。因为用户不想看到 <Photos />
,而 <Comments />
组件还没有准备好渲染其内容,而 React 为了保证用户体验的一致性,只能显示上面的 <Glimmer />
。
这种用户体验并不可取,在准备新 UI 时,展示 “旧” 的 UI 会体验更好,可以尝试使用新的 startTransition
API 来让 React 实现这一点。将标签切换为 <Comments />
不会标记为紧急更新,而是标记为需要一些准备时间的 transition。然后 React 会保留旧的 UI 并进行交互,当它准备好时,会切换为 <Comments />
。
import React, { Suspense } from 'react'
import Tabs from './Tabs'
import Glimmer from './Glimmer'
const Comments = React.lazy(() => import('./Comments'))
const Photos = React.lazy(() => import('./Photos'))
function MyComponent() {
const [tab, setTab] = React.useState('photos')
function handleTabSelect(tab) {
// setTab(tab)
startTransition(() => {
setTab(tab)
})
}
return (
<div>
<Tabs onTabSelect={handleTabSelect} />
<Suspense fallback={<Glimmer />}>
{tab === 'photos' ? <Photos /> : <Comments />}
</Suspense>
</div>
)
}
React.lazy + React.Susponse 理解
React.lazy
包裹的组件会标记 REACT_LAZY_TYPE
类型的 Element
,在调和阶段会变成 LazyComponent
类型的 Fiber
。React 对 LazyComponent
会有单独的处理逻辑:
- 第一次渲染时,首先执行
React.lazy
的init
方法,得到一个Promise
,绑定Promise.then
成功回调,在回调中得到将要渲染的组件defaultExport
。同时,因为此时Promise
状态不是Resolved
,会抛出异常Promise
,抛出异常会终止当前渲染。 - 异常
Promise
会被React.Susponse
捕获到,React.Susponse
会处理Promise
。Promise
执行成功回调得到defaultExport
(将要渲染的组件),然后发起第二次渲染 - 第二次渲染时,
React.lazy
的init
方法中Promise
状态为Resolved
状态,则直接返回result
(即,真正渲染的组件)。此时,可以正常渲染组件。
React.lazy 实现源码
// packages\react\src\ReactLazy.js
function lazyInitializer<T>(payload: Payload<T>): T {
if (payload._status === Uninitialized) {
const ctor = payload._result
const thenable = ctor()
// Transition to the next state.
// This might throw either because it's missing or throws. If so, we treat it
// as still uninitialized and try again next time. Which is the same as what
// happens if the ctor or any wrappers processing the ctor throws. This might
// end up fixing it if the resolution was a concurrency bug.
thenable.then(
moduleObject => {
if (payload._status === Pending || payload._status === Uninitialized) {
// Transition to the next state.
const resolved: ResolvedPayload<T> = (payload: any)
resolved._status = Resolved
resolved._result = moduleObject
}
},
error => {
if (payload._status === Pending || payload._status === Uninitialized) {
// Transition to the next state.
const rejected: RejectedPayload = (payload: any)
rejected._status = Rejected
rejected._result = error
}
}
)
if (payload._status === Uninitialized) {
// In case, we're still uninitialized, then we're waiting for the thenable
// to resolve. Set it as pending in the meantime.
const pending: PendingPayload = (payload: any)
pending._status = Pending
pending._result = thenable
}
}
if (payload._status === Resolved) {
const moduleObject = payload._result
return moduleObject.default
} else {
throw payload._result
}
}
export function lazy<T>(
ctor: () => Thenable<{ default: T, ... }>
): LazyComponent<T, Payload<T>> {
const payload: Payload<T> = {
// We use these fields to store the result.
_status: Uninitialized,
_result: ctor,
}
const lazyType: LazyComponent<T, Payload<T>> = {
$$typeof: REACT_LAZY_TYPE,
_payload: payload,
_init: lazyInitializer,
}
return lazyType
}
处理渲染错误边界
如果模块加载失败(如网络问题),它会触发一个错误。可以通过异常捕获边界(Error boundaries)技术来处理这些情况,以显示良好的用户体验并管理恢复事宜。
componentDidCatch(error, info)
此生命周期在后代组件抛出错误后被调用。 它接收两个参数:error
: 抛出的错误。info
: 带有componentStack
key 的对象,其中包含有关组件引发错误的栈信息。
componentDidCatch()
会在commit
(提交)阶段被调用,因此允许执行副作用。 它应该用于记录错误之类的情况。static getDerivedStateFromError(error)
此生命周期会在后代组件抛出错误后被调用。它将抛出的错误作为参数,并返回一个值以更新state
getDerivedStateFromError()
会在渲染阶段调用,因此不允许出现副作用。 如遇此类情况,请改用 componentDidCatch()。
注意:错误边界仅可以捕获其子组件的错误,它无法捕获其自身的错误。如果一个错误边界无法渲染错误信息,则错误会冒泡至最近的上层错误边界,这也类似于 JavaScript 中 catch {}
的工作机制。
// 错误边界,可以将其封装为一个常规组件去使用
class ErrorBoundary extends React.Component {
constructor(props) {
super(props)
this.state = { hasError: false }
}
static getDerivedStateFromError(error) {
// 更新 state 使下一次渲染能够显示降级后的 UI
return { hasError: true }
}
componentDidCatch(error, errorInfo) {
// 可以将错误日志上报给服务器
logErrorToMyService(error, errorInfo)
}
render() {
if (this.state.hasError) {
// 可以自定义降级后的 UI 并渲染
return <h1>Something went wrong.</h1>
}
return this.props.children
}
}
// 使用
function ExampleComponent() {
return (
<ErrorBoundary>
<MyWidget />
</ErrorBoundary>
)
}