We read every piece of feedback, and take your input very seriously.
To see all available qualifiers, see our documentation.
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
随着前端的发展,在一个前端项目中,框架和构建工具已经成了编配,而webpack显然已经成了最火热的构架工具之一。React,Vue,angularjs2等诸多知名项目也都选用其作为官方构建工具,极受业内追捧,随者工程开发的复杂程度和代码规模不断地增加,webpack暴露出来的各种性能问题也愈发明显,极大的影响着开发过程中的体验。
本文旨在分析 webpack 的性能问题,并提供不同的解决方案。
webpack
在此我们介绍一款 wepback 的可视化资源分析工具:webpack-visualizer,这款工具可以在webpack构建的时候会自动帮你计算出各个模块在你的项目工程中的依赖与分布情况,方便做更精确的资源依赖和引用的分析。
wepback
我们主要针对不同的性能问题提供不同的解决方案。
从上图中我们不难发现大多数的工程项目中,依赖库的体积永远是大头,通常体积可以占据整个工程项目的7-9成,而且在每次开发过程中也会重新读取和编译对应的依赖资源,这其实是很大的的资源开销浪费,而且对编译结果影响微乎其微,毕竟在实际业务开发中,我们很少会去主动修改第三方库中的源码,所以我们需要通过一些方案来抽离代码库。
externals
externals的官方定义是:防止将某些 import 的包(package)打包到 bundle 中,而是在运行时(runtime)再去从外部获取这些扩展依赖(external dependencies)。 例如,从 CDN 引入 react ,而不是把它打包:
import
bundle
react
index.html
... <script src="https://cdn.bootcss.com/react/15.6.1/react.js"></script> ...
webpack.config.js
externals: { react: 'React' }
这样就剥离了那些不需要改动的依赖模块,换句话,下面展示的代码还可以正常运行:
import react from 'react';
简单来说 external 就是把我们的依赖资源声明为一个外部依赖,然后通过 script 外链脚本引入。这也是我们早期页面开发中资源引入的一种翻版,只是通过配置后可以告知 webapck 遇到此类变量名时就可以不用解析和编译至模块的内部文件中,而改用从外部变量中读取,这样能极大的提升编译速度,同时也能更好的利用CDN来实现缓存。
external
script
webapck
DllPlugin
DllReferencePlugin
我们的项目依赖中通常会引用大量的npm包,而这些包在正常的开发过程中并不会进行修改,但是在每一次构建过程中却需要反复的将其解析,如何来规避此类损耗呢?这两个插件就是干这个用的。
简单来说 DllPlugin 的作用是预先编译一些模块,而 DllReferencePlugin 则是把这些预先编译好的模块引用起来。这边需要注意的是 DllPlugin必须要在 DllReferencePlugin 执行前先执行一次, dll 这个概念应该也是借鉴了windows程序开发中的 dll 文件的设计理念。
dll
相较于 externals ,DllPlugin 的主要是:
dllPlugin
(1) 配置 dllPlugin 对应资源表并编译文件
dll.config.js
const webpack = require('webpack'); const path = require('path'); const vendors = [ 'react', 'react-dom', 'react-router' ]; module.exports = { output: { path: __dirname + '/dist', filename: '[name].js', library: '[name]', }, entry: { lib: vendors, }, plugins: [ new webpack.DllPlugin({ path: path.join(__dirname, 'dll', 'manifest.json'), name: '[name]', context: __dirname, }), ], };
然后执行命令:
NODE_ENV=development webpack --config webpack.dll.lib.js
结果会生成一个 manifest.json 文件和一个 lib.js 文件。
manifest.json
lib.js
(2) dllPlugin的静态资源引入
生成了 manifest.json 文件和 lib.js 文件之后,我们还要在我们的配置文件中配置 manifest.json,让 webpack 能够不自动编译这些代码库,配置如下:
plugins: [ new webpack.DllReferencePlugin({ context: __dirname, manifest: require('./dll/manifest.json'), }) ]
注意:如果你有依赖代码库相同的项目,也可以使用同一份 manifest.json 和 lib.js 文件,只需在配置中将manifest.json引入,在 script 标签中引入 lib.js 即可。
当项目的入口很多,但是入口文件存在一些公共代码,对所有依赖的chunk进行公共部分的提取的必要性就会发挥出来。
new webpack.optimize.CommonsChunkPlugin('common.js')
new webpack.optimize.CommonsChunkPlugin('common.js',['entry1','entry2']);
new webpack.optimize.CommonsChunkPlugin({ name: "commons", minChunks: 3 filename: "commons.js" })
entry = { vendors: ['fetch', 'loadash'] }; new webpack.optimize.CommonsChunkPlugin({ name: "vendors", minChunks: Infinity });
现在很多项目都采用单页面开发,特别是一些移动端的网站;但是当网站规模越来越大的时候,首先出现的问题是 Javascript 文件变得巨大,这导致首页渲染的时间让人难以忍受。实际上程序应当只加载当前渲染页所需的 JavaScript,也就是大家说的“代码分拆" — 将所有的代码分拆成多个小包,在用户浏览过程中按需加载。 通过代码分割,我们得到的效果如下:
分割之前的页面
分割之后的效果
可以很清楚的看到,我们将一个大的js文件拆分成了若干个chunk文件。
我们项目的结构如下:
page ├── home │ ├── home.js │ ├── home.scss ├── guide │ ├── guide.js │ ├── guide.scss └── more │ ├── more.js │ └── more.scss └── app.js
按需加载之后,我们需要对Route进行改造,我们将component方法替换成getComponent,让路由去动态的加载组件。 app.js是项目入口,配置如下:
const rootRoute = { indexRoute: { getComponent(nextState, cb) { require.ensure([], (require) => { cb(null, require('./home')) }) } }, getComponent(nextState, cb) { require.ensure([], (require) => { cb(null, require('./index')) }) }, path: '/', childRoutes: [ require('./guide'), require('./more') ] } render(( <Router history={hashHistory} routes={rootRoute} /> ), document.getElementById('app'))
此处有四个属性:
path
将匹配的路由,也就是以前的 path。
getComponent
对应于以前的 component 属性,但是这个方法是异步的,也就是当路由匹配时,才会调用这个方法。 这里面有个 require.ensure 方法
require.ensure
require.ensure(dependencies, callback, chunkName)
这是 webpack 提供的方法,这也是按需加载的核心方法。第一个参数是依赖,第二个是回调函数,第三个就是上面提到的 chunkName,用来指定这个 chunk file 的 name。
如果需要返回多个子组件,则使用 getComponents 方法,将多个组件作为一个对象的属性通过 cb 返回出去即可。这个在官方示例也有,但是我们这里并不需要,而且根组件是不能返回多个子组件的,所以使用 getComponent。
getComponents
indexRoute
indexRoute用来显示默认路由,不需要进行按需加载。
childRoutes
这里面放置的就是子路由的配置,这里的子路由都应该是按需加载的。
我们还需要在子路由中进行配置。
home.js
module.exports = require('./home');
由于home是默认的路由,所以不需要进行按需加载
guide.js
module.exports = { path: '/guide', getComponent(nextState, cb) { require.ensure([], (require) => { cb(null, require('./guide')) }) } }
more.js
module.exports = { path: '/more', getComponent(nextState, cb) { require.ensure([], (require) => { cb(null, require('./more')) }) } }
项目经过webpack打包之后,会生成包含子路由的chunk文件,并且在路由切换的时候进行按需加载。
UglifyJsPlugin 凭借基于node开发,压缩比例高,使用方便等诸多优点已经成为了js压缩工具中的首选,但是我们在webpack的构建中观察发现,当webpack build进度走到80%前后时,会发生很长一段时间的停滞,经测试对比发现这一过程正是 UglifyJsPlugin 在对我们的 output 中的 bunlde 部分进行压缩耗时过长导致,针对这块我们推荐使用webpack-uglify-parallel来提升压缩速度。
UglifyJsPlugin
output
bunlde
webpack-uglify-parallel 的实现原理是采用了多核并行压缩的方式来提升我们的压缩速度。
webpack-uglify-parallel
使用配置也非常简单,只需要将我们原来webpack中自带的 UglifyJsPlugin 配置:
new webpack.optimize.UglifyJsPlugin({ exclude:/\.min\.js$/ mangle:true, compress: { warnings: false }, output: { comments: false } })
修改成如下代码即可:
const os = require('os'); const UglifyJsParallelPlugin = require('webpack-uglify-parallel'); new UglifyJsParallelPlugin({ workers: os.cpus().length, mangle: true, compressor: { warnings: false, drop_console: true, drop_debugger: true } })
happypack 的原理是让loader可以多进程去处理文件,原理如图示:
此外,happypack同时还利用缓存来使得rebuild 更快
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'] }) ]
性能优化无小事,追求快没有止境,在前端工程日益庞大复杂的今天,针对实际项目,持续改进构建工具的性能,对项目开发效率的提升和工具深度理解都是极其有益的。
webpack中文官方博客
react-router 按需加载
webpack中文站
happypack
yumu脚手架
The text was updated successfully, but these errors were encountered:
No branches or pull requests
1、前言
随着前端的发展,在一个前端项目中,框架和构建工具已经成了编配,而webpack显然已经成了最火热的构架工具之一。React,Vue,angularjs2等诸多知名项目也都选用其作为官方构建工具,极受业内追捧,随者工程开发的复杂程度和代码规模不断地增加,webpack暴露出来的各种性能问题也愈发明显,极大的影响着开发过程中的体验。
本文旨在分析
webpack
的性能问题,并提供不同的解决方案。2、性能问题源自何处
在此我们介绍一款
wepback
的可视化资源分析工具:webpack-visualizer,这款工具可以在webpack构建的时候会自动帮你计算出各个模块在你的项目工程中的依赖与分布情况,方便做更精确的资源依赖和引用的分析。3、解决方案
我们主要针对不同的性能问题提供不同的解决方案。
3.1 合理去除对一些代码库的构建
从上图中我们不难发现大多数的工程项目中,依赖库的体积永远是大头,通常体积可以占据整个工程项目的7-9成,而且在每次开发过程中也会重新读取和编译对应的依赖资源,这其实是很大的的资源开销浪费,而且对编译结果影响微乎其微,毕竟在实际业务开发中,我们很少会去主动修改第三方库中的源码,所以我们需要通过一些方案来抽离代码库。
1. 使用
externals
配置来提取常用库externals
的官方定义是:防止将某些import
的包(package)打包到bundle
中,而是在运行时(runtime)再去从外部获取这些扩展依赖(external dependencies)。例如,从 CDN 引入
react
,而不是把它打包:index.html
webpack.config.js
这样就剥离了那些不需要改动的依赖模块,换句话,下面展示的代码还可以正常运行:
简单来说
external
就是把我们的依赖资源声明为一个外部依赖,然后通过script
外链脚本引入。这也是我们早期页面开发中资源引入的一种翻版,只是通过配置后可以告知webapck
遇到此类变量名时就可以不用解析和编译至模块的内部文件中,而改用从外部变量中读取,这样能极大的提升编译速度,同时也能更好的利用CDN来实现缓存。2. 利用
DllPlugin
和DllReferencePlugin
预编译资源模块我们的项目依赖中通常会引用大量的npm包,而这些包在正常的开发过程中并不会进行修改,但是在每一次构建过程中却需要反复的将其解析,如何来规避此类损耗呢?这两个插件就是干这个用的。
简单来说
DllPlugin
的作用是预先编译一些模块,而DllReferencePlugin
则是把这些预先编译好的模块引用起来。这边需要注意的是DllPlugin
必须要在DllReferencePlugin
执行前先执行一次,dll
这个概念应该也是借鉴了windows程序开发中的dll
文件的设计理念。相较于
externals
,DllPlugin
的主要是:externals
的配置项需要对每个依赖库进行逐个定制,所以每次引入新的代码库的时候都需要手动修改外链的引入,并且在CDN上配置该代码库的资源,过程比较繁琐,而通过dllPlugin
则能完全通过配置读取,减少维护的成本。DllPlugin
会将多个代码库抽离成一个js资源,可以减少一些script
标签。(1) 配置
dllPlugin
对应资源表并编译文件dll.config.js
然后执行命令:
结果会生成一个
manifest.json
文件和一个lib.js
文件。manifest.json
记录了webpack中的预编译信息,这样等于提前拿到了依赖库中的chunk信息,在实际开发过程中就无需要进行重复编译。lib.js
就是将配置的代码库编译后生成的文件。(2)
dllPlugin
的静态资源引入生成了
manifest.json
文件和lib.js
文件之后,我们还要在我们的配置文件中配置manifest.json
,让webpack
能够不自动编译这些代码库,配置如下:注意:如果你有依赖代码库相同的项目,也可以使用同一份
manifest.json
和lib.js
文件,只需在配置中将manifest.json
引入,在script
标签中引入lib.js
即可。3.2 多入口项目合理提取出公共代码
当项目的入口很多,但是入口文件存在一些公共代码,对所有依赖的chunk进行公共部分的提取的必要性就会发挥出来。
3.3 单页面应用合理分割代码、按需加载
现在很多项目都采用单页面开发,特别是一些移动端的网站;但是当网站规模越来越大的时候,首先出现的问题是 Javascript 文件变得巨大,这导致首页渲染的时间让人难以忍受。实际上程序应当只加载当前渲染页所需的 JavaScript,也就是大家说的“代码分拆" — 将所有的代码分拆成多个小包,在用户浏览过程中按需加载。
通过代码分割,我们得到的效果如下:
可以很清楚的看到,我们将一个大的js文件拆分成了若干个chunk文件。
我们项目的结构如下:
按需加载之后,我们需要对Route进行改造,我们将component方法替换成getComponent,让路由去动态的加载组件。
app.js是项目入口,配置如下:
此处有四个属性:
path
将匹配的路由,也就是以前的 path。
getComponent
对应于以前的 component 属性,但是这个方法是异步的,也就是当路由匹配时,才会调用这个方法。
这里面有个
require.ensure
方法这是 webpack 提供的方法,这也是按需加载的核心方法。第一个参数是依赖,第二个是回调函数,第三个就是上面提到的 chunkName,用来指定这个 chunk file 的 name。
如果需要返回多个子组件,则使用
getComponents
方法,将多个组件作为一个对象的属性通过 cb 返回出去即可。这个在官方示例也有,但是我们这里并不需要,而且根组件是不能返回多个子组件的,所以使用getComponent
。indexRoute
indexRoute
用来显示默认路由,不需要进行按需加载。childRoutes
这里面放置的就是子路由的配置,这里的子路由都应该是按需加载的。
我们还需要在子路由中进行配置。
home.js
由于home是默认的路由,所以不需要进行按需加载
guide.js
more.js
项目经过webpack打包之后,会生成包含子路由的chunk文件,并且在路由切换的时候进行按需加载。
3.4 加快代码压缩速度
UglifyJsPlugin
凭借基于node开发,压缩比例高,使用方便等诸多优点已经成为了js压缩工具中的首选,但是我们在webpack的构建中观察发现,当webpack build进度走到80%前后时,会发生很长一段时间的停滞,经测试对比发现这一过程正是UglifyJsPlugin
在对我们的output
中的bunlde
部分进行压缩耗时过长导致,针对这块我们推荐使用webpack-uglify-parallel来提升压缩速度。webpack-uglify-parallel
的实现原理是采用了多核并行压缩的方式来提升我们的压缩速度。使用配置也非常简单,只需要将我们原来webpack中自带的
UglifyJsPlugin
配置:修改成如下代码即可:
3.5 让loader多进程地去处理文件
happypack 的原理是让loader可以多进程去处理文件,原理如图示:
此外,happypack同时还利用缓存来使得rebuild 更快
4、结尾
性能优化无小事,追求快没有止境,在前端工程日益庞大复杂的今天,针对实际项目,持续改进构建工具的性能,对项目开发效率的提升和工具深度理解都是极其有益的。
5、参考文章
The text was updated successfully, but these errors were encountered: