webpack4提升180%編譯速度


前言

對於現在的前端項目而言,編譯發布幾乎是必需操作,有的編譯只需要幾秒鍾,快如閃電,有的卻需要10分鍾,甚至更多,慢如蝸牛。特別是線上熱修復時,分秒必爭,響應速度直接影響了用戶體驗,用戶不會有耐心等那么長時間,讓你慢慢編譯;如果涉及到支付操作,產品損失更是以秒計,每提前哪怕一秒鍾發布,在騰訊海量用戶面前,都能挽回不小的損失。不僅如此,編譯效率的提升,帶來的最直觀收益就是,開發效率與開發體驗雙重提升。

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

 

image.png

 

 

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 次的運行截圖。

 

 

image.png

 

 

數據分析如下(單位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,這個過程涉及到大量計算,因此比較耗時。如下圖,編譯就看似卡住。

 

 

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

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM