React应用优化

随着功能不断增加,不断迭代更新,React应用会越来越臃肿了,性能也将随之下降。本文从打包和运行两个方面着手,谈谈React应用改如何优化。

一、webpack打包优化

1、缓存node_moduels

我公司的项目每次上线部署的时候,虽然说都要要Jenkins上,但项目越来越多,每个项目部署占用时间都很长,导致每次部署完一个环境的所有项目耗费很多时间。

如果将同一项目的node_mudules在每次打包完毕后缓存起来,下次打包前先判断是否与上次node_moduels相同。若相同,则直接使用上次缓存的node_modules,否则才重新安装依赖包。

那该如何实现上面所说的逻辑?

  • 检查packages.jsonmd5
  • 打包完成后以该次packages.jsonmd5值作为文件名,压缩node_modules并缓存到指定位置;
  • 下次打包前,同样先检查当次packages.jsonmd5,若相同直接使用上次的node_moduels;

具体的SHELL如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#!/bin/bash
PKG_SUM=$(md5sum package.json | cut -d\ -f 1)
NPM_TARBALL_CACHE=${HOME}/.cache/ReactCache/npmtarball/reactSPA
NPM_TARBALL=node_modules-${PKG_SUM}.tgz
NPM_TARBALL_MD5SUM=${NPM_TARBALL}.md5sum
[ ! -e ${NPM_TARBALL_CACHE} ] && mkdir -p ${NPM_TARBALL_CACHE}

TARBALL=${NPM_TARBALL_CACHE}/${NPM_TARBALL}
TARBALLMD5SUM=${NPM_TARBALL_CACHE}/${NPM_TARBALL_MD5SUM}
echo "checking node modules "${TARBALL}
if [ ! -f ${TARBALL} ];then
echo "package.json has some changes, reinstall node modules"
rm -rf ${NPM_TARBALL_CACHE}/*
yarn
echo "yarn success"
if [ -d node_modules ];then
echo "tar and caching..."
tar zcf ${TARBALL} node_modules || return 1
md5sum ${TARBALL} > ${TARBALLMD5SUM}
echo "checking current MD5."
md5sum -c ${TARBALLMD5SUM} || rm -f ${TARBALL} ${TARBALLMD5SUM}
echo "install completed, cached"
fi
else
echo "package.json has no changes, clone previous node modules"
if [ -d node_modules ];then
echo "node_modules dir existed "${TARBALL}
else
echo "unpacking..." ${TARBALL}
tar xzf ${TARBALL}
fi

fi
chmod -R a+rwx node_modules

2、加速代码压缩

webpack提供的UglifyJS插件由于采用单线程压缩,速度很慢 ,
webpack-parallel-uglify-plugin插件可以并行运行UglifyJS插件,这可以有效减少构建时间,当然,该插件应用于生产环境而非开发环境,配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13

var ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin');
new ParallelUglifyPlugin({
cacheDir: '.cache/',
uglifyJS:{
output: {
comments: false
},
compress: {
warnings: false
}
}
})

3、HappyPack加速构建

happypack的原理是让loader可以多进程去处理文件,原理如图示:

目前项目中基本只对js和less文件使用HappyPack加速,具体配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

var HappyPack = require('happypack'),
os = require('os'),
happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length });

modules: {
loaders: [
{
test: /\.js|jsx$/,
loader: 'HappyPack/loader?id=jsHappy',
exclude: /node_modules/
}
]
}

plugins: [
new HappyPack({
id: 'jsHappy',
cache: true,
threadPool: happyThreadPool,
loaders: [{
path: 'babel',
query: {
cacheDirectory: '.webpack_cache',
presets: [
'es2015',
'react'
]
}
}]
}),
//如果有单独提取css文件的话
new HappyPack({
id: 'lessHappy',
loaders: ['style','css','less']
})
]

4、DLL& DllReference

针对第三方NPM包,这些包我们并不会修改它,但仍然每次都要在build的过程消耗构建性能,我们可以通过DllPlugin来前置这些包的构建.
我们使用dllplugin把第三方的NPM包生成一个名为 manifest.json 的文件,这个文件是用来让 DLLReferencePlugin 映射到相关的依赖上去的。在文件中引入该dll文件即可。
其原理是通过引用 dllmanifest 文件来把依赖的名称映射到模块的 id 上,之后再在需要的时候通过内置的 __webpack_require__ 函数来 require 他们。

但对于antd这样的按需加载UI库,不能放在dll中,否则会全部打包进去,按需加载就无效了。

具体配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
///dll.entry.js 定义dll的入口
const DLL_ENTRY = {
react: ['react', 'react-dom', 'react-router-dom', 'prop-types'],
echarts: ['echarts'],
vendor: ['mobx', 'mobx-react', 'axios'],
db: ['dexie'],
};
// 定义需要dll 分离的包名
const DLL_CHUNKS_NAME = Object.keys(DLL_ENTRY);
module.exports = { DLL_ENTRY, DLL_CHUNKS_NAME };

//dll.config.js
const path = require('path');
const webpack = require('webpack');
const dllConstants = require('./dll.entry.js');
module.exports = {
entry: dllConstants.DLL_ENTRY,
output: {
filename: '[name].dll.js', // 动态链接库输出的文件名称
path: path.join(__dirname, '../dll'), // 动态链接库输出路径
libraryTarget: 'var', // 链接库(react.dll.js)输出方式 默认'var'形式赋给变量 b
library: '_dll_[name]_[hash]' // 全局变量名称 导出库将被以var的形式赋给这个全局变量 通过这个变量获取到里面模块
},
plugins: [
new webpack.DllPlugin({
// path 指定manifest文件的输出路径
path: path.join(__dirname, '../dll', '[name].manifest.json'),
context: __dirname,
name: '_dll_[name]_[hash]' // 和library 一致,输出的manifest.json中的name值
})
]
};

//dll.utils.js

const path = require('path');
const constants = require('../conf/dll.js');
const webpack = require('webpack');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const HtmlIncludeAssetsPlugin = require('html-webpack-include-assets-plugin');
//创建 dll 的关联包,返回[]
// 当我们需要使用动态链接库时 首先会找到manifest文件 得到name值记录的全局变量名称 然后找到动态链接库文件 进行加载
const createDllReferences = () => {
const dllChunks = constants.DLL_CHUNKS_NAME;
const tmpArr = [];
dllChunks.forEach(item => {
tmpArr.push(
new webpack.DllReferencePlugin({
manifest: require(path.join(__dirname, `../../dll/${item}.manifest.json`)) //)
})
);
});
return tmpArr;
};
//copy dll 的文件到输出目录
const copyDllToAssets = () => {
const dllChunks = constants.DLL_CHUNKS_NAME;
const tmpArr = [];
dllChunks.forEach(item => {
tmpArr.push({ from: `dll/${item}.dll.js`, to: 'dll' });
});
return new CopyWebpackPlugin(tmpArr);
};
//对dll资源添加相对html的路径
const addDllHtmlPath = () => {
const dllChunks = constants.DLL_CHUNKS_NAME;
const tmpArr = [];
dllChunks.forEach(item => {
tmpArr.push(`dll/${item}.dll.js`);
});
return new HtmlIncludeAssetsPlugin({
assets: tmpArr, // 添加的资源相对html的路径
append: false // false 在其他资源的之前添加 true 在其他资源之后添加
});
};
module.exports = { createDllReferences, copyDllToAssets, addDllHtmlPath };

然后在build.config.js中加入dll插件:

1
2
3
dllUtils.copyDllToAssets(),
...dllUtils.createDllReferences(),
dllUtils.addDllHtmlPath(),

5、缓存dll

对于上文所说的,使用dll抽离第三方npm库可以加速打包,但还存在一种情况就是,dll可能很久不会改变,那每次build的时候都要重新生成dll包,要不然每次收到复制到指定目录。

参考node_modules的缓存机制,我们可以将生成的dll包缓存起来,每次检查对象dll.entry.jsmd5值,只要dll的入口定义不变则认为无需生成新的dll包。具体配置就不写了,跟上面的差不多。

6、其他

  • 开启devtool: "#inline-source-map"会增加编译时间
  • DedupePlugin插件可以在打包的时候删除重复或者相似的文件,实际测试中应该是文件级别的重复的文件
  • 减少构建搜索或编译路径
  • 缓存与增量构建:babel-loader可以缓存处理过的模块,对于没有修改过的文件不会再重新编译,cacheDirectory有着2倍以上的速度提升,这对于rebuild 有着非常大的性能提升。

二、React运行优化

1、组件懒加载

其实webpack会把所有的源码打包成一个文件,一个比较小的应用这样没什么问题,但一个庞大而复杂的应用,这样做不仅增加应用的初始加载时间,还造成不必要性能损失。
试想,一个应用分为前后台,普通用户都是在前台页面进行操作,如果打包成bundle,每次都会把后台部分的代码加载回来,想想都觉得不爽。

这里我们使用import的动态加载属性进行代码分割已经动态加载。

  • 创建一个asyncComponent.js文件,内容如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

import React, { Component } from 'react';
const AsyncComponent = importComponent => {
return class extends Component {
constructor(props) {
super(props);
this.state = {
component: null
};
}
componentDidMount() {
importComponent().then(cmp => {
this.setState({ component: cmp.default });
});
}
render() {
const C = this.state.component;
return C ? <C {...this.props} /> : <p>loading...</p>;
}
};
};
export default AsyncComponent;
  • 使用方法:
1
const AsyncPage = AsyncComponent(() => import('./page));
  • 打包后的文件就会自动分割了。

2、不要滥用this.setState

大家都知道如何在React组件中更新组件的状态,但每一次this.setState都会renderrerender,重新计算比较组件的状态,会重渲染整个组件树。
React 应用开发中最常见的某个错误就是对于this.setState函数的使用,我们不应该将render()函数中用不到的状态放置到this.state对象中。

3、MixinHoC

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

1
2
3
4
5
6
7
8
9

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import {pure} from 'recompose';

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

const OptimizedComponent = pure(FooComponent);

## 4、`immutable.js`

- 不可突变:一旦创建,集合就不能在另一个时间点改变。
- 持久性:可以使用原始集合和一个突变来创建新的集合。原始集合在新集合创建后仍然可用。
- 结构共享:新集合尽可能多的使用原始集合的结构来创建,以便将复制操作降至最少从而提升性能。

## 5、其他

- 经常在render中声明函数,尤其是匿名函数及ES6的箭头函数,用来作为回调传递给子节点,这样做会影响性能的。

- 将常用的object/array字面量暂时保存起来,不要在render方法中声明。

×

纯属好玩

扫码支持
扫码打赏,你说多少就多少

打开支付宝扫一扫,即可进行扫码打赏哦

文章目录
  1. 1. 一、webpack打包优化
    1. 1.1. 1、缓存node_moduels
    2. 1.2. 2、加速代码压缩
    3. 1.3. 3、HappyPack加速构建
    4. 1.4. 4、DLL& DllReference
    5. 1.5. 5、缓存dll
    6. 1.6. 6、其他
  2. 2. 二、React运行优化
    1. 2.1. 1、组件懒加载
    2. 2.2. 2、不要滥用this.setState
      1. 2.2.1. 3、Mixin与HoC