性能优化 - 解决重新渲染之前,先解决渲染慢的问题

1,176 阅读6分钟

本文为译文,仅作为个人学习之用。有翻译不到位地方,欢迎指出,我会及时修正。 英文原文:kentcdodds.com/blog/fix-th…

How to start optimizing your React app renders 如何开始优化你的React引用的渲染呢?

性能是个很重要的问题,我们应该让apps尽可能地快。应该怎么做才能使我们的代码同时在有效性和复杂性上同时有一个较大的提升(我们可以更快对apps改进和更新)?

当我们谈论React优化,有一点是人们经常提到的——优化重新渲染。我们来统一一下要讨论的问题:

function Counter() {
  const [count, setCount] = React.useState(0)
  const increment = () => setCount(c => c + 1)
  return <button onClick={increment}>{count}</button>
}

每当我们点击按钮,都触发re-render。但是什么是re-render?

什么是重新渲染

当React第一次发布时,很多人的关注点在于,多亏React的虚拟树Virtual DOM,已有UI库的性能得到了提升。大多数流行的UI库在当时要么支持用户自主的更新DOM,要么是为用户更新DOM,但是是以某种顺序来让每个组件得到必要的更新。基本上可以归结为:

  1. 更新DOM很慢,比如调用element.appendChild(childElement)
  2. 执行次数越多,性能问题就越复杂。
  3. 可以通过立即执行所有必要的更新来避免一些性能问题
  4. 如果我们批处理(batch)所有DOM更新,则可以减少快速连续多次更新DOM的性能问题。

因此React团队决定采用批量更新DOM。如果一个状态state的改变造成30个DOM更新,则一次性更新,而不是一个一个更新。实现批量处理,他们将不得不拥有更新DOM的权利。所以我们有React.createElement(这就是JSX)去描述我们希望让DOM是什么样子,以及什么时候状态更新,React再次调用我们的function从而获取我们需要渲染到DOM中的React元素。然后React将这些新的React元素与我们上次用于渲染的元素进行对比。通过对比可以知道那些DOM需要更新,然后以最高效的方式为我们进行这些更新。这个更新DOM的过程成为committing,因为我们正使用你“渲染”并“提交”这些更新到DOM的React元素。

这确实是一个重要的区别,希望你不要错过(以及这个名字也有点舞蹈,所以我想说清楚)。 (1)“渲染”(Render)是当React获取React元素时调用你的function。 (2)“调和”(Reconciliation)是当React对比之前和当前的React元素。 (3)“提交”(commit)是当React获取到区别,更新DOM。

render → reconciliation → commit
      ↖                   ↙
           state change

清楚一些:

(1)渲染阶段:创建React元素React.createElement(了解更多

(2)调和阶段:将以前的元素与新的元素进行比较(了解更多

(3)提交阶段:更新DOM(如果需要)。

Typically, the slowest part of this is the "commit" phase when the DOM is updated. But not all DOM updates are slow. In fact, it's probably a bit misleading to state simply that "the DOM is slow" because it's more nuanced than that. DOM updates like adding/removing event listeners are really fast. The slow part of the DOM is "layout" (learn more about slow layout here).

通常,最慢部分是当DOM更新时的提交阶段。但是不是所有的DOM更新都慢。实际上,仅仅说“DOM慢”可能有点误导,因为它比这更细微(nuanced)。DOM更新像新增、移动事件监听都是很快的。慢的部分是DOM的布局(了解更多

得益于React的批处理和优化的代码,我们可以避免很多陷阱(pitfalls),而不必担心这个问题,但是它肯定会时不时地"咬到"我们。

不必要的重新渲染

Just because a component is re-rendered, doesn't mean that will result in a DOM update. Here's a quick contrived example of that: 恰恰因为组件的重新渲染,并不意味着DOM更新,因此这里有一些人为例子:

function Foo() {
  return <div>FOO!</div>
}
function Counter() {
  const [count, setCount] = React.useState(0)
  const increment = () => setCount(c => c + 1)
  return (
    <>
      <Foo />
      <button onClick={increment}>{count}</button>
    </>
  )
}

每次你点击按钮,Foo这个方法被调用,但是DOM不会重新渲染。因为,Foo这一组件根本没有DOM更新。通常将其称为“不必要的重新渲染”。

不幸的是,关于“渲染”和“提交”之间的区别,存在相当多的困惑(confusion)。很多人知道(或者至少他们听说)DOM速度慢。但是并没有意识到只是因为组件的re-render,不能意味着DOM会被更新。因为这个误导,他们认为“一个组件渲染了但它却没有DOM要更新”是一个性能瓶颈(performance bottleneck)。

在某些情况下,这肯定是一个问题,但是通常,即使是低端设备上的移动浏览器在创建对象(渲染阶段)和比较它们(协调阶段)时也非常快。 那么重新渲染有什么问题呢?

Slow renders 渲染慢

鉴于(Given that)JavaScript确实可以快速处理(handling)渲染和调和阶段,那么为什么我的应用会在有重新渲染时卡顿呢?

这种情况下,我建议你的问题可能是不必要的重新渲染,但是一般来说,更有可能是渲染得慢的缘故。 有几件事是你的代码在渲染阶段时做的,导致了渲染慢。你应该首先去诊断并修复。一旦你修复了这些问题,你就可以重新配置你的app,看一下是否是不必要的渲染导致的问题。

实际上,如果你忽略了渲染慢的问题溯源而是直接去减少重新渲染,那么可能会引起更糟糕的情况,遇到更复杂的代码。

假设你每次眨眼都必须打自己的脸punch 😉🤛 🥴。也许你会想:“天哪,我想我最好不要眨眼!”知道我会怎么说?我会说,你应该停止打自己的脸当你眨眼的时候!所以不要直接去减少坏事情(渲染慢)的发生频率,也许你可以评估一下坏事情并且当你的眼睛需要你时,去自由感受眨眼(渲染)。

如何解决渲染慢的问题?

所以我们得出结论,应该先解决渲染慢的问题,这样就可以确定是否重新渲染依然是个问题。如何解决渲染慢的问题。你通常已经知道哪个交互为用户带来了感觉“简陋”的经历。通常是当你打开一个tab,点击一个按钮,或者在一个文字区域输入内容时。 你应该做:使用你浏览器中的分析工具,开始分析你的应用,执行交互,然后再停止分析。比如: 看Chrome DevTools.

当你找到哪一部分bug导致你用了最长时间去修复,然后再次尝试使用分析工具发现改进或回归之处。不要错过React DevTools分析工具,真的好用!演示

结论

保证100%的渲染都是必要的这件事并不重要。如果渲染慢,仍然会给用户带来不好的体验。 每次眨眼时都不要打脸。首先修复缓慢的渲染,然后处理“不必要的重新渲染”(如果您仍然需要)。 祝你好运!