對於如今的前端項目而言,編譯發佈幾乎是必需操做,有的編譯只須要幾秒鐘,快如閃電,有的卻須要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 的過程,踩坑是必須的,關鍵是踩坑後,你能獲得什麼?
另外,除了文中介紹的一些優化方法,更多的優化策略,正在逐步驗證中…