webpack4提高180%编译速度

前言

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

那么,究竟是什么拖慢了webpack打包效率,咱们又能作哪些提高呢?html

 

image.png

 

 

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

 

 

image.png

 

 

数据分析以下(单位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,咱们先要了解它。

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,这个过程涉及到大量计算,所以比较耗时。以下图,编译就看似卡住。

 

 

image.png

 

 

实际上,搭载 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
                }
            }
        }),
    ]
}

固然,我分别测试了五组数据,以下是截图:

 

 

image.png

 

 

数据分析以下(单位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),运行过程感觉下。

 

 

image.png

 

 

另外,像 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 插件,测试截图以下:

 

image.png

 

 

数据分析以下(单位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 编译速度进一步提高,以下截图:

 

 

image.png

 

数据分析以下(单位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 插件,以下图。

 

image.png

 

 

文件 size 以下图所示:

 

 

image.png

 

 

面向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 的过程,踩坑是必须的,关键是踩坑后,你能获得什么?

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