使用 React 16.5 分析器加快渲染速度

2025-06-08

使用 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>
Enter fullscreen mode Exit fullscreen mode

总结起来就是:

React.createElement(
  NewsFeedItem,
  { contents: item.contents },
  React.createElement(VisibilitySensor, { itemId: item.id, onChange: this.handleVisibilityChange })
);
Enter fullscreen mode Exit fullscreen mode

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}
    />;
  }
}
Enter fullscreen mode Exit fullscreen mode

因为绑定函数只创建一次,所以相同的函数实例将作为 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
/>
Enter fullscreen mode Exit fullscreen mode

由于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
);
Enter fullscreen mode Exit fullscreen mode

现在,仅当依赖的 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
PREV
为什么我辞去亚马逊 50 万美元的工作,选择自己创业
NEXT
什么是调度函数?