1. 程式人生 > >記一次真實的webpack優化經歷

記一次真實的webpack優化經歷

![](https://img2020.cnblogs.com/blog/774496/202012/774496-20201222101433450-1983008947.png) # 前言 公司目前現有的一款產品是使用`vue v2.0`框架實現的,配套的打包工具為`webpack v3.0`。整個專案大概有`80`多個`vue`檔案,也算不上什麼大型專案。 只不過每次頭疼的就是打包所耗費的時間平均在`一分鐘`左右,而且打包後有幾個檔案顯示為`【big】`,也就是檔案體積過大。 最近就想著搗鼓一下,看能不能在此前的基礎上做一些優化,順帶記錄下來分享給大家。 # webpack打包優化 關於`webpack`的打包優化一般會從兩個方面考慮:`縮短打包時長`和`降低打包後的檔案體積`,這兩個方面也剛好是前面我需要解決的問題。 所以我們先來了解一下這兩個方面各自有什麼具體的實現方式。 ## 縮短打包時長 我們都知道`webpack`的執行流程就像一條生產線一樣,在這條生產線上會按順序的執行每一個流程。那很顯然如果每一個流程要乾的事情越少或者每一個流程有多個人來共同完成,那`webpack`打包的執行效率就會提高。 #### `1.減少loader搜尋檔案範圍` 我們可以通過配置`loader`的`exclude`選項,告訴對應的`loader`可以忽略某個`目錄`;或者通過配置`loader`的`include`選項,告訴`loader`只需要處理指定的`目錄`。因為`loader`處理的檔案越少,執行速度就會更快。 一般比較常見的就是給`babel-loader`配置`exclude`選項。 ```javascript // webpack.config.js module.exports = { entry: {}, output: {}, plugin: [], module: { rules:[ { test: /\.js$/, loader: 'babel-loader', exclude: /node_modules/ // exclude的值是一個正則 } ] } } ``` 以上配置即告訴`babel-loader`在轉化`JS`程式碼的時候忽略`node_modules`目錄,這麼配置是因為我們引用`node_modules`下的包基本都是編譯過的,所以不需要在通過`babel-loader`處理。 #### `2.利用快取` 關於`webpack`的快取,官方的大致解釋為:開啟快取以後,`webpack`構建將嘗試從快取中讀取資料,以避免每次執行時都需要執行代價高昂的重新編譯過程。 那如何在編譯程式碼時開啟快取呢? `◕ cacheDirectory` 第一種是配置`babel-loader`的`cacheDirectory`選項,針對`babel-loader`開啟快取。 ```javascript // webpack.config.js module.exports = { entry: {}, output: {}, plugin: [], module: { rules:[ { test: /\.js$/, loader: 'babel-loader?cacheDirectory', exclude: /node_modules/ } ] } } ``` `◕ cache-loader` 第二種是利用`cache-loader`。 首先需要對其進行安裝:`npm install cache-loader --save-dev `;接著在`webpack`中進行配置: ```javascript // webpack.config.js module.exports = { entry: {}, output: {}, plugin: [], module: { rules:[ { test: /\.js$/, loader: [ 'cache-loader', 'babel-loader' ] exclude: /node_modules/ }, { test: /\.ext$/, use: [ 'cache-loader', // 其他的loader // ... ], } ] } } ``` 對於`cache-loader`官方給出的使用建議為:`在一些效能開銷較大的loader之前新增此loader,以將結果快取到磁盤裡;儲存和讀取這些快取檔案會有一些時間開銷,所以請只對效能開銷較大的loader使用此 loader`。 > 可以簡單粗暴的認為如果一個`loader`在執行過程中處理的任務較多,較為耗時,即判定此`loader`效能開銷較大。我們就可以嘗試給該`loader`開啟快取,當然如果開啟快取以後實際的`打包時長`並沒有降低,則說明開啟快取對該`loader`的效能影響不大。 > 更多有關`cache-loader`的內容可以檢視:`https://www.webpackjs.com/loaders/cache-loader/` `◕ hard-source-webpack-plugin` 第三種是開啟快取的方式是使用`hard-source-webpack-plugin`。它是一個`webpack`外掛,安裝命令為:`npm install --save-dev hard-source-webpack-plugin`;最基礎的配置如下: ```javascript // webpack.config.js // 引入 const HardSourceWebpackPlugin = require('hard-source-webpack-plugin'); // 只在生產環境下開啟HardSourceWebpackPlugin if (process.env.NODE_ENV === "production") { module.exports.plugins = (module.exports.plugins || []).concat([ new HardSourceWebpackPlugin() ]) } ``` > 更多有關`hard-source-webpack-plugin`的用法可以檢視:`https://github.com/mzgoddard/hard-source-webpack-plugin` 以上三種`開啟快取`的方式雖然各不相同,但只要做了配置就可以在我們的磁碟中看到它們的快取結果。 ![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/63d192f1392e4500b22baf635b647441~tplv-k3u1fbpfcp-zoom-1.image) #### `3.多執行緒` 多執行緒也就是將一件事情交給多個人去做,從理論上來講是可以提高一件事情的完成效率。 `◕ happyhack` 我們都知道受限於`node`的`單執行緒`模式,`webpack`的整個`執行`和`構建`過程也是`單執行緒`模式的。 所以第一種開啟`多執行緒`的方式就是將`webpack`中`loader`的執行過程從`單執行緒`擴充套件到`多執行緒`。這種方式的具體實現依賴的是`HappyPack`外掛。 使用`happypack`的第一步依然是安裝:`npm install --save-dev happypack`;最簡單的配置如下: ```javascript // webpack.config.js // 引入 const HappyPack = require('happypack'); module.exports = { entry: {}, output: {}, plugin: [], module: { rules:[ { test: /\.js$/, // 使用loader調起happypack loader: 'happypack/loader', exclude: /node_modules/ } ] } } // 只有在生產環境下配置對應的happypack if (process.env.NODE_ENV === "production") { module.exports.plugins = (module.exports.plugins || []).concat([ new HappyPack({ // re-add the loaders you replaced above in #1: loaders: 'babel-loader', }) ]) } ``` 這樣的配置表示匹配到的`.js`原始碼將被傳遞給`HappyPack`,`HappyPack`將使用`loaders`指定的載入器(本例中是`babel-loader`)並行地轉換它們。 這種最基礎的配置,預設是`3`個執行緒並行處理。同時我們也可以通過配置`thread`選項,自定義執行緒個數。 ```javascript // webpack.config.js new HappyPack({ // re-add the loaders you replaced above in #1: loaders: 'babel-loader', // 自定義執行緒個數 threads: 2, }) ``` 關於`執行緒`的設定,官方推薦使用`共享執行緒池`的方式來控制`執行緒個數`:`However, if you're using more than one HappyPack plugin it can be more optimal to create a thread pool yourself and then configure the plugins to share that pool, minimizing the idle time of threads within it.(但是,如果您使用多個HappyPack外掛,那麼最好自己建立一個執行緒池,然後配置這些外掛來共享該池,從而最大限度地減少其中執行緒的空閒時間。)` `執行緒池`的建立也很簡單: ```javascript // webpack.config.js const happyThreadPool = HappyPack.ThreadPool({ size: 4 }); ``` 除了可以通過上面的這種方式建立具體的執行緒數,也可以根據`CPU`的核數建立: ```javascript // webpack.config.js const os = require('os'); const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length-1 }); ``` `執行緒池`建立好了以後,通過`threadPool`進行指定`共享執行緒池`: ```javascript // webpack.config.js // 此處省略一些程式碼 new HappyPack({ // re-add the loaders you replaced above in #1: loaders: 'babel-loader', // 使用共享執行緒池 threadPool: happyThreadPool }) ``` 最後一個實用的配置項是`verbose`選項,可以讓`happypack`輸出執行日誌: ```javascript // webpack.config.js // 此處省略一些程式碼 new HappyPack({ // re-add the loaders you replaced above in #1: loaders: 'babel-loader', // 使用共享執行緒池 threadPool: happyThreadPool, // 輸出執行日誌 verbose: true }) ``` > 更多有關`HappyPack`的內容的可以檢視:`https://github.com/amireh/happypack` 不過這裡很遺憾的是該外掛的作者在`github`上面宣佈他本人不會在對該專案進行更新維護了。 ![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/afd3b36483154936a475af79144e477f~tplv-k3u1fbpfcp-zoom-1.image) `◕ thread-loader` `thread-loader`和`happypack`類似,也是通過擴充套件`loader`的處理執行緒來降低打包時間。安裝命令:`npm install thread-loader --save-dev`;最簡單的配置如下: ```javascript // webpack.config.js module.exports = { entry: {}, output: {}, plugin: [], module: { rules:[ { test: /\.js$/, loader: [ 'thread-loader', 'babel-loader' ] exclude: /node_modules/ } ] } } ``` 即將`thread-loader`配置到其他的`loader`之前即可。 > 更多有關`thread-loader`的內容可以檢視:`https://www.webpackjs.com/loaders/thread-loader` `◕ webpack-parallel-uglify-plugin` 一般我們為了減少打包後的檔案體積,會對檔案進行壓縮,比如`刪除換行`、`刪除中註釋`等。那常見的就是對`JS`進行壓縮,最基本的就是使用`webpack`官方提供的`uglifyjs-webpack-plugin`外掛;不過該外掛是單執行緒壓縮程式碼,效率相對來說比較低。而`webpack-parallel-uglify-plugin`就是一款`多執行緒`壓縮`js`程式碼的外掛; 安裝命令:`npm install webpack-parallel-uglify-plugin`;簡單的配置如下: ```javascript // webpack.config.js // 引入 ParallelUglifyPlugin 外掛 const ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin'); // 只在生產環境中配置ParallelUglifyPlugin if (process.env.NODE_ENV === "production") { new ParallelUglifyPlugin({ workerCount: 4,//開啟幾個子程序去併發的執行壓縮。預設是當前執行電腦的cPU核數減去1 cacheDir: './cache/', uglifyJs:{ output:{ beautify:false,//不需要格式化 comments:false,//不保留註釋 }, compress:{ warnings:false,// 在Uglify]s除沒有用到的程式碼時不輸出警告 drop_console:true,//刪除所有的console語句,可以相容ie瀏覽器 collapse_vars:true,//內嵌定義了但是隻用到一次的變數 reduce_vars:true,//取出出現多次但是沒有定義成變數去引用的靜態值 } } }), } ``` 之後在進行打包,可以顯著提升`JS`程式碼的壓縮效率。 > 這個外掛的使用一定要注意`版本`的問題,如果配置以後在構建程式碼時出現問題,可以嘗試更換低版本。 > 本次我的`webpack`版本為`v3.6`,直接安裝的`webpack-parallel-uglify-plugin`版本為`v2.0.0`。後面打包出現錯誤,於是將其版本降低為`0.4.2`後就可以正常打包。 關於`多執行緒`我們特別需要注意,並不是`執行緒數量`越多構建時間就越短。因為子執行緒處理完成後需要將把結果傳送到`主程序`中,`主程序`在進行彙總處理,這個過程也是需要耗費時間的。所以最適合的執行緒數量可以嘗試通過實踐去獲得。 #### `4.動態連結庫` 一般我們在打包一個`vue`專案時,會將`vue`、`vue-router`、`axios`等這些外掛的程式碼跟我們的程式碼打包到一個檔案中,而這些`外掛`的程式碼除非版本有變化,否則程式碼內容基本不會發生變化。所以每次在打包專案時,實際上都在重複打包這些外掛的程式碼,很顯然浪費了很多時間。 關於`動態連結庫`這個詞實際上借用的是作業系統中的`動態連結庫`概念,`webpack`的具體實現也就是把前面我們描述的那些`外掛`分別打包成一個獨立的檔案。當有模組需要引用該外掛時會通過生成的`json`檔案連結到對應的外掛。這樣不管是我們在`開發環境`還是在`生成環境`下的打包構建,都不需要在對這些外掛做重複的處理。那接下來我們看看`動態連結庫`的配置和使用。 首先我們需要新建一個`webpack.dll.config.js`,該檔案本身是一個`webpack`配置檔案,主要用於分離第三方外掛。 ```javascript // webpack.dll.config.js const path = require("path"); const webpack = require("webpack"); const { CleanWebpackPlugin } = require("clean-webpack-plugin"); const DllPlugin = require('webpack/lib/DllPlugin'); // 分離出來的第三方庫檔案存放的目錄 const dllPath = "webpackDLL"; module.exports = { // 入口檔案 入口處配置需要分離的第三方外掛 entry: { echarts: ['echarts'], // 該配置表示分離echarts外掛 }, // 輸出檔案 output: { path: path.join(__dirname, dllPath), // 分離出來的第三方外掛儲存位置 filename: "[name]-dll.[hash:8].js", // 分離出來的第三方外掛檔名稱 library: '_dll_[name]' // 第三方外掛的名稱,後續其他模組需要引用該外掛,便用該名稱進行引用 }, resolve: { extensions: ['.js', '.vue', '.json'], alias: { 'vue$': 'vue/dist/vue.esm.js', } }, plugins: [ // 清除之前的dll檔案 new CleanWebpackPlugin(), // 使用DLLPlugin進行分離 new webpack.DllPlugin({ // 生成的 *.manfest.json 檔案的路徑 path: path.join(__dirname, dllPath, "[name]-manifest.json"), // 這裡的name需要和前面output.library配置一致 // 之後生成的*.manfest.json 中有一個name欄位,值就是我們這裡配置的值 name: '_dll_[name]', }) ] }; ``` 關於上面各個`配置項`的含義已在註釋中說明。接下來我們先用這個配置項做一個打包,看一下結果。在這之前我們需要在`package.json`中新增一個`script`指令碼。 ```javascript // package.json { "scripts": { "dev": "cross-env NODE_ENV=development webpack-dev-server --hot --port 4500 --host 192.168.1.10", "build": "cross-env NODE_ENV=production webpack --progress --hide-modules", "dll": "webpack -p --progress --config ./webpack.dll.config.js" } } ``` 新增的`dll`指令碼會使用`webpack.dll.config.js`作為配置檔案執行打包任務。 ![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/370f35fe770c448d871542acaf0b5a2c~tplv-k3u1fbpfcp-zoom-1.image) 指令碼執行成功以後,本地已經生成了對應的目錄和檔案。 ![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0d17b786606449b6ae2ea5f51712c8e3~tplv-k3u1fbpfcp-zoom-1.image) 其中`echarts.dll.2a6026f8.js`就是我們分離出來的`echarts`外掛,`echarts-manifest.json`就是前面我們說的`json`檔案。 第三方庫檔案分離後,當有模組需要引用`echarts`時,該如何引用到對應的`echarts.dll.2a6026f8.js`檔案呢? 此時就需要`DllReferencePlugin`出場了:通過配置`DllReferencePlugin`的`manifest`檔案來把依賴的模組名稱對映到對應的外掛。這一步需要在`webpack.config.js`中進行配置: ```javascript // webpack.config.js const DllReferencePlugin = require('webpack/lib/DllReferencePlugin') module.exports = { entry: {}, output: {}, module: {}, plugin: [ new DllReferencePlugin({ // manifest 就是之前打包出來的 *.manifest.json 檔案 manifest: path.join(__dirname, 'webpackDll', 'echarts-manifest.json'), }), ] } ``` 以上配置完成後,如果我們處於`開發環境`,執行`npm run dev`開啟`瀏覽器`會發現頁面無法正常顯示,且控制檯有報錯資訊: ![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/002885f0e784472ab945af411eadf90d~tplv-k3u1fbpfcp-zoom-1.image) 這裡是因為我們還需要在入口模板檔案`index.html`中手動引入分離出來的`外掛`檔案。 ```html <%= htmlWebpackPlugin.options.title %>
``` 之後在重新整理頁面就沒有問題了。 這裡需要特別注意,`開發環境`中手動引入對應外掛的路徑為`./webpackDLL/*.2a6026f9.js`,此時如果對專案進行打包部署,打包後`index.html`引用的依然是`./webpackDLL/*.2a6026f9.js`,很顯然單是在本地環境中該資源的引用路徑就是錯誤的;更甚之專案打包後的`輸出路徑`一般都會單獨配置,比如`dist`目錄,部署時也只會部署該目錄下的檔案。 ![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/3ee7231d63624227a38317ab28339d72~tplv-k3u1fbpfcp-zoom-1.image) 所以僅僅是前面的配置,專案部署以後根本無法正常執行。 解決這個問題很顯然有一個簡單粗暴的方式:`index.html`中引入的路徑依然不變,打包後的程式碼依然在`dist`目錄下,只是打包完成後手動將對應的`webpackDLL`外掛目錄以及檔案複製到`dist`目錄下,這樣直接將`dist`目錄部署到伺服器即可正常執行。 ![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b5474c7f8e8f4c06b1fdf681fbcc9373~tplv-k3u1fbpfcp-zoom-1.image) 除了這種方式之外,我們完全可以藉助`webpack`的一些外掛來完成這個功能,這裡就不演示了,大家可以自己嘗試去完成。 ## 降低打包後的檔案體積 #### `1.壓縮檔案` `◕ image-webpack-loader` 關於圖片的壓縮可以選擇`image-webpack-loader`。正常情況下安裝命令為:`npm install image-webpack-loader --save-dev`,只不過我在使用該命令安裝時出現了很多錯誤,在網上收集到一些解決方案,最終發現將`npm`換成`cnpm`去安裝`image-webpack-loader`才能安裝成功:`cnpm install image-webpack-loader --save-dev`。 關於`image-webpack-loader`的最基礎的配置如下: ```javascript // webpack.config.js module.exports = { entry: {}, output: {}, plugin: [], module: { rules:[ { test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, exclude: [resolve("src/icons")], use: [ { loader: "url-loader", options: { limit: 1024*10, name: path.posix.join("assets", "images/[name].[hash:7].[ext]"), }, }, { loader: 'image-webpack-loader',// 壓縮圖片 options: { bypassOnDebug: true, } } ] } ] } } ``` >
更多有關`image-webpack-loader`的內容請檢視:`https://www.npmjs.com/package/image-webpack-loader` > `cache-loader4.1.0` 要求`webpack4.0.0` > `cache-loader 3.0.1` 要求`3.0.1` `◕ webpack-parallel-uglify-plugin` 該外掛用於壓縮`JS`程式碼(多執行緒壓縮),用法前面已經介紹過,這裡就不在介紹了。 > 通過壓縮檔案來減少檔案的體積的同時會導致`webpack`打包時長增加,因為這相當於在做一件事的過程中增加了一些步驟。 #### `2. 抽離第三方庫` `CommonsChunkPlugin`是`webpack`官方提供的一個外掛,通過配置這個外掛,可以將公共的模組抽離出來。 >
`webpack v4`已經不再支援該外掛,使用`SplitChunksPlugin`代替。但由於本專案使用的是`webpack v3`,因此這裡不對`SplitChunksPlugin`做介紹。 首先我們需要在`webpack`的`entry`選項對我們需要分離的`公共模組`進行配置。 ```javascript module.exports = { entry: { main: ["./src/main.js"], //原有的入口檔案 vender: ['echarts'] // 表示將echarts模組分離出來 }, } ``` 接著需要在`plugin`中配置這個`公共模組`的輸出: ```javascript module.exports = { plugins:[ new webpack.optimize.CommonsChunkPlugin({ name: 'vender', // 對應entry中配置的入口vender filename: '[name].js' // 分離出來的模組以name選項作為檔名,也就是vender.js }) ] } ``` 配置完成後對專案進行打包,就會看到打包結果中多出一個名為`vendor.js`的檔案。 ![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/22c743785f9a465496445f6a49fae298~tplv-k3u1fbpfcp-zoom-1.image) 此時我們可以去檢視有使用過`echarts`的元件被打包後的`js`檔案體積明顯減少。 如果我們還需要繼續分離其他的一些`公共模組`,可以在`entry`中繼續配置: ```javascript module.exports = { entry: { main: ["./src/main.js"], //原有的入口檔案 vender: ['echarts', 'vue', 'other-lib'] }, } ``` 如果前面配置的`plugin`的保持不變,則`entry.vendor`配置的公共模組統一會打包到`vendor.js`檔案中;那如果配置的`公共模組`過多,就會導致抽離出來的`vendor.js`檔案體積過大。 > 解決這個問題可以使用前面我們介紹過的`動態連結庫`對第三方外掛進行分離,後面實踐部分會提到。 #### `3.刪除無用程式碼` 一個產品在迭代的過程中不可避免的會產生一些`廢棄程式碼`,或者我們在使用一個前端元件庫時,只使用了元件庫中的一小部分元件,而打包時會將整個元件庫的內容進行打包。那不管是`廢棄程式碼`或者`未使用到的元件程式碼`都可以稱之為`無用的程式碼`,那很顯然刪除這些無用的程式碼也可以減少打包後的檔案體積。 `◕ purgecss-webpack-plugin` `PurgeCSS`是一個用來刪除未使用的`CSS程式碼`的工具。首先對其進行安裝:`npm install purgecss-webpack-plugin -save-dev`。 > 該外掛使用是需要和`mini-css-extract-plugin`外掛結合使用,因此還需要安裝`mini-css-extract-plugin`。不過特別需要注意`mini-css-extract-plugin`要求`webpack v4`。 接著在`webpack`配置檔案中進行配置: ```javascript // webpack.config.js const path = require('path') const glob = require('glob') const MiniCssExtractPlugin = require('mini-css-extract-plugin') const PurgecssPlugin = require('purgecss-webpack-plugin') const PATHS = { src: path.join(__dirname, 'src') } module.exports = { entry: './src/index.js', output: { filename: 'bundle.js', path: path.join(__dirname, 'dist') }, optimization: { splitChunks: { cacheGroups: { styles: { name: 'styles', test: /\.css$/, chunks: 'all', enforce: true } } } }, module: { rules: [ { test: /\.css$/, use: [ MiniCssExtractPlugin.loader, "css-loader" ] } ] }, plugins: [ new MiniCssExtractPlugin({ filename: "[name].css", }), new PurgecssPlugin({ paths: glob.sync(`${PATHS.src}/**/*`, { nodir: true }), }), ] } ``` > 更多有關`purgecss-webpack-plugin`的內容可以檢視:`https://www.purgecss.cn/`。 `◕ tree-shaking` 在webpack的官網中,對`tree-shaking`的解釋如下: ![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ebb906f5b7bf4f5287e89016325e9057~tplv-k3u1fbpfcp-zoom-1.image) 官方文件有說明在`webpack v4`可以通過`sideEffects`來實現,同時給我們演示了一些很基礎的示例。 關於這個`優化方案`,不管是在一些相關概念的理解還是專案的實踐中均沒有達到我想要的效果,所以在這裡僅僅把這個優化點梳理在這裡。關於該優化方案在專案中的具體配置和效果就不在演示了,以免誤導大家。 最後關於`tree-shaking`的一些知識,看到了一些解釋的較為詳細的文章,貼到這裡供大家參考: `1. 【你的Tree-Shaking並沒什麼卵用】(https://segmentfault.com/a/1190000012794598)` `2. 【Tree-Shaking效能優化實踐 - 原理篇 】(https://juejin.cn/post/6844903544756109319)` ## 打包分析工具 那除了前面我們介紹的具體的`優化方案`之外,還有兩個常用的`打包分析工具`可以幫助我們分析`構建過程`和打包後的`檔案體積`。 #### `1.speed-measure-webpack-plugin` `speed-measure-webpack-plugin`它是一個`webpack`外掛,用於測量打包的速度,並輸出打包過程中每一個`loader`和`plugin`的執行時間。 首先是對其進行安裝:`npm install speed-measure-webpack-plugin`;接著在`webpack.config.js`中進行配置: ```javascript // webpack.config.js // 引入 const SpeedMeasureWebpackPlugin = require('speed-measure-webpack-plugin'); // 建立例項 const smw = new SpeedMeasureWebpackPlugin(); // 呼叫例項的smw.wrap並將webpack的配置作為引數傳入 module.exports = smw.wrap({ entry: {}, output: {} module: {}, plugins:[], }) ``` 完成以上步驟以後,我們在執行`npm run build`,就能看到該外掛輸出的打包時長資訊。 ![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f0bd50629cdf4a1f9452fedecef3511b~tplv-k3u1fbpfcp-zoom-1.image) 從這些資訊裡面我們能很清楚看到每一個`plugin`和`loader`所花費的時長,這樣我們就可以針對耗費時間較長的部分進行優化。 #### `2.webpack-bundle-analyzer` `webpack-bundle-analyzer`也是一個`webpack`外掛,用於建立一個互動式的`樹形圖`視覺化所有打包後的`檔案`,包括檔案的`體積`和檔案裡面包含的`內容`。 它的使用也非常簡單,首先是安裝:`npm install webpack-budle-analyzer`;安裝完成後,只需要在`webpack`的配置檔案中寫入如下內容: ```javascript // webpack.config.js // 引入 const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; module.exports = { plugins: [ new BundleAnalyzerPlugin() ] } ``` 之後我們執行`npm run build`,打包結束以後`webpack`會輸出如下日誌。 ![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e0014f0ba9da49009d63e3d9f18e5378~tplv-k3u1fbpfcp-zoom-1.image) 接著會預設彈出瀏覽器視窗,開啟`http://127.0.0.1:8888/`。 ![圖片來源於官網](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d4296b2aa32d42068d971692de87ff34~tplv-k3u1fbpfcp-zoom-1.image) > 若沒有自動開啟,可以手動輸入地址。同時需要注意的是該外掛預設啟動在`8888`埠上,假如出現端口占用情況,可以對預設的埠進行配置,詳情可參考:`https://www.npmjs.com/package/webpack-bundle-analyzer`。 從頁面中我們可以清楚的看到每一個`檔案的大小`,同時還可以看到該檔案中引入了那些`模組`、每一個`模組`的`檔案大小`。根據這些內容,我們就可以有針對性的處理一些`大檔案`和這些`大檔案`中一些體積較大的`模組`。 ## 總結 到此我們已經列舉了很多具體的`webpack`優化方案和每一種`優化方案`的簡單配置。接下來我們會將這些方案應用到實際的專案中,在實踐開始之前我們先對前面的內容簡單做一個回顧和總結。 ![](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a6221278deb149e0ae56f953fcd1e8ac~tplv-k3u1fbpfcp-watermark.image) # 實踐開始 此刻已經是萬事具備,只差實踐了。上面的優化方案在實際的專案中效果如何,一起來看看吧。 ## 縮短打包時長 首先我們利用`speed-measure-webpack-plugin`對整個專案做一個打包時長分析。 ![打包耗時資訊](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/56911706727f47a6beb3c86e9a6c6b16~tplv-k3u1fbpfcp-zoom-1.image) 這圖雖然內容不全,但是重要的部分已經給大家展示出來了。通過這個耗時分析工具輸出的日誌資訊,我們能很清晰的看到整個打包耗時`50`秒,其中`UglifyJsPlugin`就執行了長達了`33`秒的時間,其他相對比較耗時的就是各種`loader`的執行。 > 關於`縮短打包時長`,後一項的優化都是在前面一項優化基礎上進行的,所以整體打包時間會不斷縮短。 #### `1.使用webpack-parallel-uglify-plugin代替uglifyjs-webpack-plugin` 根據前面的分析我們急需優化的第一個點就是使用`webpack-parallel-uglify-plugin`代替`uglifyjs-webpack-plugin`外掛,將`js`程式碼的壓縮變成多執行緒。 將`js`程式碼擴充套件成多執行緒壓縮以後,在進行打包。 ![將JS程式碼壓縮擴充套件為多執行緒](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c2b72a199ab544cd89587b8c7e45f17d~tplv-k3u1fbpfcp-zoom-1.image) 這個效果真的算是非常明顯了,整體的打包時間由`50秒 -> 36秒`;`JS`程式碼的壓縮也由`33秒 -> 15秒`,幾乎節省了`50%`的時間。 #### `2.開啟快取` 接下來的一個優化點就是`開啟快取`,前面介紹了`三種`開啟快取的方式,只不過在本專案的實際應用中,只有`hard-source-webpack-plugin`這種方式效果比較明顯,其餘兩種幾乎沒有效果。 配置了`hard-source-webpack-plugin`以後,第一次打包所耗費的時長基本不會發生變化,還是上一步我們優化後的`30s`。 ![配置快取後第一次打包](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/fb1d419bcd354781b1462194dcdc07ea~tplv-k3u1fbpfcp-zoom-1.image) 它的作用會在發生在下一次打包時。 ![配置快取後第二次打包](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/1ad42750b35740cf8cee72c35d52ebb1~tplv-k3u1fbpfcp-zoom-1.image) 配置`hard-source-webpack-plugin`後第一次打包時長沒有發生變化是因為此時還沒有`快取檔案`,第一次打包完成後才會生成`快取檔案`;之後第二次在進行打包,直接讀取`快取檔案`,整體時間明顯縮短;而且通過第二次打包的時長分析結果可以看到已經沒有`loader`的耗時分析,也說明了本次打包是直接從快取中讀取的結果。 上面測試的`第二次打包`是在第一次的打包基礎之上且並且沒有改動程式碼。那實際開發時,我們大多數都是對程式碼做了修改瞭然後再次打包,那這種情況下快取對打包時長的影響又是如何呢?我們來試一試便知。 在此我隨意修改了兩個`.vue`檔案,分別給其增加了一行程式碼,然後在進行打包。 ![檔案發生修改後再次打包](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/443aa257c6564094a181bcf5121deeeb~tplv-k3u1fbpfcp-zoom-1.image) 檔案修改以後,對應的`快取檔案`就會失效,因此我們看到對應`loader`重新執行,整體的打包時間也有所增加,不過總體來說,該外掛是可以有效縮短打包時長的。 #### `3.開啟多執行緒` 前面我們說過多執行緒是通過將`webpack`中`loader`的執行過程從`單執行緒`擴充套件到`多執行緒`。因為前面我們開啟了快取,`loader`的執行時間已經非常之短,所以在`開啟快取`的基礎上在`開啟多執行緒`基本是沒有什麼效果的,事實證明也是如此。 因此在這一步我將快取關掉,使用`happypack`分別對`babel-loader`和`css-loader`開啟了多執行緒,但是最終打包時長並沒有太大變化,還是維持在`30s`。 > `開啟多執行緒`這個優化方案在本專案中並沒有很明顯的效果,可能源於專案本身`loader`處理時間就不長。即使開啟了多執行緒,`執行緒`之間的通訊以及`執行緒`最後的彙總耗時和單執行緒處理耗時是一樣的。 #### `4.動態連結庫` 本次我用`DLLPlugin`將`echarts`和`element`這兩個元件進行了分離。 ```javascript // webpack.dll.config.js module.exports = { // 入口檔案 entry: { echarts: ['echarts'], element: ['element-ui'] }, // 其餘程式碼省略 } ``` 最後在進行打包,打包時長明顯降低。 ![配置DLL,打包整體時長由17s->6s](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/95df494cc64d436294d823db90f15890~tplv-k3u1fbpfcp-zoom-1.image) 最後關於`DLL`的配置在實踐時,發現有兩點特別需要注意: 第一個就是`webpack.dll.config.js`中的`resolve`配置項,其實在剛開始的時候,參照網上的一些配置對`element-ui`這個外掛進行了分離,最後對整個專案進行打包部署後發現`element-ui`元件的`table`無法渲染。 ![table無法渲染](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/9f1f7fc384144b019035aea4f53fc483~tplv-k3u1fbpfcp-zoom-1.image) 經過一番搜尋,發現很多人在`element-ui`的`github`上提了很多相關的`issue`,說自己使用`DLLPlugin`分離了`element-ui`以後表格不渲染、`tooltip`控制元件失效。不過官方基本上都說不是`element-ui`本身的問題並且將`issue`至為`closed`。 ![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c8cd419498064fa89af304a2af5ea61b~tplv-k3u1fbpfcp-zoom-1.image) 最後翻了翻這些`issue`,按照其中的一個辦法添加了`resolve`配置後發現問題得以解決。 第二點需要注意其實在前面已經說過了,就是我們需要在`index.html`入口模板中手動引入分離出來的`第三方外掛`,同時`生產環境`下還需要將分離出來的`外掛程式碼`複製到`webpack`打包`輸出目錄`下,專案部署後才能正常執行。 #### `5.總結` 到此,關於縮短打包時長這方面的優化基本完成了,我們總共嘗試了`4`種方案,最終將打包時長由最初的`50s -> 6s`,可見效果還是非常明顯的。 ![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/56fa1b6372bf41b6b49e1eeaf3f04032~tplv-k3u1fbpfcp-watermark.image) ## 降低打包後的檔案體積 在優化之前我們依然是使用`webpack-bundle-analyzer`對打包後的`檔案體積`進行分析。 ![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/82b0b7d2eb07451883298a9f5cd42db4~tplv-k3u1fbpfcp-zoom-1.image) 這裡我挑出來兩個具有代表性的結果截圖給大家,一個是入口檔案`main.js`,裡面引入的體積較大的`模組`是`element-ui`的核心檔案`element-ui.common.js`和`vue`核心檔案`vue.esm.js`;另一個是`total.js`,該模組是引入了體積較大的`echarts`檔案。 #### `1.壓縮檔案` 前面我們介紹了對`js`和`images`進行壓縮可以減少檔案體積,在本專案中已經配置了`webpack-parallel-uglify-plugin`對`js`程式碼進行壓縮,所以這裡我們僅僅嘗試對`image`圖片進行壓縮。 配置`image-webpack-loader`以後,再次打包會很驚奇的發現並不是所有的圖片體積都會減少,有些圖片的體積反正變大了。 > 對於該異常結果並沒有在深入研究,所以暫時判定該項優化方案對本專案無效。 #### `2.抽離第三方庫` 根據前面的分析,如果對應的檔案體積減少,最直接的方式就是將`vue`、`echarts`、`element-ui`這些些體積較大的第三方庫用`CommonsChunkPlugin`抽離出來。 分離出來以後,`main.js`和`total.js`的檔案體積明顯下降:`main.js`由`1.5MB -> 755kB`;`total.js`從`819kB->29kB`。 ![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/3508e0812b684edd9982cfa17ff058a9~tplv-k3u1fbpfcp-zoom-1.image) 但是分離出來的`vendor.js`體積達到了`1.56MB`。 ![](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/4c984d4cadbc4df486fed03a979dc4db~tplv-k3u1fbpfcp-watermark.image) #### `3.動態連結庫` `動態連結庫`在前面實際上歸類到了`縮短打包時長`,但實際上它除了能有效的縮短打包時長,還可以將第三方庫分離到不同的檔案,同時也解決了`CommonsChunkPlugin`出現的問題。 這次我們使用`DLLPlugin`將`vue`、`echarts`、`element`這個三個外掛進行分離。 ```javascript // webpack.dll.config.js module.exports = { // 入口檔案 entry: { echarts: ['echarts'], element: ['element-ui'], vue: ["vue"], }, // 其餘程式碼省略 } ``` 分離出來的三個外掛: ![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/dc4526d6283d485abadf58a41d98fc3e~tplv-k3u1fbpfcp-zoom-1.image) 之後在進行打包,`main.js`的大小從`1.5MB`降低到`800kB`,其餘引用到`echarts`外掛的檔案體積也由原來的`幾百kB`降低到`十幾kB`。 # 總結 到此,本次關於`webpack`的打包優化實踐就完成了,整體的打包時間是大大降低;對一些體積較大的檔案進行了分離,也有效降低了檔案的大小;但是也有一些優化方案在本專案中沒有很明顯的效果,甚至有些`適得其反`,至於原因當下也沒有仔細去研究。 本篇文章介紹的一些`優化方案`可能並不全,而且大都適用於`webpack v3`,`wekpack v4`在很多時候已經預設開啟一些優化方案,所以大家理性參考。後期有機會的話會嘗試將專案的`webpack`版本進行升級,到時候在來總結分享。 同時,如果是真實的專案優化,所有的優化方案不能只關注`打包時長`是否降低或者`檔案體積`是否減小,每一個`優化方案`實踐完成以後還需要在`開發環境`和`生成環境`中對專案進行簡單測試,如果專案執行正常才能說明此項優化方案是成功的。比如前面我們實踐的`DLL`優化方案,配置完成以後如果只關注`打包時間`和`檔案體積`可能會沾沾自喜,但實則將專案部署到伺服器以後發現專案根本無法執行。 最後,若對本篇文章有疑問或者發現錯誤之處,還望指出,共同進步。 # 近期文章 [JavaScript的執行上下文,真沒你想的那麼難](https://juejin.cn/post/6901107803696398349) [骨架屏(page-skeleton-webpack-plugin)初探](https://juejin.cn/post/6885535026184716295) # 文末 如果這篇文章有幫助到你,❤️關注+點贊+收藏+評論+轉發❤️鼓勵一下作者 文章`公眾號首發`,關注 [`不知名寶藏女孩`](https://mmbiz.qpic.cn/mmbiz_gif/I4j8PCMjMMhl5J9MoqaIsAAeJVfMqYibiaJWpspxGicRiczx0xib35DLPlXvOd6amGPoLxLfnbERpC5TIPDgFBwc8gQ/640?wx_fmt=gif&tp=webp&wxfrom=5&wx_lazy=1) 第一時間獲取最新的文章 筆芯❤️~ ![](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/53d425d131ee42a29f0e3650d2b7a74e~tplv-k3u1fbpfcp-watermark.image)