跳至主要內容

React 渲染调优

Mr.LRH大约 5 分钟

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.lazyinit 方法,得到一个 Promise,绑定 Promise.then 成功回调,在回调中得到将要渲染的组件 defaultExport。同时,因为此时 Promise 状态不是 Resolved,会抛出异常 Promise,抛出异常会终止当前渲染。
  • 异常 Promise 会被 React.Susponse 捕获到,React.Susponse 会处理 PromisePromise 执行成功回调得到 defaultExport (将要渲染的组件),然后发起第二次渲染
  • 第二次渲染时,React.lazyinit 方法中 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>
  )
}
上次编辑于:
贡献者: lingronghai