原作者:Oren Farhi
原文链接:https://orizens.com/blog/500ms-to-1-7ms-in-react-a-journey-and-a checklist/
文章是基于ReadM -一个免费的应用程序,快速练习英语阅读的乐趣。
我相信每个开发者在开发的某个阶段,都有可能需要进行性能优化。关于如何优化react中的性能,已经有很多资源和文章,这篇文章也不例外。这里分享一个我实践过的从0.5s到1.7ms的性能优化的实践和结论。
优化前:
优化后:
什么时候性能表现不好?
我认为在你注意到性能的时候,才去开始调整是比较好的时机。我不会一开始就在应用中每个开发阶段,都去使用useMemo()和React.memo()。有一些场景其实并不需要这样做,所以我的建议是,不要 “over-memoize"和"optimize prematurley”(译者注:optimize prematurely,过早优化代码,应该是笔误)。
目前我正在处理的场景中,0.5s的等待时间会让用户明显地感觉到卡顿。First Meaning Paint这个词也适用于非首次出现的画面🙂,用户可能无法忍受卡顿的界面,往往会把用户吓跑。
设置和环境
- React 17
Chakra-UI
基础UI库Redux Toolkit
状态管理工具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 />
在title
或page.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"。单击齿轮图标调出弹窗,然后试着验证性能不佳的场景:
(译者注:这里的流程是:安装好React devtool -> 点击Profiler选项卡 -> 点击中间的小齿轮按钮 -> 勾选"Record why each component rendered while profiling")
- 点击"start profiling"蓝色按钮
- 在浏览器内验证下案例
- 验证完后,再次点击"stop profiling"按钮结束分析
然后会看到FlameGraph和Ranked。通常我会去看黄颜色的部分,然后我开始探究“为什么要渲染?”这个问题的提示。有一些有用的提示,可能会引导你重构或重新思考组件的组成结构是怎么样的:
- points to sepcific props that have changed
- points to a hook that has changes (sadly, all we have is a hook’s number, go figure…)
- indicates the parent component rendered
- indicates first render
(译者注:见下图,其实就是why did this render的提示)
在这个Table组件的示例中,这些提示还是有帮助的,让我能够意识到有必要去memo一些从自定义hook中返回的数据。
鼠标悬停在这些条状图上,可以显示dom中的实际组件 - 很容易能分析出该特定组件是否应该重新渲染。当组件应该渲染的时候,就要研究下这个组件的实现方式,并通过它的 props、hook 或任何其他与之相关的数据源来排查筛是什么数据变化触发了渲染。
当props包含函数回调,包括那些使用了useCallBack()
也许也能够帮助减少渲染次数。
有时候React.memo()
是有作用的,但需要反复验证。需要确定的是,我不是在吹React.memo()
这个解决方案。一般来说,我更倾向component tree进行深入的分析 - 查出从同一份数据被重新创建的非基本类型的对象 - 在这个示例中 - 我确实觉得useMemo()
能更好地解决问题,除非我可以使用store。
当其中一个条状图的提示常出现了"hooks changed"的提示时,那就要仔细review一下和hook相关的组件代码了。
回顾一下,有些性能上的瓶颈很容易被发现并解决。有些可能需要深入研究下 react 的component tree以及反复验证。
我认为性能分析应该在应用发生明显卡顿时进行。最重要的是确保能找出性能下降时的关键原因。有时可能会需要更改 jsx 的构建方式 - 如果是这种情况 - 我建议在添加任何需要维护的额外代码之前仔细考虑一下。
您想了解更多有关 React 性能的信息吗?
如果您对如何提高react性能有任何问题或想法想要我写,请联系我或发推特给我。