webpack4.x 效能優化

慕晨同學發表於2019-02-06

文章首發於:github.com/USTB-musion…

寫在前面

webpack可以說是當下最流行的打包庫,當webpack處理應用程式時,它會遞迴地構建一個依賴關係圖,其中包含應用程式需要的每一個模組,然後將所有這些模組打包成一個或多個bundle。這篇文章將介紹webpack非常重要的一部分——效能優化。文章的程式碼的在此: github.com/USTB-musion…

本文將從以下幾部分進行總結:

  1. noParse
  2. ignorePlugin
  3. dllPlugin
  4. happypack
  5. Tree-Shaking
  6. 抽離公共程式碼
  7. 懶載入
  8. 熱更新

noParse

  • noParse 配置項可以讓 Webpack 忽略對部分沒采用模組化的檔案的遞迴解析和處理,這樣做的好處是能提高構建效能。 原因是一些庫例如 jQuery 、ChartJS 它們龐大又沒有采用模組化標準,讓 Webpack 去解析這些檔案耗時又沒有意義。

啟用noParse:

  module: {
    // 不去解析jquery的依賴關係
    noParse: /jquery/
  },
複製程式碼

ignorePlugin

  • moment 2.18 會將所有本地化內容和核心功能一起打包)。可以使用 IgnorePlugin 在打包時忽略本地化內容,經過實驗,使用 ignorePlugin 之後 ? 之後的體積由 1.2M 降低至 800K

ignorePlugin啟用方法:

// 用法:
new webpack.IgnorePlugin(requestRegExp, [contextRegExp]);

//eg.
plugins: [new webpack.IgnorePlugin(/\.\/local/, /moment/)];
複製程式碼

DllPlugin

  • DllPlugin 是基於 Windows 動態連結庫(dll)的思想被創作出來的。這個外掛會把第三方庫單獨打包到一個檔案中,這個檔案就是一個單純的依賴庫。這個依賴庫不會跟著你的業務程式碼一起被重新打包,只有當依賴自身發生版本變化時才會重新打包。

用 DllPlugin 處理檔案,要分兩步走:

  • 基於 dll 專屬的配置檔案,打包 dll 庫
let path = require("path");
let webpack = require("webpack");

module.exports = {
  mode: "development",
  entry: {
    react: ["react", "react-dom"]
  },
  output: {
    filename: "_dll_[name].js", // 產生的檔名
    path: path.resolve(__dirname, "dist"),
    library: "_dll_[name]"
  },
  plugins: [
    // name要等於library裡的name
    new webpack.DllPlugin({
      name: "_dll_[name]",
      path: path.resolve(__dirname, "dist", "manifest.json")
    })
  ]
};
複製程式碼
  • 基於 webpack.config.js 檔案,打包業務程式碼
let path = require("path");
let HtmlWebpackPlugin = require("html-webpack-plugin");
let webpack = require("webpack");

module.exports = {
  mode: "development",
  // 多入口
  entry: {
    home: "./src/index.js"
  },
  devServer: {
    port: 3000,
    open: true,
    contentBase: "./dist"
  },
  module: {
    // 不去解析jquery的依賴關係
    noParse: /jquery/,
    rules: [
      {
        test: /\.css$/,
        use: ["style-loader", "css-loader"]
      },
      {
        test: /\.js$/,
        exclude: /node_modules/,
        include: path.resolve("src"),
        use: {
          loader: "babel-loader",
          options: {
            presets: ["@babel/preset-env", "@babel/preset-react"]
          }
        }
      }
    ]
  },
  output: {
    // name -> home a
    filename: "[name].js",
    path: path.resolve(__dirname, "dist")
  },
  plugins: [
    new webpack.DllReferencePlugin({
      manifest: path.resolve(__dirname, "dist", "manifest.json")
    }),
    new webpack.IgnorePlugin(/\.\/local/, /moment/),
    new HtmlWebpackPlugin({
      template: "./src/index.html",
      filename: "index.html"
    }),
    new webpack.DefinePlugin({
      DEV: JSON.stringify("production")
    })
  ]
};
複製程式碼

Happypack——將 loader 由單程式轉為多程式

  • 大家知道,webpack 是單執行緒的,就算此刻存在多個任務,你也只能排隊一個接一個地等待處理。這是 webpack 的缺點,好在我們的 CPU 是多核的,Happypack 會充分釋放 CPU 在多核併發方面的優勢,幫我們把任務分解給多個子程式去併發執行,大大提升打包效率。

happypack的使用方法:

將loader中的配置轉移到happypack中就好:

let path = require("path");
let HtmlWebpackPlugin = require("html-webpack-plugin");
let webpack = require("webpack");
// 模組 happypack 可以實現多執行緒?
let Happypack = require("happypack");

module.exports = {
  mode: "development",
  // 多入口
  entry: {
    home: "./src/index.js"
  },
  devServer: {
    port: 3000,
    open: true,
    contentBase: "./dist"
  },
  module: {
    // 不去解析jquery的依賴關係
    noParse: /jquery/,
    rules: [
      {
        test: /\.css$/,
        use: "Happypack/loader?id=css"
      },
      {
        test: /\.js$/,
        exclude: /node_modules/,
        include: path.resolve("src"),
        use: "Happypack/loader?id=js"
      }
    ]
  },
  output: {
    // name -> home a
    filename: "[name].js",
    path: path.resolve(__dirname, "dist")
  },
  plugins: [
    new Happypack({
      id: "css",
      use: ["style-loader", "css-loader"]
    }),
    new Happypack({
      id: "js",
      use: [
        {
          loader: "babel-loader",
          options: {
            presets: ["@babel/preset-env", "@babel/preset-react"]
          }
        }
      ]
    }),
    new webpack.DllReferencePlugin({
      manifest: path.resolve(__dirname, "dist", "manifest.json")
    }),
    new webpack.IgnorePlugin(/\.\/local/, /moment/),
    new HtmlWebpackPlugin({
      template: "./src/index.html",
      filename: "index.html"
    }),
    new webpack.DefinePlugin({
      DEV: JSON.stringify("production")
    })
  ]
};
複製程式碼

Tree-Shaking

  • 基於 import/export 語法,Tree-Shaking 可以在編譯的過程中獲悉哪些模組並沒有真正被使用,這些沒用的程式碼,在最後打包的時候會被去除。適合於處理模組級別的程式碼,所以儘量使用es6的import/export語法。

抽離公共程式碼

把公共程式碼抽離出來的好處:

  • 減少網路傳輸流量,降低伺服器成本;
  • 雖然使用者第一次開啟網站的速度得不到優化,但之後訪問其它頁面的速度將大大提升。

啟用抽離程式碼:

  // webpack 4.x版本之前的commonChunkPlugins
  optimization: {
    // 分割程式碼塊
    splitChunks: {
      // 快取組
      cacheGroups: {
        // 公共模組
        common: {
          chunks: "initial",
          minSize: 0,
          // 最小公用模組次數
          minChunks: 2
        },
        vendor: {
          priority: 1,
          // 抽離出來
          test: /node_modules/,
          chunks: "initial",
          minSize: 0,
          minChunks: 2
        }
      }
    }
  }
複製程式碼

按需載入

按需載入的思想

  • 一次不載入完所有的檔案內容,只載入此刻需要用到的那部分(會提前做拆分)
  • 當需要更多內容時,再對用到的內容進行即時載入

通過 es6 的 import 實現按需載入,在使用 import() 分割程式碼後,你的瀏覽器並且要支援 Promise API 才能讓程式碼正常執行, 因為 import() 返回一個 Promise,它依賴 Promise。對於不原生支援 Promise 的瀏覽器,你可以注入 Promise polyfill。

let button = document.createElement("button");

button.innerHTML = "musion";

// vue,react的懶載入原理也是如此
button.addEventListener("click", function() {
  // es6草案中的語法, jsonp實現動態載入檔案
  import("./source.js").then(data => {
    console.log(data.default);
  });
  console.log("click");
});

document.body.appendChild(button);
複製程式碼

熱更新

模組熱替換(HMR - Hot Module Replacement)是 webpack 提供的最有用的功能之一。它允許在執行時替換,新增,刪除各種模組,而無需進行完全重新整理重新載入整個頁面,其思路主要有以下幾個方面:

  • 保留在完全重新載入頁面時丟失的應用程式的狀態
  • 只更新改變的內容,以節省開發時間
  • 調整樣式更加快速,幾乎等同於就在瀏覽器偵錯程式中更改樣式

啟用HRM

  1. 引入了webpack庫
  2. 使用了new webpack.HotModuleReplacementPlugin()
  3. 設定devServer選項中的hot欄位為true
let path = require("path");
let HtmlWebpackPlugin = require("html-webpack-plugin");
let webpack = require("webpack");

module.exports = {
  mode: "production",
  // 多入口
  entry: {
    index: "./src/index.js",
    other: "./src/other.js"
  },
  devServer: {
    // 啟用熱更新
    hot: true,
    port: 3000,
    open: true,
    contentBase: "./dist"
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        include: path.resolve("src"),
        use: {
          loader: "babel-loader",
          options: {
            presets: ["@babel/preset-env", "@babel/preset-react"],
            plugins: ["@babel/plugin-syntax-dynamic-import"]
          }
        }
      }
    ]
  },
  output: {
    // name -> home a
    filename: "[name].js",
    path: path.resolve(__dirname, "dist")
  },
  plugins: [
    new webpack.NamedModulesPlugin(), // 列印更新的模組路徑
    new webpack.HotModuleReplacementPlugin() // 熱更新外掛
  ]
};
複製程式碼

參考文章

Webpack 如何實現熱更新?

前端效能優化原理與實踐

相關文章