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 的過程,踩坑是必須的,關鍵是踩坑後,你能獲得什麼?

另外,除了文中介紹的一些優化方法,更多的優化策略,正在逐步驗證中…

相關文章
相關標籤/搜索