webpack--效能優化之打包構建速度和程式碼除錯優化

丶Serendipity丶發表於2022-03-11

前言

  本文來總結寫webpack 在效能方面常見的優化方案。

正文

  本文分別總結開發環境和生產環境中在打包構建速度和程式碼除錯功能方面的優化方案,如下:

  1、開發環境效能優化

  (1)優化打包構建速度

  a、HMR: hot module replacement ,熱模組替換,作用:當一個模組發生變化的時候,只會重新打包發生變化的模組,並不會打包所有模組,極大的提升了程式碼構建速度。

  只需要在webpack.config.js中的devserver中新增:

  devServer: {
    // contentBase: resolve(__dirname, "build"),// 這個配置在新版本的webpack中使用下面的方式配置,否則會報錯
    static: { // static: ['assets']
      directory: resolve(__dirname, "build")
    },
    // 啟用gzip壓縮
    compress: true,
    // 埠號
    port: 3000,
    open:true,// 自動開啟瀏覽器
    hot:true// 開啟HMR功能
  },

  執行 npx webpack-dev-server命令,然後修改其中的檔案,會發現控制檯提示修改的檔案,如下:

  經測試發現總結:

  樣式檔案:可以使用HRM功能,因為style-loader 內部實現了

  js檔案:預設不能使用HRM功能,(解決方法:修改js程式碼,新增支援)

  html 檔案:預設不能使用HRM 功能,同時會導致html檔案不能熱更新了(解決方法:修改入口檔案entry的引入即可,如下:)開發中不需要做

    //入口檔案
      entry: ["./src/index.js","./src/index.html"],

  (2)優化程式碼除錯效能

  source-map:是一種提供原始碼到構建後程式碼的對映技術(如果構建後程式碼出錯,通過對映關係能夠追溯到原始碼錯誤)

  只需要在webpack.config.js中新增如下:

    devtool:"eval-source-map"

  引數:[ inline -|hidden-| eval- ] [ nosources- ] [cheap- [module-]] source-map

  source-map:預設外部,能夠準確提示檔案報錯的具體位置和準確資訊

  inline-source-map:內聯。只生成一個內聯的source-map,能夠準確提示檔案報錯的具體位置和準確資訊。但是體積較大

  hidden-source-map:外部,提示錯誤程式碼資訊,不能追蹤到原始碼錯誤位置,只能提示到構建後程式碼錯誤位置。

  eval-source-map:內聯,每個檔案都會生成對應的source-map ,都在eval(),能夠準確提示檔案報錯的具體位置和準確資訊,不過多了原始碼檔名加了hash值。

  nosources-source-map:外部,能夠找到錯誤資訊,但是沒有任何原始碼錯誤位置資訊。

  cheap-source-map:外部,能夠準確提示檔案報錯的具體位置和準確資訊,只能精確到程式碼行。

  cheap-module-source-map:外部,能夠準確提示檔案報錯的具體位置和準確資訊,只能精確到程式碼行。但是module會將loader的 source-map 加入進來

  內聯和外部的區別:外部生成了檔案,而內聯沒有,內聯構建速度更快。

  使用方法: 開發環境:速度快優先,便於除錯( eval > inline > cheap > 其他),因此 eval-source-map最優

       生產環境:原始碼要不要隱藏,除錯要不要友好,nosources-source-map 或者 hidden-source-map

  2、生產環境效能優化

  (1)優化打包構建速度

  (2)優化程式碼除錯效能

  a、oneOf:在loader中配置了很多個用於處理不同的檔案,但是針對不同檔案不能全部使用多個loader,只能使用其中一個或者幾個,每次使用不需要都對loader檢查一遍,因此可以對loader中程式碼進行改造。由於對js檔案即使用了eslint-loader 又使用了babel-loader,所有把eslint-loader 提取到外部即可。如下:

 module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/, // 排除node_modules
        enforce: "pre", // 優先執行,先檢查eslint 再執行babel-loader
        loader: "eslint-loader",
        options: {
          fix: true, // 自動修復eslint錯誤
        },
      },
      {
        oneOf: [
          {
            test: /\.css$/,
            use: [
              // "style-loader", // 建立style標籤將樣式放入
              //這個loader取代style-loader ,作用是將提取js中的css成單獨的檔案
              MiniCssExtractPlugin.loader,
              "css-loader", // 將css檔案整個到js中
              /* CSS相容性處理:postcss ==>postcss-loader postcss-preset-env
            幫助postcss扎到packae.json 中browserslist裡的配置,通過配置載入指定的css相容樣式
            "browserlist":{
              // 開發環境==》設定node環境變數:process.env.NODE_ENV="development"
                "development":[
                  "last 1 chrome version",
                  "last 1 firefox version",
                  "last 1 safari version"
                ],
                // 生產環境 預設是生產環境
                "production":[
                  ">0.2%",
                  "not dead",
                  "not op_mini all"
                ]
              }*/
              // 使用loader的預設配置
              // post-loader
              // 修改loader配置
              {
                loader: "postcss-loader",
                // options:{
                // ident:"postcss",
                // plugins:()=>{
                //   require("postcss-preset-env")
                // }
                // }
              },
            ],
          },

          {
            test: /\.js$/,
            loader: "babel-loader",
            exclude: /node_modules/, // 排除node_modules
            options: {
              // 預設:指示babel做哪些相容性處理
              presets: [
                // "@babel/presets-env",
                {
                  useBuiltins: "usage", // 按需載入
                  corejs: {
                    version: 3, // 指定corejs版本
                  },
                  targets: {
                    // 指定相容到哪些瀏覽器
                    chorme: "60",
                    firefox: "60",
                    ie: "9",
                    safiri: "10",
                    edge: "17",
                  },
                },
              ],
            },
          },
          // 這種方式無法處理html檔案中的圖片
          {
            test: /\.(jpg|png|gif)$/,
            loader: "url-loader",
            options: {
              // 圖片大小小於8kb,會被解析為base64處理,優點減少請求數量減輕伺服器壓力,缺點是圖片體積更大,檔案請求速度更慢
              limit: 8 * 1024,
              esModule: false,
              // 這樣可以修改圖片名稱,[hash:10]表示取hash值的前十位,[ext]表示原副檔名
              name: "img/[hash:8].[name].[ext]",
            },
          },
          {
            // 因為url-loader預設適用了es6模組解析,而html-loader 引入圖片適用commonJS處理,解析時會出現[object Module],需要關閉url-loader的es6模組化解析
            test: /\.html$/,
            // 處理html 檔案的img檔案
            loader: "html-loader",
          },
          {
            // 打包其他資源(除了css,js,html,less,json資源以外的資源)
            exclude: /\.(css|js|html|less|json)$/,
            loader: "file-loader",
          },
        ],
      },
    ],
  },

  b、快取優化

  <1>babel快取優化

  babel快取指在通過babel實現 js 相容性處理的時候,預設我們每次會對所有的js程式碼進行babel處理,而實際構建過程中,我們並不hi修改全部js的程式碼,因此只需要對我們修改的js程式碼進行babel相容性處理即可,沒有修改的可以使用快取的處理就行。

  只需要在babel的配置中新增配置,如下即可:

         {
            test: /\.js$/,
            loader: "babel-loader",
            exclude: /node_modules/, // 排除node_modules
            options: {
              //... 其他處理
              // 開啟babel快取,第二次構建時,會讀取之前的快取
              cacheDirectory: true,
            },
          },

  <2>打檔案資源的優化

  給生成的檔案新增hash值(webpack每次打包生成的唯一值),每次通過對比hash值的變化來確定是否使用快取的檔案。但是問題是:因為js和css同時使用一個hash值,重新打包,會導致所有快取失效,無法滿足快取使用。

  給生成的檔案新增 chunkhash :根據生成的hash值,如果打包來源於同一個chunk,那麼hash值就一樣。但是問題是:因為css是在js中引入的,所有同屬於一個chunk。

  給生成的檔案新增contenthash :根據檔案的內容生成的hash值,不同檔案hash值一定不一樣。只需要在filename中新增如下程式碼:

 output: {
    // 輸出檔名
    filename: "js/build.[contenthash:10]js",
    // 輸出路徑
    // __dirname 是node.js的變數,代表當前檔案的目錄的絕對路徑
    path: resolve(__dirname, "build"), // 代表輸出到當前目錄下建立的build資料夾下
  },

  c、tree shaking:去除無用程式碼(js和css)

  前提:1、使用es6 模組化 2、開啟production環境

  作用:減少打包程式碼體積

  需要在package.json 中配置如下:這樣可以把一些沒有使用的副作用檔案儲存在構建包中,比如沒有css,less檔案

  "sideEffects":[
    "*.css",
    "*.less"
  ]

  d、code split程式碼分割

  在分割之前,js檔案是打包成為一個檔案,然後在html中引入,無法實現我們的按需載入,對效能影響很大。

  (1)設定多入口,只要有一個入口,最終就會輸出一個bundle ,通過拆分入口實現生成多個bundle

  entry:{
    main:"./src/js/index.js",
    test:"./src/js/index.js"
  },

  (2)在webpack.config.js中新增如下配置,對專案依賴單獨打包

  // 可以將node_modules中程式碼單獨打包一個chunk輸出
  // 自動分析多個入口chunk中,有沒有公共的檔案,如果有會打包成一個單獨的chunk
  optimization:{
    splitChunks:{
      chunks:"all"
      }
  },

  不使用如上配置,當我們的程式碼中A.js 和 B.js 兩個檔案同時引入了 jquery 依賴的時候,都會生成對應的AB打包的檔案,這兩個檔案中就會重複存在jquery的打包檔案,這時就需要單獨分離出依賴檔案,單獨進行打包即可。

  (3)如果不想設定多個入口檔案,可以通過js程式碼將某個檔案單打包成一個chunk

import (/*webpackChunkName:"test"*/"./test.js")
.then((res)=>{
  // 檔案載入成功
  console.log(res);
})
.catch(()=>{
  console.log("檔案載入失敗");
})

  通過上面的 import動態匯入語法, test.js 檔案就會被單獨打包

  e、懶載入和預載入

  懶載入是使用上面提到的import動態匯入語法,將程式碼放在一個非同步的函式中,如下:

document.getElementById("btn").onclick = function () {
  import(/*webpackChunkName:"test"*/ "./test.js")
    .then((res) => {
      // 檔案載入成功
      console.log(res);
    })
    .catch(() => {
      console.log("檔案載入失敗");
    });
};

  在index.js 檔案中,點選btn按鈕,進行test.js 檔案 的懶載入,再次點選的時候,不會重複載入test檔案,此時會利用快取裡的test檔案即可。

  預載入prefetch,會在使用之前提前載入js檔案,在使用的時候直接在快取裡面取,程式碼如下:

document.getElementById("btn").onclick = function () {
  import(/*webpackChunkName:"test",webpackPrefetch:true*/ "./test.js")
    .then((res) => {
      // 檔案載入成功
      console.log(res);
    })
    .catch(() => {
      console.log("檔案載入失敗");
    });
};

  正常載入可以認為是並行載入,同一時間載入多個檔案,預載入prefetch是等其他資源載入完畢,瀏覽器空閒的時候取載入其他資源。極大提高效能和使用者體驗。但是預載入相容性較差,謹慎使用。

  f、PWA (Progressive Web App),即漸進式WEB應用。

  • 可以新增至主螢幕,點選主螢幕圖示可以實現啟動動畫以及隱藏位址列

  • 實現離線快取功能,即使使用者手機沒有網路,依然可以使用一些離線功能

  • 實現了訊息推送

    使用workbox技術 ===》workbox-webpack-plugin配置外掛

    首先下載workbox-webpack-plugin庫

npm i workbox-webpack-plugin -D

  在plugins中新增配置

   new WorkboxWebpackPlugin.GenerateSW({
      /**
       * 1.幫助serviceworker快速啟動
       * 2.刪除舊的serviceworker
       * 
       * 生成一個serviceworker配置檔案~
       */
      clientsClaim:true,
      skipWaiting:true
    })

  在入口檔案或者html檔案中可以新增如下程式碼註冊serviceworker

// 註冊serviceworker
// 處理相容性問題
/**
 * 1、eslint不能識別window.navigator這些變數,需要加package.json中的eslintConfig中配置
 *            "eslintConfig": {
                    "extends": "airbnb-base",
                    "env":{
                    "browser": true
                    }
                },
    2、 serviceworker 程式碼必須執行在伺服器上
                ==》通過node.js部署
                ==》npm i serve -g 安裝serve,然後serve -s build啟動伺服器,將build目錄下所有資源作為靜態資源暴露出去
 */
if("serviceWorker" in navigator){
    window.addEventListener("load",()=>{
        navigator.serviceWorker.register("/service-worker.js")// 該js檔案由外掛生成
        .then(()=>{
            console.log("註冊成功");
        })
        .catch(()=>{
            console.log("註冊失敗");
        })
    })
}

  g、thread-loader多程式打包

  安裝loader:

npm i thread-loader -D

  經常給babel-loader使用:

         {
            test: /\.js$/,
            exclude: /node_modules/, // 排除node_modules
            use: [
              {
                // 開啟多程式打包 程式開啟大概600ms,程式通訊也要花時間開銷
                loader:"thread-loader",
                options:{
                  workers:2,// 兩個程式打包
                }
              },
              {
                loader: "babel-loader",
                options: {
                  // 預設:指示babel做哪些相容性處理
                  presets: [
                    // "@babel/presets-env",
                    {
                      useBuiltins: "usage", // 按需載入
                      corejs: {
                        version: 3, // 指定corejs版本
                      },
                      targets: {
                        // 指定相容到哪些瀏覽器
                        chorme: "60",
                        firefox: "60",
                        ie: "9",
                        safiri: "10",
                        edge: "17",
                      },
                    },
                  ],
                  // 開啟babel快取,第二次構建時,會讀取之前的快取
                  cacheDirectory: true,
                },
              },
            ],
          },

  h、externals

  在webpack.cinfig.js 新增入下中配置

  externals:{
    // 拒絕jQuery 被打包進來
    jquery:"jQuery"
  }

  這樣jQuery這個包就不會用過npm的方式被打包到專案中,可以通過cdn的方式引入,比如在index.html 檔案中插入jQuery 的script標籤。通過cdn的引入,提升效能

  i、dll 對程式碼庫進行單獨打包

  建立單獨的js檔案,比如webpack.dll.js

/**
 * 使用dll技術,對某些(第三方庫:jquery、react、vue...)進行單獨打包,同時生成一個manifest.json檔案,提供了之間的對映關係
 * 當執行webpack指令 預設執行的是webpack.config.js配置檔案,需要通過webpack --config webpack.dll.js 指令執行webpack.dll.js配置檔案
 */
const { resolve } = require("path");
const webpack = require("webpakck");

module.exports = {
  entry: {
    // 最終打包生成的[name] ==>jquery
    jquery: ["jquery"],
  },
  output: {
    filename: "[name].js",
    path: resolve(__dirname, "dll"),
    library: "[name]_[hash]", // 打包的庫裡面向外暴露出去的內容叫什麼名字
  },
  plugins: [
    // 打包生成一個manifest.json 檔案(該檔案提供對映關係)
    new webpack.DllPlugin({
      name: "[name]_[hash]", // 對映庫的暴露的內容名稱
      path: resolve(__dirname, "dll/manifest.json"), // 輸出檔案路徑
    }),
  ],
  mode: "production",
};

  通過上面的指令,會將jquery單獨進行打包。

  然後在webpack.config.js 中引入新的外掛,告訴webpack不去打包哪些依賴(忽略jquery依賴)

  plugins: [
    // 告訴webpack那些庫不參與打包,同時使用時的名稱也得改變
    new webpack.DllPlugin({
      manifest:resolve(__dirname,"dll/mainfest.json")
    }),
  ],

  然後執行下面命令下載add-asset-html-webpack-plugin

npm i add-asset-html-webpack-plugin -D

  繼續如下配置(再將之前dll.js 檔案打包的jquery引入進來):

  plugins: [
    // 將某個檔案打包輸出,並在html中自動引入該資源
    new AddAssetHtmlWebpackPlugin({
      filepath:resolve(__dirname,"dll/jquery.js")
    })
  ],

  通過上面的配置優化了jquery重複打包的過程提高了效能。

寫在最後

  以上就是本文的全部內容,希望給讀者帶來些許的幫助和進步,方便的話點個關注,小白的成長之路會持續更新一些工作中常見的問題和技術點。

 

相關文章