React 处理 render 的基本思维模式是每次一有变动,就去重新渲染整个应用。当DOM树很大时,遍历两棵树进行比对是很消耗性能的(特别是在顶层 setState 时)。
Kent C. Dodds 的译文:
One simple trick to optimize React re-renders
If you give React the same element you gave it on the last render, it wont bother re-rendering that element.
— Kent C. Dodds 🧑🚀 (@kentcdodds) June 24, 2019
如果你给 React 的元素和上次渲染的一样,它就不会再渲染那个元素了。
A little before and after optimization on a react component.
I didn't use any memoization to accomplish this, yet I was able to go from a 13.4ms to a 3.6ms render.
I also didn't do anything besides move code into an extra component, which ended up cutting out 27 lines of code. pic.twitter.com/xrUN0MUm5Y
— Brooks Lybrand - 🥔 developer (@BrooksLybrand) July 12, 2019
我没使用任何缓存相关的逻辑,但是我将组件树的绘制时间从14.3ms降到了3.4ms。
示例1
// play with this on codesandbox: https://codesandbox.io/s/react-codesandbox-g9mt5
import React from 'react'
import ReactDOM from 'react-dom'
function Logger(props) {
console.log(`${props.label} rendered`)
return null // what is returned here is irrelevant...
}
function Counter() {
const [count, setCount] = React.useState(0)
const increment = () => setCount(c => c + 1)
return (
<div>
<button onClick={increment}>The count is {count}</button>
<Logger label="counter" />
</div>
)
}
ReactDOM.render(<Counter />, document.getElementById('root'))
当这段代码运行的时候,"counter rendered" 第一次输出。之后每次 count 值增加的时候,"counter rendered" 都会输出到控制台。这是因为点击 button 的时候,state 发生变化了,导致 React 需要基于改变后的 State 去获取最新的元素来绘制并提交到 DOM。
接下来会有件有趣的事情发生。<Logger label="counter" /> 是静态的,在两次render中间没有变化。所以我们可以把它抽取取来,向下面这样:
示例2
// play with this on codesandbox: https://codesandbox.io/s/react-codesandbox-o9e9f
import React from 'react'
import ReactDOM from 'react-dom'
function Logger(props) {
console.log(`${props.label} rendered`)
return null // what is returned here is irrelevant...
}
function Counter(props) {
const [count, setCount] = React.useState(0)
const increment = () => setCount(c => c + 1)
return (
<div>
<button onClick={increment}>The count is {count}</button>
{props.logger}
</div>
)
}
ReactDOM.render(
// 改变的地方
<Counter logger={<Logger label="counter" />} />,
document.getElementById('root'),
)
再次运行的时候,除了那第一次的log输出,之后每次点击button的时候log都不出输出了!
发生了什么?
是什么引起了这样的变化?这还是和 React 的元素有关。在分析之前,你可以看看我的另一篇博客 “What is JSX”来大概了解一下 React 元素和 JSX 之间的关系。
当 React 调用 counter 方法的时候,它返回的内容大概如下:
// some things removed for clarity
const counterElement = {
type: 'div',
props: {
children: [
{
type: 'button',
props: {
onClick: increment, // this is the click handler function
children: 'The count is 0',
},
},
{
type: Logger, // this is our logger component function
props: {
label: 'counter',
},
},
],
},
}
这个叫做UI描述对象,他描述了 React 在 DOM 中应该创建的UI。
让我们点击 button 然后来看看它发生了哪些变化:
const counterElement = {
type: 'div',
props: {
children: [
{
type: 'button',
props: {
onClick: increment,
children: 'The count is 1',
},
},
{
type: Logger,
props: {
label: 'counter',
},
},
],
},
}
对比返现,唯一不同是 button 元素中,onClick 和 children 元素有变化。但是整个对象是重新生成了的。从使用 React 开始,每次 render ,这个对象都会重新创建(幸运的是,即便是移动端浏览器,创建这些UI描述对象也会很快,并不会出现明显的性能问题)。
事实上,在两次 render 之间,对 React 元素树中相同的部分进行分析更容易一些,所以下面是在这两次 render 之间没有变化的部分:
const counterElement = {
type: 'div',
props: {
children: [
{
type: 'button',
props: {
onClick: increment,
children: 'The count is 1',
},
},
{
type: Logger,
props: {
label: 'counter',
},
},
],
},
}
所有的元素类型都是相同的,并且 Logger 元素的 label 属性没有改变。然而这个对象每次渲染都会变化,即使该它的属性与之前相同。
关键点
因为整个UI描述对象重新创建了,所以 Logger 对象已经变化,React 需要重新调用 Logger 函数,以避免它在新的对象基础上生成新的 JSX 。
如果我们可以阻止属性在渲染之间发生变化,那么 React 就会知道我们要呈现效果不需要重新运行,JSX 也不会改变。这正是 React 内部要做的事情,从React一开始就一直如此。
但问题是我们如何确保属性对象在渲染之间不发生变化呢?
如果我们只创建一次 JSX 元素并重用它,那么我们每次都会得到相同的 JSX !
所以,回到上面示例2,React 自动为我们提供了这种优化机制。因为Logger 从来都没发生变化,所以自然就不需要 re-render 。
结论
如果你正在做性能优化,你可以试试这么做:
- 将你认为比较消耗性能的组件提升到父级(这个父级组件相对来说不经常被重绘)
- 将这个组件当做属性,传递到子组件中。