优化React re-render的小技巧

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 。

结论

如果你正在做性能优化,你可以试试这么做:

  1. 将你认为比较消耗性能的组件提升到父级(这个父级组件相对来说不经常被重绘)
  2. 将这个组件当做属性,传递到子组件中。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,544评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,430评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,764评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,193评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,216评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,182评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,063评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,917评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,329评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,543评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,722评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,425评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,019评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,671评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,825评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,729评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,614评论 2 353