使用 React 16.5 分析器加快渲染速度
React 16.5 版本最近发布,新增了一些分析工具的支持。我们最近使用这些工具识别出了渲染性能缓慢的一个主要原因。
Faithlife.com 是一款基于 React 16.3 的 Web 应用。其主页包含按时间倒序排列的帖子。我们收到一些用户报告,称与帖子的交互(例如回复)会导致浏览器卡顿,具体卡顿程度取决于帖子在页面上的深度。帖子在页面上越深,卡顿程度越严重。
在 Faithlife 本地副本上将 React 更新到 16.5 后,我们的下一步是开始分析并捕获哪些组件正在重新渲染。以下是工具显示的点击任意帖子“赞”按钮的截图:
NewsFeed 下方的蓝色块表示在动态消息中的所有帖子上都会调用 render 函数。如果加载了 10 个条目,NewsFeedItem
那么它的所有子组件都会被渲染 10 次。对于小型组件来说,这可能没问题,但如果渲染树很深,不必要地渲染组件及其子组件可能会导致性能问题。当用户向下滚动页面时,动态消息中会加载更多帖子。这会导致所有位于顶部的帖子都会被调用 render 函数,即使它们本身并没有变化!
这似乎是尝试更改NewsFeedItem
为扩展的好时机PureComponent
,如果道具没有改变(此检查使用浅比较),它将跳过重新渲染组件及其子项。
不幸的是,仅仅应用 PureComponent 还不够——再次分析表明,不必要的组件渲染仍然存在。我们发现了两个阻碍我们利用 PureComponent 优化的问题:
第一个障碍:使用儿童道具。
我们有一个看起来像这样的组件:
<NewsFeedItem contents={item.contents}>
<VisibilitySensor itemId={item.id} onChange={this.handleVisibilityChange} />
</NewsFeedItem>
总结起来就是:
React.createElement(
NewsFeedItem,
{ contents: item.contents },
React.createElement(VisibilitySensor, { itemId: item.id, onChange: this.handleVisibilityChange })
);
VisibilitySensor
因为 React在每次渲染期间都会创建一个新的实例,children
所以 prop 总是会改变,所以创建NewsFeedItem
会使PureComponent
事情变得更糟,因为中的浅比较shouldComponentUpdate
可能运行起来并不便宜并且总是会返回 true。
我们的解决方案是将 VisibilitySensor 移到渲染道具中并使用绑定函数:
<NewsFeedItemWithHandlers
contents={item.contents}
itemId={item.id}
handleVisibilityChange={this.handleVisibilityChange}
/>
class NewsFeedItemWithHandlers extends PureComponent {
// The arrow function needs to get created outside of render, or the shallow comparison will fail
renderVisibilitySensor = () => (
<VisibilitySensor
itemId={this.props.itemId}
onChange={this.handleVisibilityChange}
/>
);
render() {
<NewsFeedItem
contents={this.props.contents}
renderVisibilitySensor={this.renderVisibilitySensor}
/>;
}
}
因为绑定函数只创建一次,所以相同的函数实例将作为 props 传递给NewsFeedItem
。
第二个障碍:渲染期间创建的内联对象
我们有一些代码在每次渲染时创建一个新的 url 助手实例:
getUrlHelper = () => new NewsFeedUrlHelper(
this.props.moreItemsUrlTemplate,
this.props.pollItemsUrlTemplate,
this.props.updateItemsUrlTemplate,
);
<NewsFeedItemWithHandlers
contents={item.contents}
urlHelper={this.getUrlHelper()} // new object created with each method call
/>
由于getUrlHelper
是根据 props 计算出来的,如果我们可以缓存之前的结果并重复使用,那么创建多个实例就没有必要了。我们曾经memoize-one
解决这个问题:
import memoizeOne from 'memoize-one';
const memoizedUrlHelper = memoizeOne(
(moreItemsUrlTemplate, pollItemsUrlTemplate, updateItemsUrlTemplate) =>
new NewsFeedUrlHelper({
moreItemsUrlTemplate,
pollItemsUrlTemplate,
updateItemsUrlTemplate,
}),
);
// in the component
getUrlHelper = memoizedUrlHelper(
this.props.moreItemsUrlTemplate,
this.props.pollItemsUrlTemplate,
this.props.updateItemsUrlTemplate
);
现在,仅当依赖的 props 发生变化时,我们才会创建一个新的 url 助手。
衡量差异
分析器现在显示出更好的结果:渲染 NewsFeed 现在从约 50 毫秒减少到约 5 毫秒!
PureComponent 可能会降低你的性能
与任何性能优化一样,衡量变化如何影响性能至关重要。
PureComponent
这不是一项可以盲目应用于应用程序所有组件的优化。它适用于具有深渲染树的列表中的组件,本例就是这种情况。如果您使用箭头函数作为 props、内联对象或带有 的内联数组作为 props PureComponent
,则shouldComponentUpdate
和 render
都会被调用,因为每次都会创建这些 props 的新实例!请测量更改的性能,以确保它们确实有所改进。
您的团队可能完全可以在简单的组件上使用内联箭头函数,例如button
在循环内为元素绑定 onClick 处理程序。请优先考虑代码的可读性,然后测量并在合适的地方添加性能优化。
奖励实验
由于在我们的代码库中,创建组件只是为了将回调绑定到 props 的模式非常常见,因此我们编写了一个助手程序,用于生成带有预绑定函数的组件。您可以在我们的 Github 仓库中查看。
您还可以使用窗口库(例如react-virtualized)来避免渲染不在视图中的组件。
感谢 Ian Mundy、Patrick Nausha 和 Auresa Nyctea 对本文早期草稿提供的反馈。
封面照片来自 Unsplash:https://unsplash.com/photos/ot-I4_x-1cQ
鏂囩珷鏉ユ簮锛�https://dev.to/dustinsoftware/making-renders-faster-with-the-react-165-profiler-f6f