SunXinFei / sunxinfei.github.io

前后端技术相关笔记,已迁移到 Issues 中
https://github.com/SunXinFei/sunxinfei.github.io/issues
32 stars 3 forks source link

React + Redux + Immutable 的性能优化 #9

Open SunXinFei opened 6 years ago

SunXinFei commented 6 years ago

问题描述

最近写的项目,工作桌面拖拽布局,项目中涉及到大量卡片的渲染,卡片分组和卡片都可以拖拽,拖拽和渲染都是耗费性能和内存,当卡片量多的时候,发现会出现内存占用过高和浏览器卡顿的现象,需要进行优化。这个项目旧的版本没有引入redux,为了多个组件之间状态的通信管理方便,后面加入了react-redux,所以拖拽优化的过程也涉及到react-redux的一些tips

优化的小结

优化之前要知道在dev开发模式和实际生产模式,性能本身就有差别,在代码不变的情况下,生产模式下的性能更高。 在项目开发时,除了一些基本的影响性能的写法上注意一下,不必上来就开始谈性能,项目上来就着手优化,这样会导致项目在开始阶段束手束脚。性能出现问题再去解决也不迟。一般情况下,处理好shouldComponentUpdate,不该render的不render,性能会出现明显提升,SCU要写好,不然可能会出现该DOM变化的时候,反而不变化的Bug。

一些原理

React 构建和维护渲染 UI 的内部表示。它包括你从组件中返回的 React 元素。这些内部状态使得React只有在必要的情况下才会创建DOM节点和访问存在DOM节点,因为对JavaScript对象的操作是比DOM操作更快。这被称为”虚拟DOM”,React Native的基于上述原理。 当组件的 props 和 state 更新时,React 通过比较新返回的元素 和 之前渲染的元素 来决定是否有必要更新DOM元素。如果二者不相等,则更新DOM元素。 react 组件生命周期 image

SunXinFei commented 6 years ago

工具

文档来源

React-developer-tools

可以使用 React DevTools 可视化这些重新渲染的虚拟DOM:

Does "Highlight Updates" trace renders? With React 15 and earlier, "Highlight Updates" had false positives and highlighted more components than were actually re-rendering. Since React 16, it correctly highlights only components that were re-rendered. 安装好这个工具之后,打开Chrome浏览器的开发者工具,可以看到一个React的tab页,在这里面有个勾选。 当与你的页面进行交互,你应该会看到,所有重新渲染的组件周围都会出现高亮显示的边框。 反过来,这可以让你知道没有必要重新渲染的组件。 image

perf

Note: As of React 16, react-addons-perf is not supported. Please use your browser’s profiling tools to get insight into which components re-render.

react_perf

开发模式 中,你可以在支持相关功能的浏览器中使用性能工具来可视化组件 装载(mount) ,更新(update) 和 卸载(unmount) 的各个过程。例如: React components in Chrome timeline 在 Chrome 中操作如下: 通过添加 ?react_perf 查询字段加载你的应用(例如:http://localhost:3000/?react_perf)。 打开 Chrome DevTools Performance 并点击 Record 。( 愚人码头注:如何使用时间轴工具 译文) 执行你想要分析的操作,不要超过20秒,否则 Chrome 可能会挂起。 停止记录。 在 User Timing 标签下,React事件将会分组列出。

SunXinFei commented 6 years ago

方法

使用PureComponent + immutable.js

PureComponent使用浅比较判断组件是否需要重绘,所以当出现直接对state数据做操作时,数据的修改并不会导致重绘,如下代码:

  options.push(new Option())
  options.splice(0, 1)
  options[i].name = "Hello"

为了避免出现这些问题,推荐使用immutable.js。immutable.js 实现的原理是 Persistent Data Structure(持久化数据结构),也就是使用旧数据创建新数据时,要保证旧数据同时可用且不变。同时为了避免 deepCopy 把所有节点都复制一遍带来的性能损耗,Immutable 使用了 Structural Sharing(结构共享),即如果对象树中一个节点发生变化,只修改这个节点和受它影响的父节点,其它节点则进行共享。请看下面动画: immutable

其他两个可以帮助我们使用不可变数据的库分别是:seamless-immutableimmutability-helper

不可变数据提供了一种更简单的方式来追踪对象的改变,这正是我们实现 shouldComponentUpdate 所需要的。这将会提供可观的性能提升。

避免更新

在一些情况下,你的组件可以通过重写生命周期函数 shouldComponentUpdate 来优化性能。该函数会在重新渲染流程前触发。该函数的默认实现中返回的是 true,使得 React 执行更新操作:

shouldComponentUpdate(nextProps, nextState) {
  return true;
}

如果你的组件在部分场景下不需要更行,你可以在 shouldComponentUpdate 返回 false 来跳过整个渲染流程,包括调用render() 和之后流程。

SunXinFei commented 6 years ago

其他注意点

  1. render方法中,不要出现bind(this), bind应该在constructor中提前绑定好 函数也经常作为props传递,由于每次需要为内联函数创建一个新的实例,所以每次function都会指向不同的内存地址。比如:
    render() {
     <MyInput onChange={e => this.props.update(e.target.value)} />;
    }

    以及:

    update(e) {
     this.props.update(e.target.value);
    }
    render() {
     return <MyInput onChange={this.update.bind(this)} />;
    }

    注意第二个例子也会导致创建新的函数实例。为了解决这个问题,需要提前绑定this指针:

    constructor(props) {
    super(props);
    this.update = this.update.bind(this);
    }
    update(e) {
    this.props.update(e.target.value);
    }
    render() {
    return <MyInput onChange={this.update} />;
    }

    2.组件传递数据,传递基本数据结构,避免传递复杂数据结构(数组、对象) 3.Literal Array与Literal Object(其他文章提出,我本人没实验)

    {this.props.items.map(i =>
    <Cell data={i} options={this.props.options || []} />
    )}

    若options为空,则会使用[]。[]每次会生成新的Array,因此导致Cell每次的props都不一样,导致需要重绘。解决方法如下:

    const default = [];
    {this.props.items.map(i =>
    <Cell data={i} options={this.props.options || default} />
    )}
  2. 绑定数据时不要出现扩展运算符
    <CardData dragCardID={-1} 
                type={c.apptype}
                id={c.pk_appregister}
                groupID = {groupID}
                groupIndex = {index}
                key={`${groupID}_${c.pk_appregister}`}
               ...{c}//这个不要出现
                />

    子组件需要什么数据就传递什么数据,而且尽量传递浅层的数据

SunXinFei commented 6 years ago

绑定函数的Bad写法:

  1. Calling .bind Within render

    class Button extends React.Component {
    handleClick() {
    console.log('clickity');
    }
    
    render() {
    return (
      <button onClick={this.handleClick.bind(this)}/>
    );
    }
    }

    另一种写法,同样Bad

    class Button extends React.Component {
    handleClick() {
    console.log('clickity');
    }
    
    render() {
    var handleClick = this.handleClick.bind(this);
    return (
      <button onClick={handleClick}/>
    );
    }
    }
  2. Arrow Function in render,render中使用箭头函数,Bad

    class Button extends React.Component {
    handleClick() {
    console.log('clickity');
    }
    
    render() {
    return (
      <button onClick={() => this.handleClick()}/>
    );
    }
    }

    正确写法

    1.Binding in the Constructor

    class Button extends React.Component {
    constructor(props) {
    super(props);
    
    this.handleClick = this.handleClick.bind(this);
    }
    
    handleClick() {
    console.log('clickity');
    }
    
    render() {
    return (
      <button onClick={this.handleClick}/>
    );
    }
    }

    2.Property Initializers

    class Button extends React.Component {
    // Use an arrow function here:
    handleClick = () => {
    console.log('clickity');
    }
    
    render() {
    return (
      <button onClick={this.handleClick}/>
    );
    }
    }
SunXinFei commented 6 years ago

seamless-immutable 与 immutability-helper 与 immutable-js

在这里我说一下关于这三个功能类似的类库的一些区别和易用性

immutable.js

Facebook 工程师 Lee Byron 花费 3 年时间,与 React 同期出现,但没有被默认放到 React 工具集里(React 提供了简化的 Helper)。它内部实现了一套完整的 Persistent Data Structure,还有很多易用的数据类型。像 Collection、List、Map、Set、Record、Seq。有非常全面的map、filter、groupBy、reduce``find函数式操作方法。同时 API 也尽量与 Object 或 Array 类似。 优点:大而全的API,尤其是Immutable.is() 或者 .equals(),直接替换了===判断,直接hash地址判断,效率不言而喻 缺点:引入项目的代价很大,这里包括需要修改redux,工程文件也很大,min文件50kb,关键是所有的循环遍历不能是简单的循环,而是都需要通过它提供的API才可以获得某个子节点。原因如下图,这是通过FromJS转换后的数据结构: image

seamless-immutable

与 Immutable.js 学院派的风格不同,seamless-immutable 并没有实现完整的 Persistent Data Structure,而是使用 Object.defineProperty(因此只能在 IE9 及以上使用)扩展了 JavaScript 的 Array 和 Object 对象来实现,只支持 Array 和 Object 两种数据类型,API 基于与 Array 和 Object 操持不变。代码库非常小,压缩后下载只有 2K。而 Immutable.js 压缩后下载有 16K。 优点:嵌入项目非常的便捷,因为相对与元数据,数据结构没有大的变化,只是添加了属性,相较于helper,该类库支持下面这种数组下标为变量的情况,变量和key的区别就是加不加引号 Immutable.setIn(groups, [groupIndex,"apps",index,"isChecked"],checked); 数据结构如下图 缺点:API相对于immutable.js略单薄一些,seamless-immutable.js 性能低于 immutable.js。使用shallow copy的方式產生新值來回傳,沒有structural sharing和value equality check所帶來的效能好處。数据嵌套层级越深,数据量越大,性能差异越明显。这里需要根据业务特点来做选择,业务没有大批量的深度数据修改需求,易用性比性能更重要。 image

immutability-helper

也是一个小巧轻便的类库,react-dnd里面有使用 优点:类库小巧轻便,不会改变原数据结构,如下图 缺点:语法糖使用起来不是很舒服,而且最关键的是,想更新数组第N个下标,只能传显性的数字,而不能传递变量,原因是源代码中的forEach所有的key时,变量会成为字符串,而不是解析之后的。 image

SunXinFei commented 5 years ago

将immutable.js引入到项目中的改造

  1. 首先是正常项目中的redux,我们定义一个store文件夹,其中index.js内容如下
    
    import { createStore, combineReducers, applyMiddleware } from 'redux';
    import { composeWithDevTools } from 'redux-devtools-extension';
    import Test from './test/reducer';
    import thunk from 'redux-thunk';

let store = createStore( combineReducers({ Test }), {}, composeWithDevTools(applyMiddleware(thunk)) ); export default store;

store文件夹中创建一个test文件夹,其中reducer.js内容如下
```js
import * as templateStore from './action-type';

let defaultState = {
    text: 'Hello world',
};
export default (state = defaultState, action = {}) => {
    return state;
};

调用的地方如下:

import React from 'react'
import { connect } from 'react-redux';

const SourceBox = ({ text }) => {
  return (
    <div>
      {text}
    </div>
  )
}
export default (connect(
  (state) => ({
    text: state.Test.text
  }),
  {}
)(SourceBox))

我们接下来进行改造:

首先是store中的index.js:

import { createStore, applyMiddleware } from 'redux';
import { composeWithDevTools } from 'redux-devtools-extension';
import Test from './test/reducer';
import { combineReducers } from 'redux-immutable';
import thunk from 'redux-thunk';
import Immutable from 'immutable';

let store = createStore(
    combineReducers({ Test }),//注意这里
    Immutable.Map({}),//注意这里
    composeWithDevTools(applyMiddleware(thunk))
);
export default store;

然后是test中的reducer.js

import * as templateStore from './action-type';
import { fromJS } from 'immutable';

let defaultState = fromJS({//注意这里
    text: 'Hello world'
});

export default (state = defaultState, action = {}) => {
    return state;
};

调用的地方也需要改造

import React from 'react'
import { connect } from 'react-redux';

const SourceBox = ({ text }) => {
  return (
    <div>
      {text} 、
    </div>
  )
}
export default (connect(
  (state) => ({
    text: state.getIn(['Test','text']) //注意这里
  }),
  {}
)(SourceBox))

如果项目中用到了prop-types需要验证对象/数组数据类型的话则需要

import propTypes from 'prop-types';
Sider.propTypes = {
  folderPathLocal: propTypes.instanceOf(List),
  folderPathActiveInSider: propTypes.string
};
export default connect(
  (state) => ({
    folderPathLocal: state.get('folderPathLocal'),
    folderPathActiveInSider: state.get('folderPathActiveInSider')
  }),
  {
    addFolderPathForLocal,
    setFolderPathActiveInSider
  }
)(Sider);