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.Suspense 理解
React.lazy 包裹的组件会标记 REACT_LAZY_TYPE 类型的 Element,在调和阶段会变成 LazyComponent 类型的 Fiber。React 对 LazyComponent 会有单独的处理逻辑:
- 第一次渲染时,首先执行 
React.lazy的init方法,得到一个Promise,绑定Promise.then成功回调,在回调中得到将要渲染的组件defaultExport。同时,因为此时Promise状态不是Resolved,会抛出异常Promise,抛出异常会终止当前渲染。 - 异常 
Promise会被React.Suspense捕获到,React.Suspense会处理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: 带有componentStackkey 的对象,其中包含有关组件引发错误的栈信息。
componentDidCatch()会在commit(提交)阶段被调用,因此允许执行副作用。 它应该用于记录错误之类的情况。static getDerivedStateFromError(error)此生命周期会在后代组件抛出错误后被调用。它将抛出的错误作为参数,并返回一个值以更新stategetDerivedStateFromError()会在渲染阶段调用,因此不允许出现副作用。 如遇此类情况,请改用 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>
  )
}