只有10%~20%的最终用户响应时间花在了下载HTML文档上,其余的80%~90%时间花在了下载页面中的 所有组件 上。 - 性能黄金法则,Steve Souders
Steve Souders在2007年提出这样的“性能黄金法则”,我猜测当他看到React这样一项技术之后,一定会觉得自己的这个法则居然如此的准确,可能甚至觉得这个比例不够极致。(虽然此组件非React组件,但是我还是忍不住想笑)
所以,今天我们就来聊聊,React应用在产品环境下的性能优化问题。
Bundle大小分析
在开始做任何的优化之前,你需要知道痛点在什么地方?既然Steve说80%~90%时间花在了下载页面中的 所有组件 上,那么就从了解项目的模块组成开始。
1.Webpack运行时的输出
在没有任何外部力量帮助的情况下,我们可以直接阅读Webpack的输出
1
webpack --display-chunks
可以查看模块在哪个分块中出现,帮助我们查找分块中重复的依赖。
2.bundle-size-analyzer
webpack-bundle-size-analyzer 是我个人比较喜欢的模块大小分析工具,使用起来非常简单,输出也非常清晰。
3.webpack-bundle-analyzer
webpack-bundle-analyzer 在github上star人数更多,功能也相对更加齐全(fancy)。
代码分离(Code Splitting)
1.Vendor代码分离
代码分离是Webpack核心功能之一,典型的做法是将第三方依赖代码从应用代码中抽离出来,这样可以利用浏览器的缓存来提高性能(减少下载次数)。
Webpack官方文档有非常详细的介绍: code-splitting-libraries ,我就不在这里赘述,下面是一个简单代码样例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var webpack = require ( 'webpack' );
var path = require ( 'path' );
module . exports = function ( env ) {
return {
entry : {
main : './index.js' ,
vendor : 'react' , 'react-dom' , 'react-redux' , 'babel-polyfill' ]
},
output : {
filename : '[name].[chunkhash].js' ,
path : path . resolve ( __dirname , 'dist' )
},
plugins : [
new webpack . optimize . CommonsChunkPlugin ({
name : 'vendor' // Specify the common bundle's name.
})
]
}
}
2.CSS代码
也许你还会想要的就是将CSS文件分离,原因是一样的。官方文档也给出了非常详细的介绍:code-splitting-css ,所以同样也不赘述了。
1
2
3
4
5
6
7
8
9
10
11
12
const extractCSS = new ExtractTextPlugin ( 'styles.css' );
module : {
rules : [
{
test : /\.scss$/ ,
use : extractCSS . extract ([ 'css-loader' , 'postcss-loader' , 'sass-loader' ])
}
]
},
plugins : [
extractCSS
]
3.React-Router按需分离
当应用逐渐变得复杂后,你会发现,仅仅将代码分离为vendor和app两个bundle,远远是不够的,要么vendor.js文件特别大,要么app.js文件特别大,这个时候你一定会想到,要按需加载(异步加载)。
ES2015 Loader spec中定义了一个import()方法来在运行时动态加载ES2015的模块,代码如下:
1
2
3
4
5
6
7
8
function determineDate () {
import ( 'moment' ). then ( function ( moment ) {
console . log ( moment (). format ());
}). catch ( function ( err ) {
console . log ( 'Failed to load moment' , err );
});
}
determineDate ();
Webpack会将import()方法看做一个“代码分离点”,将被加载的模块放在一个单独的文件块中。
那么,如果你的应用采用了React-Router,我们就可以根据路由,按需加载所使用的组件。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function errorLoading ( error ) {
throw new Error ( ` Dynamic page loading failed : $ { error } ` );
}
function loadRoute ( cb ) {
return module => cb ( null , module . default );
}
< Router history = { history } queryKey = "false" >
< Route path = "/user" name = "UserPage" getComponent = {( location , cb ) => {
System . import ( './components/UserPage' ). then ( loadRoute ( cb , false )). catch ( errorLoading )}}
/>
< Route path = "/data" name = "DataPage" getComponent = {( location , cb ) => {
System . import ( './components/DataPage' ). then ( loadRoute ( cb , false )). catch ( errorLoading )}}
/>
< Route path = "/about" name = "AboutPage" getComponent = {( location , cb ) => {
System . import ( './components/AboutPage' ). then ( loadRoute ( cb , false )). catch ( errorLoading )}}
/>
< /Router>
Webpack会根据分离点生成对应的JS文件
4.大文件异步加载
笔者遇到过这样的需求,采用了某图表库来做地图绘制,但是地图库JS文件或者JSON文件特别的大,即便压缩之后也有400+KB。所以,我将该依赖放在应用(SPA)的首页,并采用了异步加载,这样,首屏加载速度不会依赖于它,而用户从首页到需要使用该地图的部分还存在一些操作过程,所以留存了一些时间来异步加载地图数据。
1
2
System . import ( './map/china.js' ). then (). catch ( errorLoading );
System . import ( './map/world.js' ). then (). catch ( errorLoading );
当首页需要的JS加载完成之后,才开始加载:
运行webpack -p
Webpack的官方文档有详细的说明,对于产品环境的构建应该运行webpack -p:production-build 。
1
webpack -- optimize - minimize -- define process . env . NODE_ENV = "'production'"
此时,Webpack会做几件事情,我们也需要根据这些事情做相关的配置:
1.对JS代码进行压缩
–optimize-minimize 标签会对JS代码用UglifyJsPlugin做压缩,并根据Webpack中配置的devtool配置SourceMap
2.SourceMap
即便在产品环境下,仍然建议使用SourceMap,方便产品环境的bug定位,但是对于开发环境和产品环境,我们需要使用不同力度的SourceMap,才能既方便开发也兼容产品环境性能。
1
2
3
4
const config = {
// webpack config
devtool : isProd ? 'cheap-source-map' : 'cheap-module-inline-source-map' ,
}
官方提供了7种Devtool,而且有更详细的关于devtool的配置,请详见 devtool 。
3.Node环境变量production
将redux的中间件和开发环境使用的devtool通过变量分离
–define process.env.NODE_ENV=“‘production’” 标签会以下面的方式使用DefinePlugin:
1
2
3
4
5
6
7
8
9
const webpack = require ( 'webpack' );
module . exports = {
/*...*/
plugins : [
new webpack . DefinePlugin ({
'process.env.NODE_ENV' : JSON . stringify ( 'production' )
})
]
};
这个时候,就可以在产品代码里面获取到此环境变量。这个时候我们要做的就是根据环境变量的不同,来进行不同的配置,比如:这样写log
1
if ( process . env . NODE_ENV !== 'production' ) console . log ( '...' )
对于产品环境就等价于:
1
if ( false ) console . log ( '...' )
此时UglifyJS插件就会将它去除掉。
又比如:在react-redux开发中,我们一般都会配置开发插件DevTool,或者log中间件,但其实,在产品环境中,我们不需要,这个时候就需要根据环境变量来动态配置:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { applyMiddleware , compose , createStore } from 'redux' ;
import thunkMiddleware from 'redux-thunk' ;
import { createLogger } from 'redux-logger' ;
import rootReducer from './reducers' ;
import promiseMiddleware from 'redux-promise-middleware' ;
export default () => {
let finalCreateStore ;
if ( process . env . NODE_ENV === 'production' ) {
finalCreateStore = compose (
applyMiddleware ( promiseMiddleware (), thunkMiddleware )
)( createStore );
} else {
finalCreateStore = compose (
applyMiddleware ( promiseMiddleware (), thunkMiddleware , createLogger ()),
window . devToolsExtension ? window . devToolsExtension () : f => f
)( createStore );
}
return finalCreateStore ( rootReducer );
};
Tree Shaking
Webpack TreeShake
清理无用的JS代码,真实导入有用的模块。配置起来非常简单:
1
"presets" : [[ "es2015" , { "modules" : false }], "react" , "stage-0" ]
Babel对React代码的优化
除了从产品环境模块架构上优化,Babel也在编译阶段优化React应用性能作出了巨大贡献。
1.transform-react-constant-elements插件
transform-react-constant-elements ,自从React0.14版本,我们可以将React元素以及他们的属性对象当做普通的值对象看待。这时候我们就可以重用那些输入是immutable的React元素。
1
2
3
const Hr = () => {
return < hr className = "hr" /> ;
};
1
2
3
4
5
const _ref = < hr className = "hr" /> ;
const Hr = () => {
return _ref ;
};
从而减少对React.createElement的调用。
2.transform-react-inline-elements插件
transform-react-inline-elements ,自从React0.14版本,可以将React元素内联为对象,Babel将React.createElement方法替换成babelHelpers.jsx来转换元素为对象。
1
< Baz foo = "bar" key = "1" >< /Baz>;
1
2
3
4
5
6
babelHelpers . jsx ( Baz , {
foo : "bar"
}, "1" );
output :
{ type : Baz , props : { foo : "bar" }, key : "1" }
3.其他开源Babel插件
除了以上两个官方插件,在开源世界还有许多其他Babel插件可以优化代码,而且非常实用,这里留给大家自己去探索: transform-react-remove-prop-types , transform-react-pure-class-to-function , babel-react-optimize (综合所有优化的插件,此处应该有掌声)
代码本身的优化
除了利用工具和构建,以及模块按需加载,来提高产品环境下的代码性能,最最基础的还是开发在平时写代码的需要注意的一些基础原则
1.只导入需要的包
以Lodash为例,比如:如果你只用到isEqual,那么就不要把整个lodash都引入
1
import isEqual from 'lodash/isEqual' ;
通过babel-plugin-lodash 和lodash-webpack-plugin 来缩小应用所需要的lodash的模块
用高阶函数Flow来代替Chain ,以避免将整个ladash都加载。
2.使用ESLint
合理的使用ESLint,除了帮助团队指导代码风格,也可以告诉你如何正确的写React应,比如,当组件是纯presentational组件时,就应该使用PureComponent或者纯函数组件,这些eslint都会告诉你。
3.利用React官方的Perf工具
1
2
3
Perf . start ()
// ...
Perf . stop ()
服务器端优化
使用Gzip压缩倒不是React应用才有的性能优化策略,但还是要提一下,因为确实有用。
1.Nginx服务器端配置
我猜测大部分的情况下,都会用Nginx来部署静态资源,斗胆提供一个nginx的gzip配置。
1
2
3
4
gzip on ; //仅仅配置这一行是不会起作用的
gzip_types text / plain application / javascript application / x - javascript text / javascript text / xml text / css ;
gzip_proxied no - cache no - store private expired auth ;
gzip_min_length 1000 ;
2.手动压缩
另外一种方式,就是我们自己手动压缩Gzip,这样可以减少Nginx编码带来的性能消耗,Webpack插件
compression-webpack-plugin 可以做到。
还有什么别的提高性能的办法呢?
1.服务器端渲染如何
有人会说,服务器端渲染如何? 这个要看情况。服务器端渲染一般主要用来处理首屏渲染性能(注意是首次加载)和搜索引擎爬虫问题。如果你的JS文件特别大,那么服务器端渲染能够,让用户在加载完HTML和CSS之后立刻看到页面。如果不是首次加载,那么其实JS是可以缓存在客户端的,所以即便不用服务器端渲染,之后也不会很慢。
相对的缺点是:配置起来比较麻烦,但如果是一劳永逸的事情,还是值得一做的。
更多关于是否应该进行服务器端渲染 ,服务器端渲染的好处 以及如何进行服务器端渲染 ?请查看相关文章。
2.ServiceWork
渐进式 Web 应用程序思想(PWA)最近可火了,2017年ThoughtWorks技术雷达 将“Progressive Web Applications” 放在了试验阶段。
简单介绍什么是service worker:
在2014年,W3C公布了service worker的草案,service worker提供了很多新的能力,使得web app拥有与native app相同的离线体验、消息推送体验。
service worker是一段脚本,与web worker一样,也是在后台运行。
作为一个独立的线程,运行环境与普通脚本不同,所以不能直接参与web交互行为。native app可以做到离线使用、消息推送、后台自动更新,service worker的出现是正是为了使得web app也可以具有类似的能力。
Github: Google sw-toolbox , sw-precache-webpack-plugin 和offline-plugin
3.Preload
Preload 作为一个新的web标准,旨在提高性能和为web开发人员提供更细粒度的加载控制。Preload使开发者能够自定义资源的加载逻辑,且无需忍受基于脚本的资源加载器带来的性能损失。
作为新的标准,浏览器兼容性是你有必要考虑的一个方面:
github: preload-webpack-plugin
最后
文章内容有点长,但我相信这些都是干货是值得一读的,前端产品环境性能优化确实是一个说不完的话题,前端技术更新迭代也没有多少其他计算机技术能够匹敌的,这也对前端开发工程师(全栈开发工程师)的技术敏感度和追求新技术的态度有很高的要求。
作者:Benwei,ThoughtWorks高级咨询师,全栈开发工程师,《实战Gradle》译者
转载原文地址: http://benweizhu.github.io/blog/2017/05/12/react-redux-production-optimisation/
参考文献:
1.https://hackernoon.com/optimising-your-application-bundle-size-with-webpack-e85b00bab579
2.https://brotzky.co/blog/code-splitting-react-router-webpack-2/
3.http://www.jianshu.com/p/f4054b2dcc6e
4.http://2ality.com/2015/12/webpack-tree-shaking.html
5.https://hackernoon.com/how-i-built-a-super-fast-uber-clone-for-mobile-web-863680d2100f
6.http://andrewhfarmer.com/server-side-render/