前言

对于现在的前端项目而言,编译发布几乎是必需操作,有的编译只需要几秒钟,快如闪电,有的却需要10分钟,甚至更多,慢如蜗牛。特别是线上热修复时,分秒必争,响应速度直接影响了用户体验,用户不会有耐心等那么长时间,让你慢慢编译;如果涉及到支付操作,产品损失更是以秒计,每提前哪怕一秒钟发布,在腾讯海量用户面前,都能挽回不小的损失。不仅如此,编译效率的提升,带来的最直观收益就是,开发效率与开发体验双重提升。

那么,到底是什么拖慢了webpack打包效率,我们又能做哪些提升呢?

webpack 是目前非常受欢迎的打包工具,截止6天前,webpack4 已更新至 4.28.3 版本,10 个月的时间,小版本更新达几十次之多,可见社区之繁荣。

webpack4 发布时,官方也曾表示,其编译速度提升了 60% ~ 98%。

天下武功,唯快不破

由于本地项目升级到 webpack4 有几个月了,为了获得测试数据,手动将 webpack 降级为 3.12.0 版本,其它配置基本不做改动。

测试时,Mac仅运行常用的IM、邮箱、终端、浏览器等,为了尽可能避免插件对数据的影响,我关闭了一些优化插件,只保留常用的loader、js压缩插件。

以下是分别在 webpack@3.12.0 及 webpack@4.26.1 两种场景下各测 5 次的运行截图。

数据分析如下(单位ms):

  第1次 第2次 第3次 第4次 第5次 平均 速度提升
webpack3 58293 60971 57263 58993 60459 59195.8 -
webpack4 42346 40386 40138 40330 40323 40704.6 45%
 

纯粹的版本升级,编译速度提升为 45%,这里我选取的是成熟的线上运行项目,构建速度的提升只有建立在成熟项目上才有意义,demo 项目由于编译文件基数小,难以体现出构建环境的复杂性,测试时也可能存在较大误差。同时与官方数据的差距,主要是因为基于的项目及配置不同。

无论如何,近 50% 的编译速度提升,都值得你尝试升级 webpack4!当然,优化才刚刚开始,请继续往下读。

新特性

为了更流畅的升级 webpack4,我们先要了解它。

webpack4 在大幅度提升编译效率同时,引入了多种新特性:

  1. 受 Parcel 启发,支持 0 配置启动项目,不再强制需要 webpack.config.js 配置文件,默认入口 ./src/ 目录,默认entry ./src/index.js ,默认输出 ./dist 目录,默认输出文件 ./dist/main.js
  2. 开箱即用 WebAssembly,webpack4提供了wasm的支持,现在可以引入和导出任何一个 Webassembly 的模块,也可以写一个loader来引入C++、C和Rust。(注:WebAssembly 模块只能在异步chunks中使用)
  3. 提供mode属性,设置为 development 将获得最好的开发体验,设置为 production 将专注项目编译部署,比如说开启 Scope hoisting 和 Tree-shaking 功能。
  4. 全新的插件系统,提供了针对插件和钩子的新API,变化如下:
  1. 所有的 hook 由 hooks 对象统一管理,它将所有的hook作为可扩展的类属性
  2. 添加插件时,你需要提供一个名字
  3. 开发插件时,你可以选择插件的类型(sync/callback/promise之一)
  4. 通过 this.hooks = { myHook: new SyncHook(…) } 来注册hook
  1. 更多插件的工作原理,可以参考:新插件系统如何工作

快上车,升级前的准备

首先,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配置。

  • 在package.json script中指定–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"
    }
  • 在配置文件中加入mode属性
    module.exports = {
    mode: 'production' // 或 development
    };

升级至webpack4后,一些默认插件由 optimization 配置替代了,如下:

  • CommonsChunkPlugin废弃,由 optimization.splitChunks 和 optimization.runtimeChunk 替代,前者拆分代码,后者提取runtime代码。原来的CommonsChunkPlugin产出模块时,会包含重复的代码,并且无法优化异步模块,minchunks的配置也较复杂,splitChunks解决了这个问题;另外,将 optimization.runtimeChunk 设置为true(或{name: “manifest”}),便能将入口模块中的runtime部分提取出来。
  • NoEmitOnErrorsPlugin 废弃,由 optimization.noEmitOnErrors 替代,生产环境默认开启。
  • NamedModulesPlugin 废弃,由 optimization.namedModules 替代,生产环境默认开启。
  • ModuleConcatenationPlugin 废弃,由 optimization.concatenateModules 替代,生产环境默认开启。
  • optimize.UglifyJsPlugin 废弃,由 optimization.minimize 替代,生产环境默认开启。

不仅如此,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打包首选,相比之前,它有如下优势:

  1. 异步加载
  2. 不重复编译,性能更好
  3. 更容易使用

缺陷,不支持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也随之变慢,一定有办法可以进一步压榨性能。

经过很长一段时间的多个项目运行以及测试,以下几点经验非常有效。

1. 缩小编译范围,减少不必要的编译工作,即 modules、mainFields、noParse、includes、exclude、alias全部用起来。

  1. 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" // 后面会介绍
    },
    ]
    }

2. 想要进一步提升编译速度,就要知道瓶颈在哪?通过测试,发现有两个阶段较慢:① babel 等 loaders 解析阶段;② js 压缩阶段。loader 解析稍后会讨论,而 js 压缩是发布编译的最后阶段,通常webpack需要卡好一会,这是因为压缩 JS 需要先将代码解析成 AST 语法树,然后需要根据复杂的规则去分析和处理 AST,最后将 AST 还原成 JS,这个过程涉及到大量计算,因此比较耗时。如下图,编译就看似卡住。

实际上,搭载 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%。

3. 现在我们来看看,loader 解析速度如何提升。同 webpack-parallel-uglify-plugin 插件一样,HappyPack 也能实现并发编译,从而可以大幅提升 loader 的解析速度, 如下是部分配置。

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"

      这句,便需要在 plugins 中创建一个

happy-babel

    的插件实例。
plugins: [
createHappyPlugin('happy-babel', [{
loader: 'babel-loader',
options: {
babelrc: true,
cacheDirectory: true // 启用缓存
}
}])
]
    如下,happyPack开启了3个进程(默认为CPU数-1),运行过程感受下。

另外,像 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 原理解析

4. 我们都知道,webpack打包时,有一些框架代码是基本不变的,比如说 babel-polyfill、vue、vue-router、vuex、axios、element-ui、fastclick 等,这些模块也有不小的 size,每次编译都要加载一遍,比较费时费力。使用 DLLPlugin 和 DLLReferencePlugin 插件,便可以将这些模块提前打包。

为了完成 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 如下图所示:

面向tree-shaking,约束编码

sideEffects

从 webpack2 开始,tree-shaking 便用来消除无用模块,依赖的是 ES Module 的静态结构,同时通过在. babelrc 文件中设置 "modules": false 来开启无用的模块检测,相对粗暴。webapck4 灵活扩展了无用代码检测方式,主要通过在 package.json 文件中设置 sideEffects: false 来告诉编译器该项目或模块是 pure 的,可以进行无用模块删除,因此,开发公共组件时,可以尝试设置下。

为了使得 tree-shaking 真正生效,引入资源时,仅仅引入需要的组件尤为重要,如下所示:

import { Button, Input } from "element-ui"; // 只引入需要的组件

结尾

升级 webpack4 的过程,踩坑是必须的,关键是踩坑后,你能得到什么?

另外,除了文中介绍的一些优化方法,更多的优化策略,正在逐步验证中…

webpack4提升180%编译速度的更多相关文章

  1. Android提升Gradle编译速度或减少Gradle编译时间.md

    目录 Android如何提升Gradle编译速度或减少Gradle编译时间 最终优化方案 优化效果比对 将所有项目源码,各种缓存临时目录都移动到高性能SSD磁盘上 gradle.properties ...

  2. 使用 IncrediBuild 提升 VisualStudio 编译速度

    我现在有一个 100M 的代码,需要快速去编译他,我寻找了很多方法,本文记录我找到的 IncrediBuild 用于提交编译速度. 如果一个项目存在很多不相互依赖的项目,那么使用 IncrediBui ...

  3. 提升Android编译速度

    Android codebase都非常大.编译一次都须要花非常多时间.假设是preloader/lk/bootimage还好,可是Android的话都是非常久. 实际上这个编译时间还是能够进一步缩短! ...

  4. dWebpack编译速度优化实战

    当你的应用的规模还很小时,你可能不会在乎Webpack的编译速度,无论使用3.X还是4.X版本,它都足够快,或者说至少没让你等得不耐烦.但随着业务的增多,嗖嗖嗖一下项目就有上百个组件了,也是件很简单的 ...

  5. 2019-3-15-在-Windows-Defender-设置文件夹白名单提升-VisualStudio-编译速度

    title author date CreateTime categories 在 Windows Defender 设置文件夹白名单提升 VisualStudio 编译速度 lindexi 2019 ...

  6. JavaScript的工作原理:解析、抽象语法树(AST)+ 提升编译速度5个技巧

    这是专门探索 JavaScript 及其所构建的组件的系列文章的第 14 篇. 如果你错过了前面的章节,可以在这里找到它们: JavaScript 是如何工作的:引擎,运行时和调用堆栈的概述! Jav ...

  7. 微信团队分享:极致优化,iOS版微信编译速度3倍提升的实践总结

    1.引言 岁月真是个养猪场,这几年,人胖了,微信代码也翻了. 记得 14 年转岗来微信时,用自己笔记本编译微信工程才十来分钟.如今用公司配的 17 年款 27-inch iMac 编译要接近半小时:偶 ...

  8. Java动态编译优化——提升编译速度(N倍)

    一.前言 最近一直在研究Java8 的动态编译, 并且也被ZipFileIndex$Entry 内存泄漏所困扰,在无意中,看到一个第三方插件的动态编译.并且编译速度是原来的2-3倍.原本打算直接用这个 ...

  9. 如何提升代码编译的速度 iOS

    前阵子有遇到代码编译速度慢的问题,特别是在swift和object-c混编的过程中问题很突显. 网上找到一篇蛮好的文章里面又一些解决方法 推荐一下 http://www.open-open.com/l ...

随机推荐

  1. MFCdll的两种调用方法

        有同事问我mfcdll的掉用方法,这里总结了一下.   1 lib库调用   只要VS能找到dll对应的lib和h文件,就可以开发和调试.包含lib和h文件有两个方法. 第一种方法设置路径是 ...

  2. Java入门到精通——框架篇之Spring源码分析Spring两大核心类

    一.Spring核心类概述. Spring里面有两个最核心的类这是Spring实现最重要的部分. 1.DefaultListableBeanFactory 这个类位于Beans项目下的org.spri ...

  3. matlab练习程序(生成黑白网格)

    提供了两种生成方法,一个是自己编程实现,比较灵活:另一个是调用系统的checkerboard函数,似乎只能生成8*8网格. 至于用途,也许可以用来下国际象棋. 自己函数生成: 系统函数生成: 代码如下 ...

  4. ASP.NET MVC4 with MySQL: Configuration Error (MySql.Web.v20)

    今天在浏览ASP.NET项目时,提示如下错误: Could not load file or assembly ‘MySql.Web.v20, Version=6.9.4.0, Culture=neu ...

  5. 简谈 Java 中的泛型通配符

    很好的一篇文章https://zhuanlan.zhihu.com/p/26681625

  6. matlab中fprintf函数的具体使用方法

    matlab中fprintf函数的具体使用方法实例如下: fprintf函数可以将数据按指定格式写入到文本文件中.其调用格式为: 数据的格式化输出:fprintf(fid, format, varia ...

  7. Python类三种方法,函数传参,类与实例变量(一)

    1 Python的函数传递: 首先所有的变量都可以理解为内存中一个对象的'引用' a = 1 def func(a): a = 2 func(a) print(a) # 1 a = 1 def fun ...

  8. 【Leetcode】【Easy】Pascal's Triangle

    Given numRows, generate the first numRows of Pascal's triangle. For example, given numRows = 5,Retur ...

  9. Ubuntu 14.04默认以字符界面启动

    在windows上跑虚拟机比较资源,特别当以图形界面启动时,如果宿主机性能不好,就相当卡. 让Ubuntu 14.04默认以字符界面启动的方法: 编辑文件:etc/default/grub 将  GR ...

  10. Vaadin学习笔记——Page、UI和View在用法上的区别

    前言 在Vaadin技术框架中会出现三种不同的类,用于架构Web应用.它们分别是:Page.UI.View.本文将对这三者从使用角度进行比较,试图分析三者的异同.本文完全原创,我可不是在强调版权,我只 ...