React 性能优化新招,useTransition

简介: React 性能优化新招,useTransition

在 React 中,有一个高大上的概念,叫做并发模式 Concurrent React。在并发模式中,引入了两个新概念:任务优先级、异步可中断。当一个任务正在 Reconciler 阶段执行时,如果此时 Scheduler 发现了一个优先级更高的任务,那么,React 可以把正在执行的任务中断,从 Scheculer 中把优先级更高的任务拿过来执行。

Scheduler Reconciler Renderer
收集 diff 操作 DOM
优先级 可中断

当有多个 UI 发生变化,我们可以利用这个并发机制,将耗时比较长,会阻塞其他 UI 渲染的更新,标记为低优先级,这样,一部分 UI 就可以顺利无卡顿的渲染,耗时较长的更新则在其他 UI 更新完毕之后再更新。


0什么样的任务是可中断的


我们这里首先要思考的是任务最小粒度的问题。这是大多数人在学习并发模式时,忽略的重要问题。如果你无法思考清楚,那么你的 React 可能从来没有做到过异步可中断更新,一直是同步更新。


首先我们要明确一个基本概念:一个函数的执行是不可以被中断的。例如有这样一个组件

function SlowComponent({ text }: Props) {
  let startTime = performance.now();
  while (performance.now() - startTime < 1000) {
  }
  return (
    <li className="item">
      Text: {text}
    </li>
  )
}

我们发现,函数 SlowComponent 的执行过程中,我们模拟他被阻塞了 1000ms,这个阻塞在函数内部我们没有任何办法能够中断他的执行。React 底层是通过广度优先遍历的方式,将更新任务转换为队列。而这个函数任务已经是最小粒度,无法拆分自然也无法中断。


因此,要做到可中断的更新,我们在编写代码时,应该把阻塞拆分到多个子组件中去。这样每个子组件的执行时间可能稍微比较短,但是多个子组件综合起来的时间就会比较长而造成卡顿。拆分之后,那么在协调器遍历执行子组件的任务时,对于整个大任务而言,就有机会在协调器遍历没有完成时,做到任务中断。否则,React 也无法做到中断。


因此,合理的手动拆分任务,是 React 并发模式能够发挥作用的关键。


例如,我们要渲染一个列表组件,如果列表组件是父组件,列表项是子组件,那么我们应该确保父组件不会有长时间的逻辑要执行,从而把渲染压力拆分到子组件中去,例如如下代码。

function SlowList({ text }: Props) {
  let items = [];
  for (let i = 0; i < 250; i++) {
    items.push(<SlowItem key={i} text={text} />);
  }
  return (
    <ul className="items">
      {items}
    </ul>
  );
}
function SlowItem({ text }: Props) {
  let startTime = performance.now();
  while (performance.now() - startTime < 1) {
    // 每个 item 暂停 1ms,模拟极其缓慢的代码
  }
  return (
    <li className="item">
      Text: {text}
    </li>
  )
}


1复现卡顿


我们来尝试写一个 demo 复现一下 input 输入卡顿的问题。当我在输入内容时,列表组件会根据我输入内容的变化而发生变化。此时列表组件是一个耗时较长的渲染,因此在 input 中输入内容时会感觉到明显的卡顿。


如下图,此时我在快速输入内容,但输入时卡顿明显。

scroll.gif


该 demo 目录结构如下

+ App
  - index.tsx
  - api.ts
  - List.tsx
  - SearchResults.tsx

首先模拟一个函数,用于创建列表数据

export function createList(param?: string) {
  const p = (param || '').split('')
  const arr: string[] = []
  for(var i = 0; i < 250; i++) {
    const pindex = i % p.length
    arr.push(`${p[pindex] || '^ ^'} - ${Math.random()}`)
  }
  return arr
}

然后我们随意封装一个简单的 List 列表组件,该组件是一个基础 UI 组件,只负责处理数据渲染,不包含逻辑。之所以要这样封装,是为了尽可能的还原真实场景,而非单纯的将本案例看成学习 demo.

import { ReactNode } from 'react'
import s from './index.module.scss'
interface ListProps<T> {
  list?: T[],
  renderItem: (item: T) => ReactNode
}
export default function List<T>(props: ListProps<T>) {
  const {list = [], renderItem} = props
  return (
    <div className={s.list}>
      {list.map(renderItem)}
    </div>
  )
}

然后我们基于刚才的基础组件,开始封装业务组件 SearchResults。业务组件表示搜索结果,该组件接收搜索条件,然后根据条件计算出要显示的列表内容,最终由 List 负责展示。我们将列表项子组件 Item 也写在这里,阻塞 1ms 表示子组件渲染耗时。250 个子项则一共至少耗时 250ms.

import { createList } from './api'
import List from './List'
interface Props {
  query: string
}
export default function SearchResults({ query }: Props) {
  const list = createList(query)
  return (
    <List 
      list={list} 
      renderItem={(item) => (
        <Item key={item} text={item} />
      )}
    />
  )
}
function Item(props: { text: string }) {
  let startTime = performance.now();
  while (performance.now() - startTime < 1) {}
  return (
    <div>{props.text}</div>
  )
}

入口文件的内容比较简单,语义为搜索结果要响应输入内容的变化

import { useState } from 'react'
import SearchResults from './SearchResults'
export default function Demo01() {
  const [text, setText] = useState('')
  return (
    <div>
      <input type="text" 
        onChange={(e: any) => setText(e.target.value)} 
      />
      <SearchResults query={text} />
    </div>
  )
}

这样,当你在连续输入内容时,你会感觉到输入框此时有明显的卡顿感。


2useTransition


useTransition 是 React 专门为并发模式提供的一个基础 hook。它能够帮助你在不阻塞 UI 渲染的情况下更新状态。意思就是说,将更新任务的优先级调低一点。

const [isPending, startTransition] = useTransition()

useTransition 的调用不需要参数,他的执行返回两个参数


  • isPending:是否还存在等待处理的 transition,表示被降低优先级的更新还没有完成
  • startTransition:标记任务的优先级为 transition,该优先级低于正常更新


startTransition 的用法如下,我会将更新任务在它的回调函数中执行

function TabContainer() {
  const [isPending, startTransition] = useTransition();
  const [tab, setTab] = useState('about');
  function selectTab(nextTab) {
    startTransition(() => {
      setTab(nextTab);
    });
  }
  // ……
}

回到刚才那个输入卡顿的例子。此案例中,有两个 UI 更新,一个是输入框的 UI,另外一个是列表的 UI,此时,我们只需要在 index.tsx 中,把列表的 UI 使用 startTransition 标记为低优先级即可。代码更改如下

import { useState, useTransition } from 'react'
import SearchResults from './SearchResults'
export default function Demo01() {
  const [text, setText] = useState('')
  const [pending, startTransition] = useTransition()
  function onchange(e: any) {
    startTransition(() => {
      setText(e.target.value)
    })
  }
  return (
    <div>
      <input type="text" 
        onChange={onchange}
      />
      <div>{pending ? 'input...' : 'end' }</div>
      <SearchResults query={text} />
    </div>
  )
}

除此之外,在 SearchResults 组件中,我们观察发现列表的代码已经具备可拆分的可能性,那么,我们就只需要给 SearchResults 组件包裹一层 memo 优化,避免冗余的渲染即可


如果不包裹 memo,优化效果会降低很多。

function SearchResults({ query }: Props) {
  const list = createList(query)
  return (
    <List 
      list={list} 
      renderItem={(item) => (
        <Item key={item} text={item} />
      )}
    />
  )
}
function Item(props: { text: string }) {
  let startTime = performance.now();
  while (performance.now() - startTime < 1) {}
  return (
    <div>{props.text}</div>
  )
}
+ export default memo(SearchResults)

观察一下运行结果,发现往输入框中输入内容已经变得非常流畅,列表渲染因为多次被中断,加上 memo 的作用,此时我们发现列表的渲染次数变得非常少,最终也能响应最后的正确结果。

scroll.gif


3防抖


我们最终的优化效果与防抖有一点类似。但是他们的原理和解决的问题完全不一样。防抖是结合闭包和 setTiemout 让任务不发生,更适合用于任务无法拆分的场景。


而 useTransition 则是中断已经开始执行的任务,更适合于任务可以被拆分的场景。

相关文章
|
1月前
|
存储 前端开发 JavaScript
深入理解React Fiber架构及其性能优化
【10月更文挑战第5天】深入理解React Fiber架构及其性能优化
87 1
|
6月前
|
前端开发 API 开发者
你可能没有关注过的 React 性能优化,帮你突破瓶颈
你可能没有关注过的 React 性能优化,帮你突破瓶颈
|
2月前
|
前端开发 JavaScript UED
深入React Hooks与性能优化实践
深入React Hooks与性能优化实践
48 0
|
3月前
|
开发者 搜索推荐 Java
超越传统:JSF自定义标签库如何成为现代Web开发的个性化引擎
【8月更文挑战第31天】JavaServer Faces(JSF)框架支持通过自定义标签库扩展其内置组件,以满足特定业务需求。这涉及创建`.taglib`文件定义标签库及组件,并实现对应的Java类与渲染器。本文介绍如何构建和应用JSF自定义标签库,包括定义标签库、实现标签类与渲染器逻辑,以及在JSF页面中使用这些自定义标签,从而提升代码复用性和可维护性,助力开发更复杂且个性化的Web应用。
73 0
|
3月前
|
前端开发 测试技术 UED
React性能优化的神奇之处:如何用懒加载与代码分割让你的项目一鸣惊人?
【8月更文挑战第31天】在现代Web开发中,性能优化至关重要。本文探讨了React中的懒加载与代码分割技术,通过示例展示了如何在实际项目中应用这些技术。懒加载能够延迟加载组件,提高页面加载速度;代码分割则将应用程序代码分割成多个块,按需加载。两者结合使用,可以显著提升用户体验。遵循合理使用懒加载、编写测试及关注性能等最佳实践,能够更高效地进行性能优化,提升应用程序的整体表现。随着React生态的发展,懒加载与代码分割技术将在未来Web开发中发挥更大作用。
52 0
|
3月前
|
缓存 前端开发 JavaScript
React.memo 与 useMemo 超厉害!深入浅出带你理解记忆化技术,让 React 性能优化更上一层楼!
【8月更文挑战第31天】在React开发中,性能优化至关重要。本文探讨了`React.memo`和`useMemo`两大利器,前者通过避免不必要的组件重渲染提升效率,后者则缓存计算结果,防止重复计算。结合示例代码,文章详细解析了如何运用这两个Hook进行性能优化,并强调了合理选择与谨慎使用的最佳实践,助你轻松掌握高效开发技巧。
94 0
|
4月前
|
缓存 监控 前端开发
react 性能优化方案?
【7月更文挑战第15天】改善React应用性能的关键策略包括:使用生产环境构建减少体积,避免不必要的渲染(如用React.memo或PureComponent),正确设置列表渲染的key,简化组件层级,实施懒加载,避免render中的复杂计算,选择优化过的库,控制重渲染范围,监控性能并合并state更新。这些优化能提升响应速度和用户体验。
54 0
|
6月前
|
前端开发 数据可视化 UED
React的代码分割:使用React.lazy和Suspense进行性能优化
【4月更文挑战第25天】使用React的`React.lazy`和`Suspense`进行代码分割可优化性能,按需加载组件以提升应用启动速度和用户体验。`React.lazy`接收返回Promise的组件动态导入,而`Suspense`提供加载指示器,保证加载过程中的用户体验。适用于大型组件或路由应用,但需注意服务器配置、避免过度拆分和确保关键代码即时加载。合理运用能显著改善应用性能。
|
缓存 前端开发 JavaScript
react中的性能优化方案有哪些
react中的性能优化方案有哪些
72 0
|
6月前
|
存储 前端开发 JavaScript
React 性能优化终章,成为顶尖高手的最后一步
React 性能优化终章,成为顶尖高手的最后一步
下一篇
无影云桌面