記一次真實的webpack優化經歷

小土豆biubiubiu發表於2020-12-22

前言

公司目前現有的一款產品是使用vue v2.0框架實現的,配套的打包工具為webpack v3.0。整個專案大概有80多個vue檔案,也算不上什麼大型專案。

只不過每次頭疼的就是打包所耗費的時間平均在一分鐘左右,而且打包後有幾個檔案顯示為【big】,也就是檔案體積過大。

最近就想著搗鼓一下,看能不能在此前的基礎上做一些優化,順帶記錄下來分享給大家。

webpack打包優化

關於webpack的打包優化一般會從兩個方面考慮:縮短打包時長降低打包後的檔案體積,這兩個方面也剛好是前面我需要解決的問題。

所以我們先來了解一下這兩個方面各自有什麼具體的實現方式。

縮短打包時長

我們都知道webpack的執行流程就像一條生產線一樣,在這條生產線上會按順序的執行每一個流程。那很顯然如果每一個流程要乾的事情越少或者每一個流程有多個人來共同完成,那webpack打包的執行效率就會提高。

1.減少loader搜尋檔案範圍

我們可以通過配置loaderexclude選項,告訴對應的loader可以忽略某個目錄;或者通過配置loaderinclude選項,告訴loader只需要處理指定的目錄。因為loader處理的檔案越少,執行速度就會更快。

一般比較常見的就是給babel-loader配置exclude選項。

// 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-loadercacheDirectory選項,針對babel-loader開啟快取。

// 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中進行配置:

// 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;最基礎的配置如下:

// 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

以上三種開啟快取的方式雖然各不相同,但只要做了配置就可以在我們的磁碟中看到它們的快取結果。

3.多執行緒

多執行緒也就是將一件事情交給多個人去做,從理論上來講是可以提高一件事情的完成效率。

◕ happyhack

我們都知道受限於node單執行緒模式,webpack的整個執行構建過程也是單執行緒模式的。

所以第一種開啟多執行緒的方式就是將webpackloader的執行過程從單執行緒擴充套件到多執行緒。這種方式的具體實現依賴的是HappyPack外掛。

使用happypack的第一步依然是安裝:npm install --save-dev happypack;最簡單的配置如下:

// 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原始碼將被傳遞給HappyPackHappyPack將使用loaders指定的載入器(本例中是babel-loader)並行地轉換它們。

這種最基礎的配置,預設是3個執行緒並行處理。同時我們也可以通過配置thread選項,自定義執行緒個數。

// 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外掛,那麼最好自己建立一個執行緒池,然後配置這些外掛來共享該池,從而最大限度地減少其中執行緒的空閒時間。)

執行緒池的建立也很簡單:

// webpack.config.js
const happyThreadPool = HappyPack.ThreadPool({ size: 4 });

除了可以通過上面的這種方式建立具體的執行緒數,也可以根據CPU的核數建立:

// webpack.config.js
const os = require('os');
const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length-1 });

執行緒池建立好了以後,通過threadPool進行指定共享執行緒池

// webpack.config.js

// 此處省略一些程式碼

new HappyPack({
    // re-add the loaders you replaced above in #1:
    loaders: 'babel-loader',
    // 使用共享執行緒池
    threadPool: happyThreadPool
})

最後一個實用的配置項是verbose選項,可以讓happypack輸出執行日誌:

// 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上面宣佈他本人不會在對該專案進行更新維護了。

◕ thread-loader

thread-loaderhappypack類似,也是通過擴充套件loader的處理執行緒來降低打包時間。安裝命令:npm install thread-loader --save-dev;最簡單的配置如下:

// 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;簡單的配置如下:

// 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專案時,會將vuevue-routeraxios等這些外掛的程式碼跟我們的程式碼打包到一個檔案中,而這些外掛的程式碼除非版本有變化,否則程式碼內容基本不會發生變化。所以每次在打包專案時,實際上都在重複打包這些外掛的程式碼,很顯然浪費了很多時間。

關於動態連結庫這個詞實際上借用的是作業系統中的動態連結庫概念,webpack的具體實現也就是把前面我們描述的那些外掛分別打包成一個獨立的檔案。當有模組需要引用該外掛時會通過生成的json檔案連結到對應的外掛。這樣不管是我們在開發環境還是在生成環境下的打包構建,都不需要在對這些外掛做重複的處理。那接下來我們看看動態連結庫的配置和使用。

首先我們需要新建一個webpack.dll.config.js,該檔案本身是一個webpack配置檔案,主要用於分離第三方外掛。

// 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指令碼。

// 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作為配置檔案執行打包任務。

指令碼執行成功以後,本地已經生成了對應的目錄和檔案。

其中echarts.dll.2a6026f8.js就是我們分離出來的echarts外掛,echarts-manifest.json就是前面我們說的json檔案。

第三方庫檔案分離後,當有模組需要引用echarts時,該如何引用到對應的echarts.dll.2a6026f8.js檔案呢?

此時就需要DllReferencePlugin出場了:通過配置DllReferencePluginmanifest檔案來把依賴的模組名稱對映到對應的外掛。這一步需要在webpack.config.js中進行配置:

// 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開啟瀏覽器會發現頁面無法正常顯示,且控制檯有報錯資訊:

這裡是因為我們還需要在入口模板檔案index.html中手動引入分離出來的外掛檔案。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title><%= htmlWebpackPlugin.options.title %></title>
  </head>
  <body>
    <div id="app"></div>
    <!-- 手動引入 --> 
    <script type="text/javascript" src="./webpackDLL/echarts-dll.2a6026f9.js"></script>
  </body>
</html>

之後在重新整理頁面就沒有問題了。

這裡需要特別注意,開發環境中手動引入對應外掛的路徑為./webpackDLL/*.2a6026f9.js,此時如果對專案進行打包部署,打包後index.html引用的依然是./webpackDLL/*.2a6026f9.js,很顯然單是在本地環境中該資源的引用路徑就是錯誤的;更甚之專案打包後的輸出路徑一般都會單獨配置,比如dist目錄,部署時也只會部署該目錄下的檔案。

所以僅僅是前面的配置,專案部署以後根本無法正常執行。

解決這個問題很顯然有一個簡單粗暴的方式:index.html中引入的路徑依然不變,打包後的程式碼依然在dist目錄下,只是打包完成後手動將對應的webpackDLL外掛目錄以及檔案複製到dist目錄下,這樣直接將dist目錄部署到伺服器即可正常執行。

除了這種方式之外,我們完全可以藉助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的最基礎的配置如下:

// 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. 抽離第三方庫

CommonsChunkPluginwebpack官方提供的一個外掛,通過配置這個外掛,可以將公共的模組抽離出來。

webpack v4已經不再支援該外掛,使用SplitChunksPlugin代替。但由於本專案使用的是webpack v3,因此這裡不對SplitChunksPlugin做介紹。

首先我們需要在webpackentry選項對我們需要分離的公共模組進行配置。

module.exports = {
    entry: {
        main: ["./src/main.js"], //原有的入口檔案
        vender: ['echarts']     // 表示將echarts模組分離出來
    },
}

接著需要在plugin中配置這個公共模組的輸出:

module.exports = {
    plugins:[
        new webpack.optimize.CommonsChunkPlugin({
            name: 'vender',  // 對應entry中配置的入口vender
            filename: '[name].js'  // 分離出來的模組以name選項作為檔名,也就是vender.js
        })
    ]
}

配置完成後對專案進行打包,就會看到打包結果中多出一個名為vendor.js的檔案。

此時我們可以去檢視有使用過echarts的元件被打包後的js檔案體積明顯減少。

如果我們還需要繼續分離其他的一些公共模組,可以在entry中繼續配置:

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配置檔案中進行配置:

// 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的解釋如下:

官方文件有說明在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外掛,用於測量打包的速度,並輸出打包過程中每一個loaderplugin的執行時間。

首先是對其進行安裝:npm install speed-measure-webpack-plugin;接著在webpack.config.js中進行配置:

// 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,就能看到該外掛輸出的打包時長資訊。

從這些資訊裡面我們能很清楚看到每一個pluginloader所花費的時長,這樣我們就可以針對耗費時間較長的部分進行優化。

2.webpack-bundle-analyzer

webpack-bundle-analyzer也是一個webpack外掛,用於建立一個互動式的樹形圖視覺化所有打包後的檔案,包括檔案的體積和檔案裡面包含的內容

它的使用也非常簡單,首先是安裝:npm install webpack-budle-analyzer;安裝完成後,只需要在webpack的配置檔案中寫入如下內容:

// webpack.config.js
// 引入 
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; 

module.exports = {
  plugins: [
    new BundleAnalyzerPlugin()
  ]
}

之後我們執行npm run build,打包結束以後webpack會輸出如下日誌。

接著會預設彈出瀏覽器視窗,開啟http://127.0.0.1:8888/

圖片來源於官網

若沒有自動開啟,可以手動輸入地址。同時需要注意的是該外掛預設啟動在8888埠上,假如出現埠占用情況,可以對預設的埠進行配置,詳情可參考:https://www.npmjs.com/package/webpack-bundle-analyzer

從頁面中我們可以清楚的看到每一個檔案的大小,同時還可以看到該檔案中引入了那些模組、每一個模組檔案大小。根據這些內容,我們就可以有針對性的處理一些大檔案和這些大檔案中一些體積較大的模組

總結

到此我們已經列舉了很多具體的webpack優化方案和每一種優化方案的簡單配置。接下來我們會將這些方案應用到實際的專案中,在實踐開始之前我們先對前面的內容簡單做一個回顧和總結。

實踐開始

此刻已經是萬事具備,只差實踐了。上面的優化方案在實際的專案中效果如何,一起來看看吧。

縮短打包時長

首先我們利用speed-measure-webpack-plugin對整個專案做一個打包時長分析。

打包耗時資訊

這圖雖然內容不全,但是重要的部分已經給大家展示出來了。通過這個耗時分析工具輸出的日誌資訊,我們能很清晰的看到整個打包耗時50秒,其中UglifyJsPlugin就執行了長達了33秒的時間,其他相對比較耗時的就是各種loader的執行。

關於縮短打包時長,後一項的優化都是在前面一項優化基礎上進行的,所以整體打包時間會不斷縮短。

1.使用webpack-parallel-uglify-plugin代替uglifyjs-webpack-plugin

根據前面的分析我們急需優化的第一個點就是使用webpack-parallel-uglify-plugin代替uglifyjs-webpack-plugin外掛,將js程式碼的壓縮變成多執行緒。

js程式碼擴充套件成多執行緒壓縮以後,在進行打包。

將JS程式碼壓縮擴充套件為多執行緒

這個效果真的算是非常明顯了,整體的打包時間由50秒 -> 36秒;JS程式碼的壓縮也由33秒 -> 15秒,幾乎節省了50%的時間。

2.開啟快取

接下來的一個優化點就是開啟快取,前面介紹了三種開啟快取的方式,只不過在本專案的實際應用中,只有hard-source-webpack-plugin這種方式效果比較明顯,其餘兩種幾乎沒有效果。

配置了hard-source-webpack-plugin以後,第一次打包所耗費的時長基本不會發生變化,還是上一步我們優化後的30s

配置快取後第一次打包

它的作用會在發生在下一次打包時。

配置快取後第二次打包

配置hard-source-webpack-plugin後第一次打包時長沒有發生變化是因為此時還沒有快取檔案,第一次打包完成後才會生成快取檔案;之後第二次在進行打包,直接讀取快取檔案,整體時間明顯縮短;而且通過第二次打包的時長分析結果可以看到已經沒有loader的耗時分析,也說明了本次打包是直接從快取中讀取的結果。

上面測試的第二次打包是在第一次的打包基礎之上且並且沒有改動程式碼。那實際開發時,我們大多數都是對程式碼做了修改瞭然後再次打包,那這種情況下快取對打包時長的影響又是如何呢?我們來試一試便知。

在此我隨意修改了兩個.vue檔案,分別給其增加了一行程式碼,然後在進行打包。

檔案發生修改後再次打包

檔案修改以後,對應的快取檔案就會失效,因此我們看到對應loader重新執行,整體的打包時間也有所增加,不過總體來說,該外掛是可以有效縮短打包時長的。

3.開啟多執行緒

前面我們說過多執行緒是通過將webpackloader的執行過程從單執行緒擴充套件到多執行緒。因為前面我們開啟了快取,loader的執行時間已經非常之短,所以在開啟快取的基礎上在開啟多執行緒基本是沒有什麼效果的,事實證明也是如此。

因此在這一步我將快取關掉,使用happypack分別對babel-loadercss-loader開啟了多執行緒,但是最終打包時長並沒有太大變化,還是維持在30s

開啟多執行緒這個優化方案在本專案中並沒有很明顯的效果,可能源於專案本身loader處理時間就不長。即使開啟了多執行緒,執行緒之間的通訊以及執行緒最後的彙總耗時和單執行緒處理耗時是一樣的。

4.動態連結庫

本次我用DLLPluginechartselement這兩個元件進行了分離。

// webpack.dll.config.js
module.exports = {
  // 入口檔案
  entry: {
    echarts: ['echarts'],
    element: ['element-ui']
  },
  // 其餘程式碼省略
}

最後在進行打包,打包時長明顯降低。

配置DLL,打包整體時長由17s->6s

最後關於DLL的配置在實踐時,發現有兩點特別需要注意:

第一個就是webpack.dll.config.js中的resolve配置項,其實在剛開始的時候,參照網上的一些配置對element-ui這個外掛進行了分離,最後對整個專案進行打包部署後發現element-ui元件的table無法渲染。

table無法渲染

經過一番搜尋,發現很多人在element-uigithub上提了很多相關的issue,說自己使用DLLPlugin分離了element-ui以後表格不渲染、tooltip控制元件失效。不過官方基本上都說不是element-ui本身的問題並且將issue至為closed

最後翻了翻這些issue,按照其中的一個辦法新增了resolve配置後發現問題得以解決。

第二點需要注意其實在前面已經說過了,就是我們需要在index.html入口模板中手動引入分離出來的第三方外掛,同時生產環境下還需要將分離出來的外掛程式碼複製到webpack打包輸出目錄下,專案部署後才能正常執行。

5.總結

到此,關於縮短打包時長這方面的優化基本完成了,我們總共嘗試了4種方案,最終將打包時長由最初的50s -> 6s,可見效果還是非常明顯的。

降低打包後的檔案體積

在優化之前我們依然是使用webpack-bundle-analyzer對打包後的檔案體積進行分析。

這裡我挑出來兩個具有代表性的結果截圖給大家,一個是入口檔案main.js,裡面引入的體積較大的模組element-ui的核心檔案element-ui.common.jsvue核心檔案vue.esm.js;另一個是total.js,該模組是引入了體積較大的echarts檔案。

1.壓縮檔案

前面我們介紹了對jsimages進行壓縮可以減少檔案體積,在本專案中已經配置了webpack-parallel-uglify-pluginjs程式碼進行壓縮,所以這裡我們僅僅嘗試對image圖片進行壓縮。

配置image-webpack-loader以後,再次打包會很驚奇的發現並不是所有的圖片體積都會減少,有些圖片的體積反正變大了。

對於該異常結果並沒有在深入研究,所以暫時判定該項優化方案對本專案無效。

2.抽離第三方庫

根據前面的分析,如果對應的檔案體積減少,最直接的方式就是將vueechartselement-ui這些些體積較大的第三方庫用CommonsChunkPlugin抽離出來。

分離出來以後,main.jstotal.js的檔案體積明顯下降:main.js1.5MB -> 755kBtotal.js819kB->29kB

但是分離出來的vendor.js體積達到了1.56MB

3.動態連結庫

動態連結庫在前面實際上歸類到了縮短打包時長,但實際上它除了能有效的縮短打包時長,還可以將第三方庫分離到不同的檔案,同時也解決了CommonsChunkPlugin出現的問題。

這次我們使用DLLPluginvueechartselement這個三個外掛進行分離。

// webpack.dll.config.js
module.exports = {
  // 入口檔案
  entry: {
    echarts: ['echarts'],
    element: ['element-ui'],
    vue: ["vue"],
  },
  // 其餘程式碼省略
}

分離出來的三個外掛:

之後在進行打包,main.js的大小從1.5MB降低到800kB,其餘引用到echarts外掛的檔案體積也由原來的幾百kB降低到十幾kB

總結

到此,本次關於webpack的打包優化實踐就完成了,整體的打包時間是大大降低;對一些體積較大的檔案進行了分離,也有效降低了檔案的大小;但是也有一些優化方案在本專案中沒有很明顯的效果,甚至有些適得其反,至於原因當下也沒有仔細去研究。

本篇文章介紹的一些優化方案可能並不全,而且大都適用於webpack v3wekpack v4在很多時候已經預設開啟一些優化方案,所以大家理性參考。後期有機會的話會嘗試將專案的webpack版本進行升級,到時候在來總結分享。

同時,如果是真實的專案優化,所有的優化方案不能只關注打包時長是否降低或者檔案體積是否減小,每一個優化方案實踐完成以後還需要在開發環境生成環境中對專案進行簡單測試,如果專案執行正常才能說明此項優化方案是成功的。比如前面我們實踐的DLL優化方案,配置完成以後如果只關注打包時間檔案體積可能會沾沾自喜,但實則將專案部署到伺服器以後發現專案根本無法執行。

最後,若對本篇文章有疑問或者發現錯誤之處,還望指出,共同進步。

近期文章

JavaScript的執行上下文,真沒你想的那麼難
骨架屏(page-skeleton-webpack-plugin)初探

文末

如果這篇文章有幫助到你,❤️關注+點贊+收藏+評論+轉發❤️鼓勵一下作者

文章公眾號首發,關注 不知名寶藏女孩 第一時間獲取最新的文章

筆芯❤️~

相關文章