FE FAME
首发于FE FAME

关于PureComponent对性能影响的一些探究

在之前关于React中的对象字面量型props的优化一文中,我们展示了一些“在prop不变的情况下计算出相同引用的对象”的手段,得到了不少同行的反馈,其中一个比较集中的话题就是“这样做是否有足够的收益”。

在此我的回答很明确:在该文中的示例上,这样做毫无收益

实验

为了验证一下我们的做法对性能的影响,我决定做一个很简单的实验。首先以下是我的电脑配置:

MacBook Pro (15-inch, 2016) 处理器:2.6 GHz Intel Core i7 内存:16 GB 2133 MHz LPDDR3 显卡:Intel HD Graphics 530 1536 MB

独显没有开启,使用的是Intel的集显。

我们使用以下的代码来验证效果,一次性渲染10000个DOM元素并对其进行更新:

const sizeToStyle = size => ({width: size, height: size});

const Item = ({size}) => (
    <div style={sizeToStyle(size)} />
);

class App extends Component {

    state = {
        value: 0
    };

    update = () => {
        const {value} = this.state;
        this.setState(({value}) => ({value: value + 1}));
        console.time(`update ${value + 1}`);
    };

    componentDidUpdate() {
        const {value} = this.state;
        console.timeEnd(`update ${value}`);
        if (value < 10) {
            this.update();
        }
    }

    render() {
        return (
            <div>
                <button type="button" onClick={this.update}>Click ME</button>
                {range(10000).map(i => <Item key={i} size={i} />)}
            </div>
        );
    }
}

只要按一下按钮,就能进行10次的更新,并在控制台显示相应的时间。在进行了一次测试后,我们计算平均的时间为285.36ms

随后我们将sizeToStyle函数改为一个带缓存的形式:

const SIZE_CACHE = [];
const sizeToStyle = size => (SIZE_CACHE[size] || (SIZE_CACHE[size] = {width: size, height: size}));

再次进行了测试,10次更新的平均值为369.72ms

可见即便排除电脑状态的意外,也几乎可以肯定加个缓存带来的额外运算反而有损于性能。对于DOM组件来说,使用基于PureComponent思路的优化无异于对着屏幕撸——没有卵用。

解剖

原本事情到这个阶段就算完了,也算回答了大家的问题。但是随之而来的,我们想整明白一个问题:

PureComponent在什么情况下才能开始发挥性能优势。为

为了得到一个相对有用的结论,我们对其工作原理进行了一些解剖:

  1. PureComponent的关键是在shouldComponentUpdate阶段使用shallowEquals进行了一次比较来减少更新。
  2. 在对象引用优化对DOM组件没有用处的前提下,减少更新更多的效果是减少render的调用次数。
  3. render中最常见的操作是声明JSX结构,翻译一下就是调用createElement函数。

因此不难看出,在最简单地理解下,PureComponent的性能收益是shallowEquals的开销与createElement的开销相互比较的结果。那么我们探索的其实就是,一次shallowEquals能顶多少个createElement

继续实验

为了弄明白这个阈值,我们打算先简单地看一下单个prop(即props对象只有一个属性)能换多少次createElement,为此我们制作了一个简单的实验:

import {pure} from 'recompose';

const Item = ({size}) => (
    <div style={{display: 'flex', width: size, height: size}}>
        {range(500).map(i => <div key={i} />)}
    </div>
);

const PureItem = pure(Item);

class App extends Component {

    state = {
        value: 0
    };

    update = () => {
        const {value} = this.state;
        this.setState(({value}) => ({value: value + 1}));
        console.time(`update ${value + 1}`);
    };

    componentDidUpdate() {
        const {value} = this.state;
        console.timeEnd(`update ${value}`);
        if (value < 10) {
            this.update();
        }
    }

    render() {
        return (
            <div>
                <button type="button" onClick={this.update}>Click ME</button>
                <Item size={400} />
            </div>
        );
    }
}

我们首先让一个组件渲染出500个DOM,对其进行10次更新,得到了平均时间为25.08ms,可见这个数字已经会严重影响60FPS的页面流畅性

随后我们使用著名的recompose库提供的pure高阶组件,轻松地得到了PureItem组件,并对其进行更新,10次的平均时间为0.79ms

显而易见得,在此处PureComponent发挥了预期的作用,大大地提升了性能。那么Item组件到底渲染多少个DOM可以超过0.79ms这个时间以至PureComponent的优化失去作用呢?在我的电脑上经过多次测试,得到的值差不多是8-10个。即当props中有一个属性时,对应渲染8个以上组件时,使用PureComponent会得到收益。

思考

上面的结论其实啥也说明不了,因为大部分组件既不可能只有一个prop,也不可能具有如此精确可控的DOM数量,更不可能让开发者在构建一整个React应用时不断地去记着这个仿佛能效比一般的阈值。

因而,我是比较赞同“不过早优化”的,但同时,我也坚信一个优秀的工程师必须是对性能和细节敏感的,一些显著的会出现性能负作用的场景,不应该仅仅因为一个“不过早优化”的信念就随之而去。在此,我们留下几个思考和结论:

  • 对于DOM而言,优化其prop为引用相等并没有意义。
  • 对于一个prop较少而渲染的DOM较多的组件,使用PureComponent是有明显收益的(通常一个信息流卡片就是10个左右的DOM),此时即便是函数组件,也可以使用pure等高阶组件进行一下优化。
  • React的应用是一个完整的树,而render是一个递归的过程,这注定一个组件最终产生的createElement数量并不仅仅是它自身render实现中的那部分。因此开发者最好在脑子中随时有一个完整的应用树的大致结构,并可以判断在哪几层、哪些节点上应用PureComponent具备最大的收益。
  • 基于整个应用来选择优化的层与节点是一个高难度的工作,相信有很多人做得好也有很多人做不了。因此如果并没有十足的把控力,依旧建议将一些最佳实践全程应用,以获得相对一致的性能结果。

文章被以下专栏收录