【译】从500ms到1.7ms的React性能优化经历

原作者:Oren Farhi

原文链接:https://orizens.com/blog/500ms-to-1-7ms-in-react-a-journey-and-a checklist/

img

文章是基于ReadM -一个免费的应用程序,快速练习英语阅读的乐趣。

我相信每个开发者在开发的某个阶段,都有可能需要进行性能优化。关于如何优化react中的性能,已经有很多资源和文章,这篇文章也不例外。这里分享一个我实践过的从0.5s到1.7ms的性能优化的实践和结论。

优化前:

alt text

优化后:

alt text

什么时候性能表现不好?

我认为在你注意到性能的时候,才去开始调整是比较好的时机。我不会一开始就在应用中每个开发阶段,都去使用useMemo()和React.memo()。有一些场景其实并不需要这样做,所以我的建议是,不要 “over-memoize"和"optimize prematurley”(译者注:optimize prematurely,过早优化代码,应该是笔误)。

目前我正在处理的场景中,0.5s的等待时间会让用户明显地感觉到卡顿。First Meaning Paint这个词也适用于非首次出现的画面🙂,用户可能无法忍受卡顿的界面,往往会把用户吓跑。

设置和环境

  1. React 17
  2. Chakra-UI 基础UI库
  3. Redux Toolkit 状态管理工具
  4. react-table v7 演示案例

性能问题

这个案例中,Table是一个定制的网格,有几个定制的单元格渲染器。该Table组件的父组件,还包含了其他组件。首次渲染有时会比较顺利,但是,当父组件中的hook内或者和父组件相关的状态触发更新时,表格会重新渲染,因为0.5s才能完成渲染,所以用户肯定会看到比较不流畅的画面。从用户体验的角度来看,卡顿感比较明显。Table组件甚至会在其数据(列和行)完全没有变化的情况下重新渲染。

阶段 1 - 将大型组件分解为定义明确的较小组件

react-table中的钩子不暴露任何 UI - 所以 - 必须根据文档实现一个。大多数案例演示了一个非常简单的表格的非嵌套平面版本,效果很好。Table组件由大约 200 多行的嵌套和非嵌套jsx/components组成。一些非嵌套的 jsx 包含.map() 遍历来渲染行和表头。

很明显要必须对其进行重构,将其分解成更小的组件。我们很容易发现一些代码块可以被封装成一个内嵌组件,这也是有意义的,例如:<TableHeader /><TableRows />等。这一步使Table组件更小,更容易阅读和维护。当属性是基本类型时,将少量块封装成组件的其中一个好处就是可以减少无效的渲染。仅在完成这一步,就可以减少150ms,优化效果不错,但350ms仍然会感觉得有些小卡顿。

阶段 2 - React.memo() 组合组件

对于包含非基本类型(例如数组、对象和函数)属性的组件,使用 React.memo 确实减少了重新渲染,同时,从渲染中取出静态对象属性也有帮助:

const config = {
  headerHandlers: { isCustom: true },
  rowHandlers: { isCustomPadding: true },
  paginationConfig: {
    autoResetPage: false,
    autoResetGlobalFilter: false,
    initialState: { pageSize: 20 },
  },
};

<ReadMTable {...config} data={events} columns={eventColumns} />

对于组合组件 - 使用 .map() 遍历数组的组件,每个子项都能映射到对应的组件并获得设置唯一的key。这意味着当一个组件包括基本类型的属性时,再次使用无需重新渲染:

export const ReadMTableRows = React.memo(EventsTableRows);
export function EventsTableRows({
  rows,
  prepareRow,
  onClick,
  isRowDisabled,
}: TableRowsProps) {
  return rows.length > 0 ? (
    <>
      {rows.map(row => (
        <TableRow
          row={prepareRow(row)}
          key={`event-table-row-${row.id}`}
          onRowClick={onClick}
          disabled={isRowDisabled}
        />
        )
      )}
    </>
  ) : null;
}

按照上述模式,减少 300 ms。我感到很高兴,但我看到了更大的改进空间。

阶段 3 - 将 jsx 的常量转换为组件

在这个阶段,我注意到有一些常量是指向使用Table组件部分属性的jsx代码。为了利用基本类型与React的组件渲染生命周期的优势,我将这些转换为组件,即

// BEFORE
const renderTableTitle = (title, totalRows) => {
  return (
    <Flex
      alignItems="center"
    >
      <Heading as="h4" fontWeight="bold">{title}</Heading>
      {totalRows}
    </Columns>
  );
}
// this is always invoked
{renderTableTitle('student statistics', page.length)}

// AFTER
function TableTitle ({ title, totalRows }) {
  return (
    <Flex
      alignItems="center"
    >
      <Heading as="h4" fontWeight="bold">{title}</Heading>
      {totalRows}
    </Flex>
  );
}
// this one rerenders when title (string) or page.length (number) - changes.
<TableTitle title={title} totalRows={page.length} />

很容易看到组件<TableTitle />titlepage.length改变时被重新渲染,这两个基本类型属性都是如此。当作为一个组件使用时,该应用会受益于React的协调算法–只要props具有相同的值,该组件就不会被重新渲染。我所说的相同值,指的是基本类型(number、string、boolean)或相同的引用类型(function、object)值(这可以通过多种方式实现:store、memoized值或一个静态引用)。

阶段 4 - Chasing the white rabbit

(译者注:Chasing the white rabbit 在这里的意思和”刨根问底“比较接近)

虽然有点标题党,但我发现它很有趣,足以表达我在尝试理解组件重新渲染的原因时的感受。幸运的是,React 的 devtool 包含"Profiler"工具。确保设置了"Record why each component rendered while profiling"。单击齿轮图标调出弹窗,然后试着验证性能不佳的场景:

perf-controls

(译者注:这里的流程是:安装好React devtool -> 点击Profiler选项卡 -> 点击中间的小齿轮按钮 -> 勾选"Record why each component rendered while profiling")

image-20220817110434419.png

  1. 点击"start profiling"蓝色按钮
  2. 在浏览器内验证下案例
  3. 验证完后,再次点击"stop profiling"按钮结束分析

然后会看到FlameGraph和Ranked。通常我会去看黄颜色的部分,然后我开始探究“为什么要渲染?”这个问题的提示。有一些有用的提示,可能会引导你重构或重新思考组件的组成结构是怎么样的:

  1. points to sepcific props that have changed
  2. points to a hook that has changes (sadly, all we have is a hook’s number, go figure…)
  3. indicates the parent component rendered
  4. indicates first render

(译者注:见下图,其实就是why did this render的提示)

在这个Table组件的示例中,这些提示还是有帮助的,让我能够意识到有必要去memo一些从自定义hook中返回的数据。

props-changed

鼠标悬停在这些条状图上,可以显示dom中的实际组件 - 很容易能分析出该特定组件是否应该重新渲染。当组件应该渲染的时候,就要研究下这个组件的实现方式,并通过它的 props、hook 或任何其他与之相关的数据源来排查筛是什么数据变化触发了渲染。

当props包含函数回调,包括那些使用了useCallBack()也许也能够帮助减少渲染次数。

有时候React.memo()是有作用的,但需要反复验证。需要确定的是,我不是在吹React.memo()这个解决方案。一般来说,我更倾向component tree进行深入的分析 - 查出从同一份数据被重新创建的非基本类型的对象 - 在这个示例中 - 我确实觉得useMemo()能更好地解决问题,除非我可以使用store。

hooks-changed

当其中一个条状图的提示常出现了"hooks changed"的提示时,那就要仔细review一下和hook相关的组件代码了。

component-hooks

回顾一下,有些性能上的瓶颈很容易被发现并解决。有些可能需要深入研究下 react 的component tree以及反复验证。

我认为性能分析应该在应用发生明显卡顿时进行。最重要的是确保能找出性能下降时的关键原因。有时可能会需要更改 jsx 的构建方式 - 如果是这种情况 - 我建议在添加任何需要维护的额外代码之前仔细考虑一下。

您想了解更多有关 React 性能的信息吗?

如果您对如何提高react性能有任何问题或想法想要我写,请联系我或发推特给我。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值