React应用优化:避免不必要的render

1.shouldComponentUpdate

  React在组件的生命周期方法中提供了一个钩子shouldComponentUpdate,这个方法默认返回true,表示需要重新执行render方法并使用其返回的结果作为新的Virtual DOM节点。通过实现这个方法,并在合适的时候返回false,告诉React可以不用重新执行render,而是使用原有的Virtual DOM 节点,这是最常用的避免render的手段,这一方式也常被很形象地称为“短路”(short circuit)。
  shouldComponentUpdate方法会获得两个参数:nextProps及nextState。常见的实现是,将新旧props及state分别进行比较,确认没有改动或改动对组件没有影响的情况下返回false,否则返回true。
如果shouldComponentUpdate使用不当,实现中的判断并不正确,会导致产生数据更新而界面没有更新、二者不一致的bug,“在合适的时候返回false”是使用这个方法最需要注意的点。要在不对组件做任何限制的情况下保证shouldComponentUpdate完全的正确性,需要手工依据每个组件的逻辑精细地对props、state中的每个字段逐一比对,这种做法不具备复用性,也会影响组件本身的可维护性。
  所以一般情况下,会对组件及其输入进行一定的限制,然后提出一个通用的shouldComponentUpdate实现。
首先要求组件的render是“pure”的,即对于相同的输入,render总是给出相同的输出。在这样的基础上,可以对输入采用通用的比较行为,然后依据输入是否一致,直接判断输出是否会是一致的。若是,则可以返回false以避免重复渲染。
其次是对组件输入的限制,要求props与state都是不可修改的(immutable)。如果props与state会被修改,那么判断两次render的输入是否相同便无从说起。
  最后值得一说的是,“通用的比较行为”的实现。从理论上说,要判断JavaScript中的两个值是否相等,对于基本类型可以通过===直接比较,而对于复杂类型,如Object、Array,===意味着引用比较,即使引用比较结果为false,其内容也可能是一致的,遍历整个数据结构进行深层比较(deep compare)才能得到准确的答案。但是,shouldComponentUpdate是一个会被频繁调用的方法,而深比较是代价很大的行为,如果数据结构较为复杂,进行深比较甚至会不如直接执行一遍render,通过shouldComponentUpdate实现“短路”也就失去了意义。因此一般来说,会采取一个相对可以接受的方案:浅比较(shallow compare)。相比深比较会遍历整个树状结构而言,浅比较最多只遍历一层子节点。即对于下例的两个对象:

const props = { foo, bar };
const nextProps = { foo, bar };

  浅比较会对props.foo与nextProps.foo、props.bar与nextProps.bar进行比较(要求严格相等),而不会深入比较props.foo与nextProps.foo的内容。如此,比较的复杂度会大大降低。

2.Mixin与HoC

  前面提到,一个普遍的性能优化做法是,在shouldComponentUpdate中进行浅比较,并在判断为相等时避免重新render。PureRenderMixin是React官方提供的实现,采用Mixin的形式,用法如下。

var PureRenderMixin = require('react-addons-pure-render-mixin');
React.createClass({
    mixins: [PureRenderMixin],

    render: function() {
        return <div className={this.props.className}>foo</div>;
    }
});

  Mixin是ES5写法实现的React组件所推荐的能力复用形式,ES6写法的React组件并不支持,虽然你也可以这么做。

import PureRenderMixin from 'react-addons-pure-render-mixin';
class FooComponent extends React.Component {
    constructor(props) {
        super(props);
        this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this);
    }

    render() {
        return <div className={this.props.className}>foo</div>;
    }
}

  手动将 PureRenderMixin提供的shouldComponentUpdate方法挂载到组件实例上。但与其这样,不如直接使用另一个React提供的辅助工具shallow-compare。

import shallowCompare from 'react-addons-shallow-compare';
export class FooComponent extends React.Component {
    shouldComponentUpdate(nextProps, nextState) {
        return shallowCompare(this, nextProps, nextState);
    }

    render() {
        return <div className={this.props.className}>foo</div>;
    }
}

  上面两种方式本质上是一致的。
  另外也有以高阶组件形式提供这种能力的工具,如库recompose提供的pure方法,用法更简单,很适合ES6写法的React组件。

import {pure} from 'recompose';

class FooComponent extends React.Component {
    render() {
        return <div className={this.props.className}>foo</div>;
    }
}

const OptimizedComponent = pure(FooComponent);

  与前两种方式不同的是,这种做法也支持函数式组件。

const FunctionalComponent = ({ className }) => (
<div className={className}>foo</div>;
);
const OptimizedComponent = pure(FunctionalComponent);

3.不可变数据

  前面提到,为了让这种“短路”的做法产生预期的效果,要求数据(props与state)是不可变的。然而在JavaScript中,数据天生是可变的,修改复杂的数据结构也是很自然的做法。

const a = { foo: { bar: 1} };
a.foo.bar = 2;

  但以这种方式修改数据会导致使用了a作为props的组件失去实现shouldComponentUpdate的意义。为此,Facebook的工程师开发了immutable-js用于创建并操作不可变数据结构。典型的使用是如下这样的。

import Immutable from 'immutable';
const map1 = Immutable.Map({ a: 1, b: 2, c: 3 });
const map2 = map1.set('b', 50);
map1.get('b'); // 2
map2.get('b'); // 50

  使用immutable-js的代价主要有两部分,一方面库本身的体积并不算小(55.7KB,Gzip压缩后16.3KB),另一方面在开发中需要引入一套新的数据操作方式。除了immutable-js外,mori、Cortex等也是可选的方案,但也都有着类似的问题。幸而大部分情况下都可以选择另外一个相对代价较小的做法:使用 JavaScript原生语法或方法中对不可变数据更友好的那些部分。
  对于基本数据类型(boolean、number、string 等),它们本身就是不可变的,它们的操作与计算会产生新的值。而对于复杂数据类型,主要是object与array,在修改时需要稍加注意。
对于object,像如下这样的操作方式是会修改原数据本身的。

obj.a = 1;
obj['b'] = 2;
Object.assign(obj, { a: 1 });

  而下面这样的操作是不会的。

const newObj = Object.assign({}, obj, { a: 1 });

  如果借助Object Rest/Spread Properties的语法(目前处于Stage 2的提案,在未来可能成为标准),还可以如下这么写。

const newObj = { ...obj, { a: 1 } };

  对于array,如下这样的操作会修改原数据本身。

arr[0] = 1;
arr.push(2);
arr.pop();
arr.unshift(3);
arr.shift();
arr.splice(0, 1, [2]);

  而Array.prototype也提供了很多不会修改原数组的变换方法,它们会返回一个新的数组作为结果。

arr.concat(1);
arr.slice(-1);
arr.map(item => item.name);
arr.filter(item => item.name !== '');

  也可以通过增加一步复制数组的行为,然后在新的数组上进行操作。

const newArr = Array.from(arr);
newArr.push(1);

const newArr2 = Array.from(arr);
newArr2[0] = 1;

  如果借助ES6的Array Rest/Spread语法,还可以如下这么做。

[...arr, 1];
[...arr.slice(0, -1), 1];

  React官方也有提供一个便于修改较复杂数据结构深层次内容的工具——react-addons-update,它的用法借鉴了MongoDB的query语法(示例来自React官方文档)。

var update = require('react-addons-update');

var newData = update(myData, {
    x: {y: {z: {$set: 7}}},
    a: {b: {$push: [9]}}
});

  如上的行为会在myData的基础上创造一个新的对象newData,且newData.x.y.z会被赋值为7,newData.a.b的内容(一个数组)会被push进值9。对比不使用update的写法(示例来自React官方文档)如下。

var newData = extend(myData, { x: extend(myData.x, { y: extend(myData.x.y, {z: 7}), }), a: extend(myData.a, {b: myData.a.b.concat(9)}) });

  上例中extend(myData, ...) 的行为类似于Object.assign({},myData, ...)。可见,在很多场景下,update都是一个非常有用的工具,可以提高代码的简洁性与可读性。

4.计算结果记忆

  使用immutable data可以低成本地判断状态是否发生变化,而在修改数据时尽可能复用原有节点(节点内容未更改的情况下)的特点,使得在整体状态的局部发生变化时,那些依赖未变更部分数据的组件所接触到的数据保持不变,这在一定程度上减少了重复渲染。
  然而很多时候,组件依赖的数据往往不是简单地读取全局state上的一个或几个节点,而是基于全局state中的数据计算组合出的结果。以一个Todo List应用为例,在全局的state中通过list存放所有项,而组件VisibleList需要展示未完成项。

const stateToProps = state => {
    const list = state.list;
    const visibleFilter = state.visibleFilter;
    const visibleList = list.filter(
        item => (item.status === visibleFilter)
    );
    return {
        list: visibleList
    };
};
function List({list}) {/ ... /}
const VisibleList = connect(stateToProps)(List);

  如上,在方法stateToProps中基于state计算出当前要展示的项列表visibleList,并将其传递给组件List进行展示。有一个潜在的性能问题是,当state的内容变更时,即使state.list与state.filter均未变更,每次执行stateToProps都会计算生成一个新的visibleList数组。这时即便组件List在shouldComponentUpdate方法中对props进行比较,得到的结果也是不相等的,从而触发重新render。
  当应用变得复杂时,绝大部分组件所使用的数据都是基于全局state的不同部分,通过各种方式计算处理得到的,这一情况会随处可见,很多基于shouldComponentUpdate的“短路”式优化都会失去效果。
  对此,有一个简单的解决方法是记忆计算结果。一般把从state计算得到一份可用数据的行为称为selector。

const visibleListSelector = state => state.list.filter(
    item => (item.status === state.visibleFilter)
);

  如果这样的selector具备记忆能力,即在其结果所依赖的部分数据未变更的情况下,直接返回先前的计算结果,那么前面提到的问题将迎刃而解。
  reselect就是实现了这样一个能力的JavaScript库。它的使用很简单,下面来改写一下上边的几个selector。

import { createSelector } from 'reselect';

const listSelector = state => state.list;
const visibleFilterSelector = state => state.visibleFilter;
const visibleListSelector = createSelector(
    listSelector,
    visibleFilterSelector,
    (list, visibleFilter) => list.filter(
        item => (item.status === visibleFilter)
    )
);

  可以看到,实现了3个selector:listSelector、visibleFilterSelector及visibleListSelector,其中visibleListSelector由listSelector与visibleFilterSelector通过createSelector组合而成。即,一个selector可以由一个或多个已有的selector结合一个计算函数组合得到,其中组合函数的参数就是传入的几个selector的结果。reselect的价值不仅在于提供了这种组合selector的能力,而且通过createSelector组合产生的selector具有记忆能力,即除非计算函数有参数变更,否则它不会被重新执行。也就是说,除非state.list或state.visibleFilter发生变化,visibleListSelector才会返回新的结果,否则visibleListSelector会一直返回同一份被记忆的数据。
  可见,类似reselect这样的方案帮助解决了基于原始state的计算结果比较的问题,有助于实现shouldComponentUpdate来提升应用性能。同时,将基于state的计算行为以统一的形式实现并组装,也有助于复用逻辑,提高应用的可维护性。

5.容易忽视的细节

  最后,在组件的实现中,一些很容易被忽视的细节,会趋于让相关组件的shouldComponentUpdate失效,给性能带来潜在的风险。它们的特点是,对于相同的内容,每次都创造并使用一个新的对象/函数,这一行为存在于前面提到的selector之外,典型的位置包括父组件的render方法、生成容器组件的stateToProps方法等。下面是一些常见的例子。

  • 函数声明
    经常在render中声明函数,尤其是匿名函数及ES6的箭头函数,用来作为回调传递给子节点,一个典型的例子如下。

    const onItemClick = id => console.log(id);
    function List({list}) {
    const items = list.map(
        item => (
    <Item key={item.id} onClick={() => onItemClick(item.id)}>{item.name}</Item>
        )
    );
    return (
    <p>{items}</p>
    );
    }
    

      如上,希望监听列表每一项的点击事件,获取当前被点击的项的ID,很自然地,在render 中为每个item创建了箭头函数作为其点击回调。这会导致每次组件BtnList的render都会重新生成一遍这些回调函数,而这些回调函数是子节点Item的props的组成,从而子节点不得不重新渲染。

  • 函数绑定
    与函数声明类似,函数绑定(Function.prototype.bind)也会在每次执行时产生一个新的函数,从而影响使用方对props的比对。
    函数绑定的使用场景有两种,一是为函数绑定上下文(this),如下。

    class WrappedInput extends React.Component {
    // ……
    onChange(e) {
        //在此添加回调代码
    }
    render() {
        return (
    <Input onChange={this.onChange.bind(this)} />
        );
    }
    //……
    }
    

      这种情况一般出现在ES6写法的React组件中,因为通过ES5的写法React.createClass创建的组件,在被实例化时,其原型上的方法会被统一绑定到实例本身。因此对于这种情况,通常建议参考ES5写法的组件的做法,将bind行为提前,即在实例化时将需要绑定的方法进行手动绑定。

    class WrappedInput extends React.Component {
    constructor(props) {
    super(props);
    this.onChange = this.onChange.bind(this); }
    //……
    onChange(e) {
    // do some stuff……}
    render() {
    return ( ); } //……}
    

      这样bind只需执行一次,每次render传入给子组件Input的都是同一个方法。
      二是为函数绑定参数,在父组件的同一个方法需要给多个子节点使用时尤为常见,如下。

    class List extends React.Component {
    onRemove(id) {
        //在此添加回调代码
    }
    render() {
        const items = this.props.items.map(
            item => (
    <Item key={item.id} onRemove={this.onRemove.bind(this, item.id)}>
                    {item.name}
    </Item>
            )
        );
        return (
    <section>{items}</section>
        );
    }
    }
    

      对于这个场景最简单的做法是,将bind了上下文的父组件方法onRemove连同item.id传递给子组件,由子组件在调用onRemove时传入item.id,像如下这样。

    class Item extends React.Component {
    onRemove() {
        this.props.onRemove(this.props.id);
    }
    render() {
        //在此this.onRemove方法
    }
    }
    class List extends React.Component {
    constructor(props) {
        super(props);
        this.onRemove = this.onRemove.bind(this);
    }
    onRemove(id) {}
    render() {
        const items = this.props.items.map(
            item => (
    <Item key={item.id} onRemove={this.onRemove} id={id}>
                    {item.name}
    </Item>
            )
        );
        return (
    <section>{items}</section>
        );
    }
    }
    

      但不得不承认的是,对于子组件Item来说,拿到一个通用的onRemove方法是不太合理的。所以会有一些解决方案采取这样的思路:提供一个具有记忆能力的绑定方法,对于相同的参数,返回相同的绑定结果。或者借助React组件记忆先前render结果的特点,将绑定行为实现为一个组件,Saif Hakim在文章《Performance EngineeringWith React》中介绍了一种这样的实现,感兴趣的读者可以了解一下。
      笔者的观点是,绝大部分情况下,都不至于需要为了性能做这么多的妥协。除非极端情况,否则代码的简洁、可读要比性能更重要。对于这种情况,已知的解决方法或者会影响应用逻辑分布的合理性,或者会引入过多的复杂度,这里提出仅供参考,实际的必要性需要结合具体项目分析。

  • object/array字面量
    代码中的对象与数组字面量是另一处“新数据”的源头,它们经常表现为如下样式。

    function Foo() {
    return (
    <Bar options={['a', 'b', 'c']} />
    );
    }
    

       处理这种情况,只需将字面量保存在常量中即可,如下。

    const OPTIONS = ['a', 'b', 'c'];
    function Foo() {
    return (
    <Bar options={OPTIONS} />
    );
    }
    

  本文选自《React与Redux开发实例精解》。

                      
  想及时获得更多精彩文章,可在微信中搜索“博文视点”或者扫描下方二维码并关注。
                      

时间: 2024-10-31 09:30:57

React应用优化:避免不必要的render的相关文章

热点技术:React性能优化总结

初学者对React可能满怀期待,觉得React可能完爆其它一切框架,甚至不切实际地认为React可能连原生的渲染都能完爆--对框架的狂热确实会出现这样的不切实际的期待.让我们来看看React的官方是怎么说的.React官方文档在Advanced Performanec这一节,这样写道: One of the first questions people ask when considering React for a project is whether their application wi

React Native JS Module 加载性能优化

关于React Native 性能 React Native 在手淘中已开始逐步推广, 在拍立淘首页的使用场景中,我们发现React Native并没有想 象中的那么快,实测效果在离线状态下性能甚至比不过H5 WindVane,React Native的UI会出现延迟渲 染存在视觉差,经过具体的代码性能测试,整个过程平均在300 ms (IPhone 5S机型下,整个JS文件 400K), 然后其核心系统调用代码加载解析整个JS (JSEvaluateScript)耗时在220 ms左右,在目前

基于react的H5开发入门基础简介

React官方网站React中文文档javascript参考教程 一.react是什么? React是一个 JavaScript 库 ,可用来创建用户界面的,可认为是MVC 中的V(视图). React是 基于Component 的,即组件,React认为一切页面元素都可以抽象成组件,且大部分操作都是针对组件的. 1.1 React诞生背景 传统的DOM(文件对象模型)操作会对整个DOM树进行重新渲染,时间成本.复杂度高 ,很慢且容易造成卡顿使页面短暂失去响应. 为解决DOM操作慢的问题,Rea

[译] 高性能 React:3 个新工具加速你的应用

本文讲的是[译] 高性能 React:3 个新工具加速你的应用, 原文地址:High Performance React: 3 New Tools to Speed Up Your Apps 原文作者:Ben Edelstein 译文出自:掘金翻译计划 本文永久链接:github.com/xitu/gold-m- 译者:sunui 校对者:yzgyyang.reid3290 通常来说 React 是相当快的,但开发者也很容易犯一些错误导致出现性能问题.组件挂载过慢.组件树过深和一些非必要的渲染周

从性能角度看react组件拆分的重要性

React是一个UI层面的库,它采用虚拟DOM技术减少Javascript与真正DOM的交互,提升了前端性能:采用单向数据流机制,父组件通过props将数据传递给子组件,这样让数据流向一目了然.一旦组件的props或则state发生改变,组件及其子组件都将重新re-render和vdom-diff,从而完成数据的流向交互.但是这种机制在某些情况下比如说数据量较大的情况下可能会存在一些性能问题.下面就来分析react的性能瓶颈,并用结合着react-addons-perf工具来说明react组件拆

Vue.js 2.0 和 React、Augular等其他前端框架大比拼_javascript技巧

React React 和 Vue 有许多相似之处,它们都有: 使用 Virtual DOM 提供了响应式(Reactive)和组件化(Composable)的视图组件. 保持注意力集中在核心库,伴随于此,有配套的路由和负责处理全局状态管理的库. 相似的作用域,我们会用更多的时间来讲这一块的比较.不仅我们要保持技术的准确性,同时兼顾平衡.我们指出React比Vue更好的地方,例如,他们的生态系统和丰富的自定义渲染器. React社区在这里非常积极地帮助我们实现这一平衡,特别感谢来自 React

如何利用工具提高 React 页面渲染性能之 Perf

前言 用 React 一段时间了,也做了不少列表页.在用 React 做无限下拉加载的列表页时发现个问题:页面前几页渲染速度还挺快的,但是越往下拉加载内容页面的渲染就越慢.这是怎么回事呢?让我们先来看下 React 的组件渲染流程吧. React 的组件渲染流程 React 的组件渲染分为初始化渲染和更新渲染.在初始化时,React 会调用根组件下所有组件的 render 方法进行渲染. 在每个生命周期更新时,React 会先调用 shouldComponentUpdate(nextProps,

如何写出漂亮的React组件

在Walmart Labs的产品开发中,我们进行了大量的Code Review工作,这也保证了我有机会从很多优秀的工程师的代码中学习他们的代码风格与样式.在这篇博文里我会分享出我最欣赏的五种组件模式与代码片.不过我首先还是要谈谈为什么我们需要执着于提高代码的阅读体验.就好像你有很多种方式去装扮一只猫,如果你把你的爱猫装扮成了如下这样子: 你或许可以认为萝卜青菜各有所爱,但是代码本身是应当保证其可读性,特别是在一个团队中,你的代码是注定要被其他人阅读的.电脑是不会在意这些的,不管你朝它们扔过去什么

超级给力的JavaScript的React框架入门教程_基础知识

 React 是 Facebook 里一群牛 X 的码农折腾出的牛X的框架. 实现了一个虚拟 DOM,用 DOM 的方式将需要的组件秒加,用不着的秒删.React 扮演着 MVC 结构中 V 的角色, 不过你要是 Flux 搭配使用, 你就有一个很牛X的能让轻松让 M 和 V 同步的框架了,Flux 的事以后再说~组件们 在 React 中,你可以创建一个有特殊功能的组件,这在 HTML 元素里你是打着灯笼也找不到的,比如这个教程里的下拉导航.每个组件都有自己的地盘(scope),所以我们定义一