浅谈webpack 构建性能优化策略小结


Posted in Javascript onJune 13, 2018

背景

如今前端工程化的概念早已经深入人心,选择一款合适的编译和资源管理工具已经成为了所有前端工程中的标配,而在诸多的构建工具中,webpack以其丰富的功能和灵活的配置而深受业内吹捧,逐步取代了grunt和gulp成为大多数前端工程实践中的首选,React,Vue,Angular等诸多知名项目也都相继选用其作为官方构建工具,极受业内追捧。但是,随者工程开发的复杂程度和代码规模不断地增加,webpack暴露出来的各种性能问题也愈发明显,极大的影响着开发过程中的体验。

浅谈webpack 构建性能优化策略小结

问题归纳

历经了多个web项目的实战检验,我们对webapck在构建中逐步暴露出来的性能问题归纳主要有如下几个方面:

  • 代码全量构建速度过慢,即使是很小的改动,也要等待长时间才能查看到更新与编译后的结果(引入HMR热更新后有明显改进);
  • 随着项目业务的复杂度增加,工程模块的体积也会急剧增大,构建后的模块通常要以M为单位计算;
  • 多个项目之间共用基础资源存在重复打包,基础库代码复用率不高;
  • node的单进程实现在耗cpu计算型loader中表现不佳;

针对以上的问题,我们来看看怎样利用webpack现有的一些机制和第三方扩展插件来逐个击破。

慢在何处

作为工程师,我们一直鼓励要理性思考,用数据和事实说话,“我觉得很慢”,“太卡了”,“太大了”之类的表述难免显得太笼统和太抽象,那么我们不妨从如下几个方面来着手进行分析:

浅谈webpack 构建性能优化策略小结

  • 从项目结构着手,代码组织是否合理,依赖使用是否合理;
  • 从webpack自身提供的优化手段着手,看看哪些api未做优化配置;
  • 从webpack自身的不足着手,做有针对性的扩展优化,进一步提升效率;

在这里我们推荐使用一个wepback的可视化资源分析工具:webpack-bundle-analyzer,在webpack构建的时候会自动帮你计算出各个模块在你的项目工程中的依赖与分布情况,方便做更精确的资源依赖和引用的分析。

从上图中我们不难发现大多数的工程项目中,依赖库的体积永远是大头,通常体积可以占据整个工程项目的7-9成,而且在每次开发过程中也会重新读取和编译对应的依赖资源,这其实是很大的的资源开销浪费,而且对编译结果影响微乎其微,毕竟在实际业务开发中,我们很少会去主动修改第三方库中的源码,改进方案如下:

方案一、合理配置 CommonsChunkPlugin

webpack的资源入口通常是以entry为单元进行编译提取,那么当多entry共存的时候,CommonsChunkPlugin的作用就会发挥出来,对所有依赖的chunk进行公共部分的提取,但是在这里可能很多人会误认为抽取公共部分指的是能抽取某个代码片段,其实并非如此,它是以module为单位进行提取。

假设我们的页面中存在entry1,entry2,entry3三个入口,这些入口中可能都会引用如utils,loadash,fetch等这些通用模块,那么就可以考虑对这部分的共用部分机提取。通常提取方式有如下四种实现:

1、传入字符串参数,由chunkplugin自动计算提取

new webpack.optimize.CommonsChunkPlugin('common.js')

这种做法默认会把所有入口节点的公共代码提取出来, 生成一个common.js

2、有选择的提取公共代码

new webpack.optimize.CommonsChunkPlugin('common.js',['entry1','entry2']);

只提取entry1节点和entry2中的共用部分模块, 生成一个common.js

3、将entry下所有的模块的公共部分(可指定引用次数)提取到一个通用的chunk中

new webpack.optimize.CommonsChunkPlugin({
  name: 'vendors',
  minChunks: function (module, count) {
    return (
     module.resource &&
     /\.js$/.test(module.resource) &&
     module.resource.indexOf(
      path.join(__dirname, '../node_modules')
     ) === 0
    )
  }
});

提取所有node_modules中的模块至vendors中,也可以指定minChunks中的最小引用数;

4、抽取enry中的一些lib抽取到vendors中

entry = {
  vendors: ['fetch', 'loadash']
};
new webpack.optimize.CommonsChunkPlugin({
  name: "vendors",
  minChunks: Infinity
});

添加一个entry名叫为vendors,并把vendors设置为所需要的资源库,CommonsChunk会自动提取指定库至vendors中。

方案二、通过 externals 配置来提取常用库

在实际项目开发过程中,我们并不需要实时调试各种库的源码,这时候就可以考虑使用external选项了。

浅谈webpack 构建性能优化策略小结

简单来说external就是把我们的依赖资源声明为一个外部依赖,然后通过script外链脚本引入。这也是我们早期页面开发中资源引入的一种翻版,只是通过配置后可以告知webapck遇到此类变量名时就可以不用解析和编译至模块的内部文件中,而改用从外部变量中读取,这样能极大的提升编译速度,同时也能更好的利用CDN来实现缓存。

external的配置相对比较简单,只需要完成如下三步:

1、在页面中加入需要引入的lib地址,如下:

<head>
<script src="//cdn.bootcss.com/jquery.min.js"></script>
<script src="//cdn.bootcss.com/underscore.min.js"></script>
<script src="/static/common/react.min.js"></script>
<script src="/static/common/react-dom.js"></script>
<script src="/static/common/react-router.js"></script>
<script src="/static/common/immutable.js"></script>
</head>

2、在webapck.config.js中加入external配置项:

module.export = {
  externals: {
    'react-router': {
      amd: 'react-router',
      root: 'ReactRouter',
      commonjs: 'react-router',
      commonjs2: 'react-router'
    },
    react: {
      amd: 'react',
      root: 'React',
      commonjs: 'react',
      commonjs2: 'react'
    },
    'react-dom': {
      amd: 'react-dom',
      root: 'ReactDOM',
      commonjs: 'react-dom',
      commonjs2: 'react-dom'
    }
  }
}

这里要提到的一个细节是:此类文件在配置前,构建这些资源包时需要采用amd/commonjs/cmd相关的模块化进行兼容封装,即打包好的库已经是umd模式包装过的,如在node_modules/react-router中我们可以看到umd/ReactRouter.js之类的文件,只有这样webpack中的require和import * from 'xxxx'才能正确读到该类包的引用,在这类js的头部一般也能看到如下字样:

if (typeof exports === 'object' && typeof module === 'object') {
  module.exports = factory(require("react"));
} else if (typeof define === 'function' && define.amd) {
  define(["react"], factory);
} else if (typeof exports === 'object') {
  exports["ReactRouter"] = factory(require("react"));
} else {
  root["ReactRouter"] = factory(root["React"]);
}

3、非常重要的是一定要在output选项中加入如下一句话:

output: {
 libraryTarget: 'umd'
}

由于通过external提取过的js模块是不会被记录到webapck的chunk信息中,通过libraryTarget可告知我们构建出来的业务模块,当读到了externals中的key时,需要以umd的方式去获取资源名,否则会有出现找不到module的情况。

通过配置后,我们可以看到对应的资源信息已经可以在浏览器的source map中读到了。

浅谈webpack 构建性能优化策略小结

对应的资源也可以直接由页面外链载入,有效地减小了资源包的体积。

浅谈webpack 构建性能优化策略小结

方案三、利用 DllPlugin 和 DllReferencePlugin 预编译资源模块

我们的项目依赖中通常会引用大量的npm包,而这些包在正常的开发过程中并不会进行修改,但是在每一次构建过程中却需要反复的将其解析,如何来规避此类损耗呢?这两个插件就是干这个用的。

简单来说DllPlugin的作用是预先编译一些模块,而DllReferencePlugin则是把这些预先编译好的模块引用起来。这边需要注意的是DllPlugin必须要在DllReferencePlugin执行前先执行一次,dll这个概念应该也是借鉴了windows程序开发中的dll文件的设计理念。

相对于externals,dllPlugin有如下几点优势:

  1. dll预编译出来的模块可以作为静态资源链接库可被重复使用,尤其适合多个项目之间的资源共享,如同一个站点pc和手机版等;
  2. dll资源能有效地解决资源循环依赖的问题,部分依赖库如:react-addons-css-transition-group这种原先从react核心库中抽取的资源包,整个代码只有一句话:
module.exports = require('react/lib/ReactCSSTransitionGroup');

却因为重新指向了react/lib中,这也会导致在通过externals引入的资源只能识别react,寻址解析react/lib则会出现无法被正确索引的情况。

由于externals的配置项需要对每个依赖库进行逐个定制,所以每次增加一个组件都需要手动修改,略微繁琐,而通过dllPlugin则能完全通过配置读取,减少维护的成本;

1、配置dllPlugin对应资源表并编译文件

那么externals该如何使用呢,其实只需要增加一个配置文件:webpack.dll.config.js:

const webpack = require('webpack');
const path = require('path');
const isDebug = process.env.NODE_ENV === 'development';
const outputPath = isDebug ? path.join(__dirname, '../common/debug') : path.join(__dirname, '../common/dist');
const fileName = '[name].js';

// 资源依赖包,提前编译
const lib = [
 'react',
 'react-dom',
 'react-router',
 'history',
 'react-addons-pure-render-mixin',
 'react-addons-css-transition-group',
 'redux',
 'react-redux',
 'react-router-redux',
 'redux-actions',
 'redux-thunk',
 'immutable',
 'whatwg-fetch',
 'byted-people-react-select',
 'byted-people-reqwest'
];

const plugin = [
 new webpack.DllPlugin({
  /**
   * path
   * 定义 manifest 文件生成的位置
   * [name]的部分由entry的名字替换
   */
  path: path.join(outputPath, 'manifest.json'),
  /**
   * name
   * dll bundle 输出到那个全局变量上
   * 和 output.library 一样即可。
   */
  name: '[name]',
  context: __dirname
 }),
 new webpack.optimize.OccurenceOrderPlugin()
];

if (!isDebug) {
 plugin.push(
  new webpack.DefinePlugin({
   'process.env.NODE_ENV': JSON.stringify('production')
  }),
  new webpack.optimize.UglifyJsPlugin({
   mangle: {
    except: ['$', 'exports', 'require']
   },
   compress: { warnings: false },
   output: { comments: false }
  })
 )
}

module.exports = {
 devtool: '#source-map',
 entry: {
  lib: lib
 },
 output: {
  path: outputPath,
  filename: fileName,
  /**
   * output.library
   * 将会定义为 window.${output.library}
   * 在这次的例子中,将会定义为`window.vendor_library`
   */
  library: '[name]',
  libraryTarget: 'umd',
  umdNamedDefine: true
 },
 plugins: plugin
};

然后执行命令:

$ NODE_ENV=development webpack --config webpack.dll.lib.js --progress
$ NODE_ENV=production webpack --config webpack.dll.lib.js --progress

即可分别编译出支持调试版和生产环境中lib静态资源库,在构建出来的文件中我们也可以看到会自动生成如下资源:

common
├── debug
│  ├── lib.js
│  ├── lib.js.map
│  └── manifest.json
└── dist
  ├── lib.js
  ├── lib.js.map
  └── manifest.json

文件说明:

  1. lib.js可以作为编译好的静态资源文件直接在页面中通过src链接引入,与externals的资源引入方式一样,生产与开发环境可以通过类似charles之类的代理转发工具来做路由替换;
  2. manifest.json中保存了webpack中的预编译信息,这样等于提前拿到了依赖库中的chunk信息,在实际开发过程中就无需要进行重复编译;

2、dllPlugin的静态资源引入

lib.js和manifest.json存在一一对应的关系,所以我们在调用的过程也许遵循这个原则,如当前处于开发阶段,对应我们可以引入common/debug文件夹下的lib.js和manifest.json,切换到生产环境的时候则需要引入common/dist下的资源进行对应操作,这里考虑到手动切换和维护的成本,我们推荐使用add-asset-html-webpack-plugin进行依赖资源的注入,可得到如下结果:

<head>
<script src="/static/common/lib.js"></script>
</head>

在webpack.config.js文件中增加如下代码:

const isDebug = (process.env.NODE_ENV === 'development');
const libPath = isDebug ? '../dll/lib/manifest.json' : 
'../dll/dist/lib/manifest.json';

// 将mainfest.json添加到webpack的构建中

module.export = {
 plugins: [
    new webpack.DllReferencePlugin({
    context: __dirname,
    manifest: require(libPath),
   })
 ]
}

配置完成后我们能发现对应的资源包已经完成了纯业务模块的提取

浅谈webpack 构建性能优化策略小结

多个工程之间如果需要使用共同的lib资源,也只需要引入对应的lib.js和manifest.js即可,plugin配置中也支持多个webpack.DllReferencePlugin同时引入使用,如下:

module.export = {
 plugins: [
   new webpack.DllReferencePlugin({
    context: __dirname,
    manifest: require(libPath),
   }),
   new webpack.DllReferencePlugin({
    context: __dirname,
    manifest: require(ChartsPath),
   })
 ]
}

方案四、使用 Happypack 加速你的代码构建

以上介绍均为针对webpack中的chunk计算和编译内容的优化与改进,对资源的实际体积改进上也较为明显,那么除此之外,我们能否针对资源的编译过程和速度优化上做些尝试呢?

众所周知,webpack中为了方便各种资源和类型的加载,设计了以loader加载器的形式读取资源,但是受限于node的编程模型影响,所有的loader虽然以async的形式来并发调用,但是还是运行在单个 node的进程以及在同一个事件循环中,这就直接导致了当我们需要同时读取多个loader文件资源时,比如babel-loader需要transform各种jsx,es6的资源文件。在这种同步计算同时需要大量耗费cpu运算的过程中,node的单进程模型就无优势了,那么happypack就针对解决此类问题而生。

开启happypack的线程池

happypack的处理思路是将原有的webpack对loader的执行过程从单一进程的形式扩展多进程模式,原本的流程保持不变,这样可以在不修改原有配置的基础上来完成对编译过程的优化,具体配置如下:

const HappyPack = require('happypack');
 const os = require('os')
 const HappyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length}); // 启动线程池});

module:{
  rules: [
   {
    test: /\.(js|jsx)$/,
    // use: ['babel-loader?cacheDirectory'],
    use: 'happypack/loader?id=jsx',
    exclude: /^node_modules$/
   }
  ]
 },
 plugins:[
  new HappyPack({
   id: 'jsx',
   cache: true,
   threadPool: HappyThreadPool,
   loaders: ['babel-loader']
  })
 ]

我们可以看到通过在loader中配置直接指向happypack提供的loader,对于文件实际匹配的处理 loader,则是通过配置在plugin属性来传递说明,这里happypack提供的loader与plugin的衔接匹配,则是通过id=happybabel来完成。配置完成后,laoder的工作模式就转变成了如下所示:

浅谈webpack 构建性能优化策略小结

happypack在编译过程中除了利用多进程的模式加速编译,还同时开启了cache计算,能充分利用缓存读取构建文件,对构建的速度提升也是非常明显的,经过测试,最终的构建速度提升如下:

优化前:

浅谈webpack 构建性能优化策略小结

优化后:

浅谈webpack 构建性能优化策略小结

关于happyoack的更多介绍可以查看:

happypack

方案五、增强 uglifyPlugin

uglifyJS凭借基于node开发,压缩比例高,使用方便等诸多优点已经成为了js压缩工具中的首选,但是我们在webpack的构建中观察发现,当webpack build进度走到80%前后时,会发生很长一段时间的停滞,经测试对比发现这一过程正是uglfiyJS在对我们的output中的bunlde部分进行压缩耗时过长导致,针对这块我们可以使用webpack-uglify-parallel来提升压缩速度。

从插件源码中可以看到,webpack-uglify-parallel的是实现原理是采用了多核并行压缩的方式来提升我们的压缩速度。

plugin.nextWorker().send({
  input: input,
  inputSourceMap: inputSourceMap,
  file: file,
  options: options
});

plugin._queue_len++;
        
if (!plugin._queue_len) {
  callback();
}        

if (this.workers.length < this.maxWorkers) {
  var worker = fork(__dirname + '/lib/worker');
  worker.on('message', this.onWorkerMessage.bind(this));
  worker.on('error', this.onWorkerError.bind(this));
  this.workers.push(worker);
}

this._next_worker++;
return this.workers[this._next_worker % this.maxWorkers];

使用配置也非常简单,只需要将我们原来webpack中自带的uglifyPlugin配置:

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
    }
  })

目前webpack官方也维护了一个支持多核压缩的UglifyJs插件:uglifyjs-webpack-plugin,使用方式类似,优势在于完全兼容webpack.optimize.UglifyJsPlugin中的配置,可以通过uglifyOptions写入,因此也做为推荐使用,参考配置如下:

const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
 new UglifyJsPlugin({
  uglifyOptions: {
   ie8: false,
   ecma: 8,
   mangle: true,
   output: { comments: false },
   compress: { warnings: false }
  },
  sourceMap: false,
  cache: true,
  parallel: os.cpus().length * 2
 })

方案六、Tree-shaking & Scope Hoisting

wepback在2.X和3.X中从rolluo中借鉴了tree-shaking和Scope Hoisting,利用es6的module特性,利用AST对所有引用的模块和方法做了静态分析,从而能有效地剔除项目中的没有引用到的方法,并将相关方法调用归纳到了独立的webpack_module中,对打包构建的体积优化也较为明显,但是前提是所有的模块写法必须使用ES6 Module进行实现,具体配置参考如下:

// .babelrc: 通过配置减少没有引用到的方法
 {
  "presets": [
   ["env", {
    "targets": {
     "browsers": ["last 2 versions", "safari >= 7"]
    }
   }],
   // https://www.zhihu.com/question/41922432
   ["es2015", {"modules": false}] // tree-shaking
  ]
 }

 // webpack.config: Scope Hoisting
 {
  plugins:[
   // https://zhuanlan.zhihu.com/p/27980441
   new webpack.optimize.ModuleConcatenationPlugin()
  ]
 }

适用场景

在实际的开发过程中,可灵活地选择适合自身业务场景的优化手段。

优化手段 开发环境 生产环境
CommonsChunk
externals  
DllPlugin
Happypack  
uglify-parallel  

工程演示demo

温馨提醒

本文中的所有例子已经重新优化,支持最新的webpack3特性,并附带有分享ppt地址,可以在线点击查看

小结

性能优化无小事,追求快没有止境,在前端工程日益庞大复杂的今天,针对实际项目,持续改进构建工具的性能,对项目开发效率的提升和工具深度理解都是极其有益的。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
写入cookie的JavaScript代码库 cookieLibrary.js
Oct 24 Javascript
javascript getElementsByClassName实现代码
Oct 11 Javascript
原生JS操作网页给p元素添加onclick事件及表格隔行变色
Dec 01 Javascript
js QQ客服悬浮效果实现代码
Dec 12 Javascript
seajs加载jquery时提示$ is not a function该怎么解决
Oct 23 Javascript
JS实现弹出居中的模式窗口示例
Jun 20 Javascript
AngularJS 入门教程之HTML DOM实例详解
Jul 28 Javascript
JS实现获取当前URL和来源URL的方法
Aug 24 Javascript
easyui取消表单实时验证,提交时统一验证的简单实例
Nov 07 Javascript
js时间戳格式化成日期格式的多种方法介绍
Feb 16 Javascript
Three.js利用orbit controls插件(轨道控制)控制模型交互动作详解
Sep 25 Javascript
Javascript 编码约定(编码规范)
Mar 11 Javascript
详解webpack运行Babel教程
Jun 13 #Javascript
Babel 入门教程学习笔记
Jun 13 #Javascript
Vue中在新窗口打开页面及Vue-router的使用
Jun 13 #Javascript
微信小程序支付功能 php后台对接完整代码分享
Jun 12 #Javascript
js replace 全局替换的操作方法
Jun 12 #Javascript
微信小程序自定义prompt组件步骤详解
Jun 12 #Javascript
js实现购物车功能
Jun 12 #Javascript
You might like
PHP函数常用用法小结
2010/02/08 PHP
那些年一起学习的PHP(一)
2012/03/21 PHP
php读取mysql的简单实例
2014/01/15 PHP
浅析php适配器模式(Adapter)
2014/11/25 PHP
详解Yii2 rules 的验证规则
2016/12/02 PHP
PHP中命名空间的使用例子
2019/03/22 PHP
自己开发Dojo的建议框架
2008/09/24 Javascript
jquery获取tagName再进行判断
2014/05/29 Javascript
js jquery获取当前元素的兄弟级 上一个 下一个元素
2015/09/01 Javascript
javascript设计简单的秒表计时器
2020/09/05 Javascript
js检测iframe是否加载完成的方法
2015/11/26 Javascript
Node.js刷新session过期时间的实现方法推荐
2016/05/18 Javascript
详解PHP中pathinfo()函数导致的安全问题
2017/01/05 Javascript
babel的使用及安装配置教程
2018/02/22 Javascript
详解关于Angular4 ng-zorro使用过程中遇到的问题
2018/12/05 Javascript
微信小程序云开发如何实现数据库自动备份实现
2019/08/16 Javascript
python中django框架通过正则搜索页面上email地址的方法
2015/03/21 Python
Python使用tablib生成excel文件的简单实现方法
2016/03/16 Python
windows系统下Python环境搭建教程
2017/03/28 Python
利用python GDAL库读写geotiff格式的遥感影像方法
2018/11/29 Python
python的列表List求均值和中位数实例
2020/03/03 Python
Python如何设置指定窗口为前台活动窗口
2020/08/12 Python
使用BeautifulSoup4解析XML的方法小结
2020/12/07 Python
HTML5实现应用程序缓存(Application Cache)
2020/06/16 HTML / CSS
英国玛莎百货美国官网:Marks & Spencer美国
2018/11/06 全球购物
俄罗斯最大的在线珠宝大卖场:Nebo
2019/12/08 全球购物
戴尔新西兰官网:Dell New Zealand
2020/01/07 全球购物
如何写毕业求职自荐信
2013/11/06 职场文书
七夕相亲活动策划方案
2014/08/31 职场文书
房地产经营管理专业自荐信
2014/09/02 职场文书
党政领导班子民主生活会整改措施
2014/09/18 职场文书
家长对孩子的寄语
2015/02/26 职场文书
道歉信范文
2015/05/12 职场文书
谁动了我的奶酪读书笔记
2015/06/30 职场文书
Python代码风格与编程习惯重要吗?
2021/06/03 Python
MongoDB误操作后使用oplog恢复数据
2022/04/11 MongoDB