React优化React应用优化
7码几盎随着功能不断增加,不断迭代更新,React应用会越来越臃肿了,性能也将随之下降。本文从打包和运行两个方面着手,谈谈React应用改如何优化。
一、webpack打包优化
1、缓存node_moduels
我公司的项目每次上线部署的时候,虽然说都要要Jenkins上,但项目越来越多,每个项目部署占用时间都很长,导致每次部署完一个环境的所有项目耗费很多时间。
如果将同一项目的node_mudules在每次打包完毕后缓存起来,下次打包前先判断是否与上次node_moduels相同。若相同,则直接使用上次缓存的node_modules,否则才重新安装依赖包。
那该如何实现上面所说的逻辑?
- 检查
packages.json的 md5;
- 打包完成后以该次
packages.json的md5值作为文件名,压缩node_modules并缓存到指定位置;
- 下次打包前,同样先检查当次
packages.json的 md5,若相同直接使用上次的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 35
| #!/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}
|
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 38
| 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' ] } }] }), new HappyPack({ id: 'lessHappy', loaders: ['style','css','less'] }) ]
|
4、DLL & DllReference
针对第三方NPM包,这些包我们并不会修改它,但仍然每次都要在build的过程消耗构建性能,我们可以通过DllPlugin来前置这些包的构建.
我们使用dllplugin把第三方的NPM包生成一个名为 manifest.json 的文件,这个文件是用来让 DLLReferencePlugin 映射到相关的依赖上去的。在文件中引入该dll文件即可。
其原理是通过引用 dll 的 manifest 文件来把依赖的名称映射到模块的 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 77
| const DLL_ENTRY = { react: ['react', 'react-dom', 'react-router-dom', 'prop-types'], echarts: ['echarts'], vendor: ['mobx', 'mobx-react', 'axios'], db: ['dexie'], };
const DLL_CHUNKS_NAME = Object.keys(DLL_ENTRY); module.exports = { DLL_ENTRY, DLL_CHUNKS_NAME };
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', library: '_dll_[name]_[hash]' }, plugins: [ new webpack.DllPlugin({ path: path.join(__dirname, '../dll', '[name].manifest.json'), context: __dirname, name: '_dll_[name]_[hash]' }) ] };
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');
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; };
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); };
const addDllHtmlPath = () => { const dllChunks = constants.DLL_CHUNKS_NAME; const tmpArr = []; dllChunks.forEach(item => { tmpArr.push(`dll/${item}.dll.js`); }); return new HtmlIncludeAssetsPlugin({ assets: tmpArr, append: false }); }; module.exports = { createDllReferences, copyDllToAssets, addDllHtmlPath };
|
然后在build.config.js中加入dll插件:
1 2 3 4
| dllUtils.copyDllToAssets(), ...dllUtils.createDllReferences(), dllUtils.addDllHtmlPath(),
|
5、缓存dll
对于上文所说的,使用dll抽离第三方npm库可以加速打包,但还存在一种情况就是,dll可能很久不会改变,那每次build的时候都要重新生成dll包,要不然每次收到复制到指定目录。
参考node_modules的缓存机制,我们可以将生成的dll包缓存起来,每次检查对象dll.entry.js的md5值,只要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 23
| 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 2
| const AsyncPage = AsyncComponent(() => import('./page));
|
2、不要滥用this.setState
大家都知道如何在React组件中更新组件的状态,但每一次this.setState都会render或rerender,重新计算比较组件的状态,会重渲染整个组件树。
React 应用开发中最常见的某个错误就是对于this.setState函数的使用,我们不应该将render()函数中用不到的状态放置到this.state对象中。
3、Mixin与HoC
一个普遍的性能优化做法是,在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 22 23 24
| 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方法中声明。
|