对于如今的前端项目而言,编译发布几乎是必需操做,有的编译只须要几秒钟,快如闪电,有的却须要10分钟,甚至更多,慢如蜗牛。特别是线上热修复时,分秒必争,响应速度直接影响了用户体验,用户不会有耐心等那么长时间,让你慢慢编译;若是涉及到支付操做,产品损失更是以秒计,每提早哪怕一秒钟发布,在腾讯海量用户面前,都能挽回不小的损失。不只如此,编译效率的提高,带来的最直观收益就是,开发效率与开发体验双重提高。css
那么,究竟是什么拖慢了webpack打包效率,咱们又能作哪些提高呢?html
webpack 是目前很是受欢迎的打包工具,截止6天前,webpack4 已更新至 4.28.3
版本,10 个月的时间,小版本更新达几十次之多,可见社区之繁荣。前端
webpack4 发布时,官方也曾表示,其编译速度提高了 60% ~ 98%。vue
因为本地项目升级到 webpack4 有几个月了,为了得到测试数据,手动将 webpack 降级为 3.12.0 版本,其它配置基本不作改动。node
测试时,Mac仅运行经常使用的IM、邮箱、终端、浏览器等,为了尽量避免插件对数据的影响,我关闭了一些优化插件,只保留经常使用的loader、js压缩插件。jquery
如下是分别在 webpack@3.12.0 及 webpack@4.26.1 两种场景下各测 5 次的运行截图。webpack
数据分析以下(单位ms):ios
第1次 | 第2次 | 第3次 | 第4次 | 第5次 | 平均 | 速度提高 | |
webpack3 | 58293 | 60971 | 57263 | 58993 | 60459 | 59195.8 | - |
webpack4 | 42346 | 40386 | 40138 | 40330 | 40323 | 40704.6 | 45% |
纯粹的版本升级,编译速度提高为 45%
,这里我选取的是成熟的线上运行项目,构建速度的提高只有创建在成熟项目上才有意义,demo 项目因为编译文件基数小,难以体现出构建环境的复杂性,测试时也可能存在较大偏差。同时与官方数据的差距,主要是由于基于的项目及配置不一样。web
不管如何,近 50% 的编译速度提高,都值得你尝试升级 webpack4!固然,优化才刚刚开始,请继续往下读。vue-router
为了更流畅的升级 webpack4,咱们先要了解它。
./src/
目录,默认entry ./src/index.js
,默认输出 ./dist
目录,默认输出文件 ./dist/main.js
。development
将得到最好的开发体验,设置为 production
将专一项目编译部署,好比说开启 Scope hoisting 和 Tree-shaking 功能。
首先,webpack-dev-server 插件须要升级至最新,同时,因为webpack-cli 承担了webpack4 命令行相关的功能,所以 webpack-cli 也是必需的。
与以往不一样的是,mode属性必须指定,不然按照 约定优于配置
原则,将默认按照 production
生产环境编译,以下是警告原文。
WARNING in configuration
The ‘mode’ option has not been set, webpack will fallback to ‘production’ for this value. Set ‘mode’ option to ‘development’ or ‘production’ to enable defaults for each environment.
You can also set it to ‘none’ to disable any default behavior. Learn more: https://webpack.js.org/concepts/mode/
有两种方式能够加入mode配置。
"scripts": { "dev": "webpack-dev-server --mode development --inline --progress --config build/webpack.dev.config.js", "build": "webpack --mode production --progress --config build/webpack.prod.config.js" }
module.exports = { mode: 'production' // 或 development };
升级至webpack4后,一些默认插件由 optimization 配置替代了,以下:
不只如此,optimization 还提供了以下默认配置:
optimization: { minimize: env === 'production' ? true : false, // 开发环境不压缩 splitChunks: { chunks: "async", // 共有三个值可选:initial(初始模块)、async(按需加载模块)和all(所有模块) minSize: 30000, // 模块超过30k自动被抽离成公共模块 minChunks: 1, // 模块被引用>=1次,便分割 maxAsyncRequests: 5, // 异步加载chunk的并发请求数量<=5 maxInitialRequests: 3, // 一个入口并发加载的chunk数量<=3 name: true, // 默认由模块名+hash命名,名称相同时多个模块将合并为1个,能够设置为function automaticNameDelimiter: '~', // 命名分隔符 cacheGroups: { // 缓存组,会继承和覆盖splitChunks的配置 default: { // 模块缓存规则,设置为false,默认缓存组将禁用 minChunks: 2, // 模块被引用>=2次,拆分至vendors公共模块 priority: -20, // 优先级 reuseExistingChunk: true, // 默认使用已有的模块 }, vendors: { test: /[\\/]node_modules[\\/]/, // 表示默认拆分node_modules中的模块 priority: -10 } } } }
splitChunks是拆包优化的重点,若是你的项目中包含 element-ui 等第三方组件(组件较大),建议单独拆包,以下所示。
splitChunks: { // ... cacheGroups: { elementUI: { name: "chunk-elementUI", // 单独将 elementUI 拆包 priority: 15, // 权重需大于其它缓存组 test: /[\/]node_modules[\/]element-ui[\/]/ } } }
其更多用法,请参考以上注释或官方文档 SplitChunksPlugin。
webpack4再也不支持Node 4,因为使用了JavaScript新语法,Webpack的创始人之一,Tobias,建议用户使用Node版本 >= 8.94,以便使用最优性能。
正式升级后,你可能会遇到各类各样的错误,其中,下面一些问题较为常见。
vue-loader v15 须要在 webpack 中添加 VueLoaderPlugin 插件,参考以下。
const { VueLoaderPlugin } = require("vue-loader"); // const VueLoaderPlugin = require("vue-loader/lib/plugin"); // 二者等同 //... plugins: [ new VueLoaderPlugin() ]
升级到 webpack4 后,mini-css-extract-plugin 替代 extract-text-webpack-plugin 成为css打包首选,相比以前,它有以下优点:
缺陷,不支持css热更新。所以需在开发环境引入 css-hot-loader,以便支持css热更新,以下所示:
{ test: /\.scss$/, use: [ ...(isDev ? ["css-hot-loader", "style-loader"] : [MiniCssExtractPlugin.loader]), "css-loader", postcss, "sass-loader" ] }
发布到生产环境以前,css是须要优化压缩的,使用 optimize-css-assets-webpack-plugin 插件便可,以下。
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin'); //... plugins: [ new OptimizeCssAssetsPlugin({ cssProcessor: cssnano, cssProcessorOptions: { discardComments: { removeAll: true } } }) ]
文章开始,我曾提到,优化才刚刚开始。是的,随着项目愈来愈复杂,webpack也随之变慢,必定有办法能够进一步压榨性能。
通过很长一段时间的多个项目运行以及测试,如下几点经验很是有效。
const resolve = dir => path.join(__dirname, '..', dir); // ... resolve: { modules: [ // 指定如下目录寻找第三方模块,避免webpack往父级目录递归搜索 resolve('src'), resolve('node_modules'), resolve(config.common.layoutPath) ], mainFields: ['main'], // 只采用main字段做为入口文件描述字段,减小搜索步骤 alias: { vue$: "vue/dist/vue.common", "@": resolve("src") // 缓存src目录为@符号,避免重复寻址 } }, module: { noParse: /jquery|lodash/, // 忽略未采用模块化的文件,所以jquery或lodash将不会被下面的loaders解析 // noParse: function(content) { // return /jquery|lodash/.test(content) // }, rules: [ { test: /\.js$/, include: [ // 表示只解析如下目录,减小loader处理范围 resolve("src"), resolve(config.common.layoutPath) ], exclude: file => /test/.test(file), // 排除test目录文件 loader: "happypack/loader?id=happy-babel" // 后面会介绍 }, ] }
实际上,搭载 webpack-parallel-uglify-plugin 插件,这个过程能够倍速提高。咱们都知道 node 是单线程的,但node可以fork子进程,基于此,webpack-parallel-uglify-plugin 可以把任务分解给多个子进程去并发的执行,子进程处理完后再把结果发送给主进程,从而实现并发编译,进而大幅提高js压缩速度,以下是配置。
const ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin'); // ... optimization: { minimizer: [ new ParallelUglifyPlugin({ // 多进程压缩 cacheDir: '.cache/', uglifyJS: { output: { comments: false, beautify: false }, compress: { warnings: false, drop_console: true, collapse_vars: true, reduce_vars: true } } }), ] }
固然,我分别测试了五组数据,以下是截图:
数据分析以下(单位ms):
第1次 | 第2次 | 第3次 | 第4次 | 第5次 | 平均 | 速度提高 | |
webpack3 | 58293 | 60971 | 57263 | 58993 | 60459 | 59195.8 | - |
webpack3搭载ParallelUglifyPlugin插件 | 44380 | 39969 | 39694 | 39344 | 39295 | 40536.4 | 46% |
webpack4 | 42346 | 40386 | 40138 | 40330 | 40323 | 40704.6 | - |
webpack4搭载ParallelUglifyPlugin插件 | 31134 | 29554 | 31883 | 29198 | 29072 | 30168.2 | 35% |
搭载 webpack-parallel-uglify-plugin 插件后,webpack3 的构建速度可以提高 46%;即便升级到 webpack4 后,构建速度依然可以进一步提高 35%。
const HappyPack = require('happypack'); const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length }); const createHappyPlugin = (id, loaders) => new HappyPack({ id: id, loaders: loaders, threadPool: happyThreadPool, verbose: process.env.HAPPY_VERBOSE === '1' // make happy more verbose with HAPPY_VERBOSE=1 })
loader: "happypack/loader?id=happy-babel"
happy-babel
plugins: [ createHappyPlugin('happy-babel', [{ loader: 'babel-loader', options: { babelrc: true, cacheDirectory: true // 启用缓存 } }]) ]
另外,像 vue-loader、css-loader 都支持 happyPack 加速,以下所示。
plugins: [ createHappyPlugin('happy-css', ['css-loader', 'vue-style-loader']), new HappyPack({ loaders: [{ path: 'vue-loader', query: { loaders: { scss: 'vue-style-loader!css-loader!postcss-loader!sass-loader?indentedSyntax' } } }] }) ]
基于 webpack4,搭载 webpack-parallel-uglify-plugin 和 happyPack 插件,测试截图以下:
数据分析以下(单位ms):
第1次 | 第2次 | 第3次 | 第4次 | 第5次 | 平均 | 速度提高 | |
仅搭载ParallelUglifyPlugin | 31134 | 29554 | 31883 | 29198 | 29072 | 30168.2 | 35% |
搭载ParallelUglifyPlugin 和 happyPack | 26036 | 25884 | 25645 | 25627 | 25794 | 25797.2 | 17% |
可见,在搭载 webpack-parallel-uglify-plugin 插件的基础上,happyPack 插件依然可以提高 17% 的编译速度,实际上因为 sass 等 loaders 不支持 happyPack,happyPack 的性能依然有提高空间。更多介绍不妨参考 happypack 原理解析。
为了完成 dll 过程,咱们须要准备一份新的webpack配置,即 webpack.dll.config.js。
const webpack = require("webpack"); const path = require('path'); const CleanWebpackPlugin = require("clean-webpack-plugin"); const dllPath = path.resolve(__dirname, "../src/assets/dll"); // dll文件存放的目录 module.exports = { entry: { // 把 vue 相关模块的放到一个单独的动态连接库 vue: ["babel-polyfill", "fastclick", "vue", "vue-router", "vuex", "axios", "element-ui"] }, output: { filename: "[name]-[hash].dll.js", // 生成vue.dll.js path: dllPath, library: "_dll_[name]" }, plugins: [ new CleanWebpackPlugin(["*.js"], { // 清除以前的dll文件 root: dllPath, }), new webpack.DllPlugin({ name: "_dll_[name]", // manifest.json 描述动态连接库包含了哪些内容 path: path.join(__dirname, "./", "[name].dll.manifest.json") }), ], };
接着, 须要在 package.json 中新增 dll 命令。
"scripts": { "dll": "webpack --mode production --config build/webpack.dll.config.js" }
运行 npm run dll
后,会生成 ./src/assets/dll/vue.dll-[hash].js
公共js 和 ./build/vue.dll.manifest.json
资源说明文件,至此 dll 准备工做完成,接下来在 wepack 中引用便可。
externals: { 'vue': 'Vue', 'vue-router': 'VueRouter', 'vuex': 'vuex', 'elemenct-ui': 'ELEMENT', 'axios': 'axios', 'fastclick': 'FastClick' }, plugins: [ ...(config.common.needDll ? [ new webpack.DllReferencePlugin({ manifest: require("./vue.dll.manifest.json") }) ] : []) ]
dll 公共js轻易不会变化,假如在未来真的发生了更新,那么新的dll文件名便须要加上新的hash,从而避免浏览器缓存老的文件,形成执行出错。因为 hash 的不肯定性,咱们在 html 入口文件中没办法指定一个固定连接的 script 脚本,恰好,add-asset-html-webpack-plugin 插件能够帮咱们自动引入 dll 文件。
const autoAddDllRes = () => { const AddAssetHtmlPlugin = require('add-asset-html-webpack-plugin'); return new AddAssetHtmlPlugin([{ // 往html中注入dll js publicPath: config.common.publicPath + "dll/", // 注入到html中的路径 outputPath: "dll", // 最终输出的目录 filepath: resolve("src/assets/dll/*.js"), includeSourcemap: false, typeOfAsset: "js" // options js、css; default js }]); }; // ... plugins: [ ...(config.common.needDll ? [autoAddDllRes()] : []) ]
搭载 dll 插件后,webpack4 编译速度进一步提高,以下截图:
数据分析以下(单位ms):
第1次 | 第2次 | 第3次 | 第4次 | 第5次 | 平均 | 速度提高 | |
搭载ParallelUglifyPlugin 和 happyPack | 26036 | 25884 | 25645 | 25627 | 25794 | 25797.2 | 17% |
搭载ParallelUglifyPlugin 、happyPack 和 dll | 20792 | 20963 | 20845 | 21675 | 21023 | 21059.6 | 22% |
可见,搭载 dll 后,webpack4 编译速度仍能提高 22%。
综上,咱们汇总上面的屡次数据,获得下表:
第1次 | 第2次 | 第3次 | 第4次 | 第5次 | 平均 | 速度提高 | |
webpack3 | 58293 | 60971 | 57263 | 58993 | 60459 | 59195.8 | - |
webpack4 | 42346 | 40386 | 40138 | 40330 | 40323 | 40704.6 | 45% |
搭载ParallelUglifyPlugin 、happyPack 和 dll | 20792 | 20963 | 20845 | 21675 | 21023 | 21059.6 | 181% |
升级至 webpack4 后,经过搭载 ParallelUglifyPlugin 、happyPack 和 dll 插件,编译速度能够提高181%,总体编译时间减小了将近 2/3,为开发节省了大量编译时间!并且随着项目发展,这种编译提高愈来愈可观。
实际上,为了得到上面的测试数据,我关闭了 babel、ParallelUglifyPlugin 的缓存,开启缓存后,第二次编译时间平均为 12.8s,因为以前缓存过,编译速度相对 webpack3 将提高362%,即便你已经升级到 webpack4,搭载上述 3 款插件后,编译速度仍能得到 218% 的提高!
固然,编译速度做为一项指标,影响的更可能是开发者体验,与之相比,编译后文件大小更为重要。webpack4 编译的文件,比以前版本略小一些,为了更好的追踪文件 size 变化,开发环境和生产环境都须要引入 webpack-bundle-analyzer 插件,以下图。
文件 size 以下图所示:
sideEffects
从 webpack2 开始,tree-shaking 便用来消除无用模块,依赖的是 ES Module 的静态结构,同时经过在. babelrc 文件中设置 "modules": false
来开启无用的模块检测,相对粗暴。webapck4 灵活扩展了无用代码检测方式,主要经过在 package.json
文件中设置 sideEffects: false
来告诉编译器该项目或模块是 pure 的,能够进行无用模块删除,所以,开发公共组件时,能够尝试设置下。
为了使得 tree-shaking 真正生效,引入资源时,仅仅引入须要的组件尤其重要,以下所示:
import { Button, Input } from "element-ui"; // 只引入须要的组件
升级 webpack4 的过程,踩坑是必须的,关键是踩坑后,你能获得什么?
另外,除了文中介绍的一些优化方法,更多的优化策略,正在逐步验证中…