使用Webpack4優化Web效能

一個笑點低的妹紙發表於2019-05-09

使用Webpack4優化Web效能

利用 Webpack 來優化 Web 效能屬於載入效能優化 的一部分: ☛ Web Performance Optimization with webpack

本文目錄:

  • 減少前端資源體積
  • 使用長期快取
  • 監控和分析應用程式
  • 總結

一、減少前端資源體積

1、webpack 4 開啟 production 模式

production 模式下 webpack 會對程式碼進行優化,如減小程式碼體積,刪除只在開發環境用到的程式碼。

可以在 webpack 中指定:

module.exports = {
  mode: 'production' // 或 development
};
複製程式碼

或者 package.json 中配置:

"scripts": {
    "dev": "webpack-dev-server --mode development --open --hot",
    "build": "webpack --mode production --progress"
}
複製程式碼
2、壓縮程式碼

使用 bundle-level minifier 和 loader options 壓縮程式碼。

  • Bundle-level minification

Bundle-level 的壓縮會在程式碼編譯後對整個包進行壓縮。

在 webpack 4 中,production 模式下會自動執行 bundle-level 的壓縮,底層使用了 the UglifyJS minifier。(如果不想開啟壓縮,可以採用 development 模式或者設定 optimization.minimize 為 false)

  • Loader-specific options

通過 loader 層面的選項配置來對程式碼進行壓縮,是為了壓縮 bundle-level minifier 無法壓縮的內容,比如,通過 css-loader 編譯後的檔案,會成為字串,就無法被 minifier 壓縮。因此,要進一步壓縮檔案內容,可進行如下配置:

// webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          'style-loader',
          { loader: 'css-loader', options: { minimize: true } },
        ],
      },
    ],
  },
};
複製程式碼
3、使用 ES 模組

當使用 ES 模組時, webpack 能夠進行 tree-shaking。

tree-shaking 是指 bundler 遍歷整個依賴關係樹,檢查使用了哪些依賴關係,並刪除未使用的依賴關係。因此,如果使用ES模組語法,webpack 可以消除未使用的程式碼。

★ 注意:在 webpack 中,如果沒有 minifier,tree-shaking 就無法工作。webpack 只刪除不使用的匯出語句,而 minifier 則會刪除未使用的程式碼。因此,如果在編譯時不使用 minifier,程式碼量並不會減小。(除了使用 wbpack 內建的 minifier,其它的外掛如 Babel Minify plugin 也能對程式碼進行壓縮)。

✘ 警告:不要意外地將 ES 模組編譯成 CommonJS 模組。如果你使用 Babel 的時候,採用了 babel-preset-env 或者 babel-preset-es2015,請檢查這些預置的設定。預設情況下,它們會將 ES 的匯入和匯出轉換為 CommonJS 的 requiremodule.exports,可以通過傳遞 { modules: false } 選項來禁用它。

Introduction to ES Modules一口(很長的)氣了解 babel ➹ Webpack docs about tree shaking

4、壓縮圖片資源

針對具體的依賴項進行優化(dependency-specific optimization

影像佔了頁面大小的一半以上。雖然它們不像JavaScript那樣重要(例如,它們不會阻塞呈現),但它們仍然佔用了很大一部分頻寬。在 webpack 中可以使用 url-loadersvg-url-loaderimage-webpack-loader 來優化它們。

url-loader 可以將小型靜態檔案內聯到應用程式中。如果不進行配置,它將把接受一個傳遞的檔案,將其放在已編譯的包旁邊,並返回該檔案的url。但是,如果指定 limit 選項,它將把小於這個限制的檔案編碼為Base64 資料的 url 並返回這個url,這會將影像內聯到 JavaScript 程式碼中,從而可以減少一個HTTP請求。

// webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.(jpe?g|png|gif)$/,
        loader: 'url-loader',
        options: {
          // Inline files smaller than 10 kB (10240 bytes)
          limit: 10 * 1024,
        },
      },
    ],
  }
};
複製程式碼
// index.js
import imageUrl from './image.png';
// → If image.png is smaller than 10 kB, `imageUrl` will include
// the encoded image: 'data:image/png;base64,iVBORw0KGg…'
// → If image.png is larger than 10 kB, the loader will create a new file,
// and `imageUrl` will include its url: `/2fcd56a1920be.png`
複製程式碼

★ 注意:需要在增大程式碼體積和減少 HTTP 請求數之前進行權衡。

svg-url-loader 的工作原理與 url-loader 類似 — 只是它使用的是URL編碼而不是Base64編碼來編碼檔案。這對SVG影像很有用 — 因為SVG檔案只是純文字,這種編碼更高效。

// webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.svg$/,
        loader: 'svg-url-loader',
        options: {
          // Inline files smaller than 10 kB (10240 bytes)
          limit: 10 * 1024,
          // Remove the quotes from the url
          // (they’re unnecessary in most cases)
          noquotes: true,
        },
      },
    ],
  },
};
複製程式碼

★ 注意: svg-url-loader 有一些選項可以改進Internet Explorer的支援,但會使其他瀏覽器的內聯更加糟糕。如果需要支援此瀏覽器,請應用 iesafe: true 選項。

image-webpack-loader 可支援JPG、PNG、GIF和SVG影像的壓縮。

這個載入器不嵌入影像到應用程式,所以它必須與 url-loadersvg-url-loader 成對工作。為了避免將其複製貼上到兩個規則中(一個用於JPG/PNG/GIF影像,另一個用於SVG影像),我們通過 enforce: 'pre' 將這個載入器設為一個單獨的規則:

 // webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.(jpe?g|png|gif|svg)$/,
        loader: 'image-webpack-loader',
        // This will apply the loader before the other ones
        enforce: 'pre',
      },
    ],
  },
};
複製程式碼
5、優化第三方依賴

JavaScript 的大小平均有一半以上來自依賴項,而其中的一部分可能是不必要的。我們可以對這些依賴的庫進行優化➡️ webpack-libs-optimizations

比如:moment.js 刪除未使用的地區、react-router 移除未使用的模組,生產環境去除 react propTypes 宣告等。

6、對於ES6模組開啟模組連線

也叫做作用域提升(Scope Hoisting)

早期的時候,為了隔離 CommonJS/AMD 模組,webpack 在打包的時候,會把每個模組都打包到一個函式中,這樣就會增大每個模組的大小和效能開銷。webpack 2 的時候支援了 ES 模組,然後 webpack 3 的時候使模組連線成為了可能。

【原理】:它會分析模組間的依賴關係,儘可能將被打散的模組合併到一個函式中,但不能造成程式碼冗餘,所以只有被引用一次的模組才能被合併。由於需要分析模組間的依賴關係,所以原始碼必須是採用了ES6模組化的,否則Webpack會降級處理不採用Scope Hoisting。

開啟模組連線之後,打出的包將會具有更少的模組,以及更少的模組開銷。如果在生產模式下使用 webpack 4,則模組連線已經啟用。

// webpack.config.js (for webpack 4)
module.exports = {
  optimization: {
    concatenateModules: true,
  },
};
複製程式碼

★ 注意:為什麼預設情況下不啟用此行為?連線模組很酷,但是它增加了構建時間,並中斷了熱模組替換。這就是為什麼應該只在生產中啟用它。

三十分鐘掌握Webpack效能優化

7、如果覺得有意義的話,使用 externals

具體請參考:webpack-configuration-externals

二、使用長期快取

1、檔名輸出

快取包(bundle),並通過更改包名稱(bundle name)來區分版本,將檔名替換成 [name].[chunkname].js

[hash] 替換:可以用於在檔名中包含一個構建相關(build-specific)的 hash; [chunkhash] 替換:在檔名中包含一個 chunk 相關(chunk-specific)的雜湊,比[hash]替換更好; [contenthash] 替換:會根據資源的內容新增一個唯一的 hash,當資源內容不變時,[contenthash] 就不會變。

const HtmlWebpackPlugin = require('html-webpack-plugin');

  module.exports = {
-   entry: './index.js',
+   entry: {
+     main: './index.js',
+   },
    output: {
-     filename: 'bundle.js',
+     filename: '[name].[contenthash].js',  // / → bundle.8e0d62a03.js
      path: path.resolve(__dirname, 'dist')
    }
    plugins: [
      new HtmlWebpackPlugin({
-       title: 'Output Management'
+       title: 'Caching'
      })
    ],
  };
複製程式碼

Hash vs chunkhash vs ContentHash

2、提取第三方庫和樣板程式碼

bundle 拆分成程式程式碼(app)、第三方庫程式碼(vendor)和執行時程式碼(runtime)。

  • 開啟智慧 code splitting

在 webpack 4 中新增以下的程式碼,當第三方庫程式碼大於 30 kb 時(未壓縮和未gzip前),webpack 能夠自動提取 vendor 程式碼,並且如果你在路由層面使用了程式碼分割的話,它也能夠提取公共程式碼。

// webpack.config.js (for webpack 4)
module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'all',
    }
  },
};
複製程式碼

這樣,每次打包都會生成兩個檔案:main.[chunkhash].jsvendors~main.[chunkhash].js (for webpack 4). 在 webpack 4 中, 當第三方庫依賴很小的時候,vendor 包可能不會被生成,但也沒關係。

  • webpack 執行時程式碼

Webpack 在入口 chunk 中,包含了其執行時的引導程式碼: runtime,以及伴隨的 manifest 資料,runtime 是用來管理模組互動的一小片段程式碼。當你將程式碼分割成多個檔案時,這段程式碼包含了 chunk id 和模組檔案之間的對映,包括瀏覽器中的已載入模組的連線,以及懶載入模組的執行邏輯。

Webpack 會將這個執行時包含到最後生成的 chunk 中,即 vendor。每次有任何塊發生變化時,這段程式碼也會發生變化,導致 vendor bundle 發生變化。

【解決方法】:設定 runtimeChunktrue 來為所有 chunks 建立一個單一的執行時包:

// webpack.config.js (for webpack 4)
module.exports = {
  optimization: {
    runtimeChunk: true,
  },
};
複製程式碼

webpack 執行時程式碼很小,內聯它,可以減少 HTTP 請求。

// webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
const InlineSourcePlugin = require('html-webpack-inline-source-plugin');

module.exports = {
  plugins: [
    new HtmlWebpackPlugin({
      // Inline all files which names start with “runtime~” and end with “.js”.
      // That’s the default naming of runtime chunks
      inlineSource: 'runtime~.+\\.js',
    }),
    // This plugin enables the “inlineSource” option
    new InlineSourcePlugin(),
  ],
};
複製程式碼

webpack-concepts-manifest

3、程式碼懶載入

單頁應用中,使用 import 對不關鍵的程式碼進行懶載入。

// videoPlayer.js
export function renderVideoPlayer() { … }

// comments.js
export function renderComments() { … }

// index.js
import {renderVideoPlayer} from './videoPlayer';
renderVideoPlayer();

// …Custom event listener
onShowCommentsClick(() => {
  import('./comments').then((comments) => {
    comments.renderComments();
  });
});
複製程式碼

import() 表示你想要動態載入特定模組,當 webpack 看到 import('./module.js') 時,它會自動把該模組從 chunk 中移除,只有在執行的時候才會被下載。

這會使 main 模組更小,能夠減少初始載入時間,並且也能很好的提高快取,如果你在 main chunk 中改了程式碼,懶載入的模組不會被影響。

按路由/頁面分割程式碼(Code Splitting),以避免載入不必要的內容。

單頁應用中,除了通過 import() 進行懶載入,還可以通過框架層面的手段來進行。 React 應用懶載入——> Code Splitting(react-router) 或者 React.lazy(react doc)

WebpackGuides-CachingWebpackConcepts-The Manifest

4、模組識別符號

使模組識別符號更穩定

在 webpack 構建時,每個 module.id 會基於預設的解析順序(resolve order)進行增量,也就是說,當解析順序發生變化,ID 也會隨之改變。如:當新增一個模組的時候,它可能會出現在模組列表的中間,那麼它之後的模組 ID 就會發生變化。

如果在業務程式碼裡新引入一個模組,則:

  • main bundle 會隨著自身的新增內容的修改,而發生變化 ——> 符合預期
  • vendor bundle 會隨著自身的 module.id 的修改,而發生變化 ——> 【不符合預期】
  • runtime bundle 會因為當前包含一個新模組的引用,而發生變化 ——> 符合預期
+ const webpack = require('webpack');

  module.exports = {
    plugins: [
+      new webpack.HashedModuleIdsPlugin()
    ],
  };
複製程式碼

為了解決這個問題,模組 ID 通過 HashedModuleIdsPlugin 來進行計算,它會把基於數字增量的 ID 替換成模組自身的 hash。這樣的話,一個模組的 ID 只會在重新命名或者移除的時候才會改變,新模組不會影響到它的 ID 變化。

[3IRH] ./index.js 29 kB {1} [built]
[DuR2] (webpack)/buildin/global.js 488 bytes {2} [built]
[JkW7] (webpack)/buildin/module.js 495 bytes {2} [built]
[LbCc] ./webPlayer.js 24 kB {1} [built]
[lebJ] ./comments.js 58 kB {0} [built]
[02Tr] ./ads.js 74 kB {1} [built]
    + 1 hidden module
複製程式碼

三、監控和分析應用程式

在開發階段使用 webpack-dashboardbundlesize 來調整應用程式的大小

  • webpack-dashboard

webpack-dashboard 通過展示依賴項大小、進度和其他細節來增強 webpack 輸出,有助於跟蹤大型依賴項。

npm install webpack-dashboard --save-dev
複製程式碼
// webpack.config.js
const DashboardPlugin = require('webpack-dashboard/plugin');

module.exports = {
  plugins: [
    new DashboardPlugin(),
  ],
};
複製程式碼
  • bundlesize

bundlesize 用於驗證 webpack 的資源不超過指定的大小,當應用程式變得太大時能夠及時得知。

(1)執行打包命令 (2)開啟 bundlesize

npm install bundlesize --save-dev
複製程式碼

(3)在 package.json 中指定檔案大小限制

// package.json
{
  "bundlesize": [
    {
      "path": "./dist/*.png",
      "maxSize": "16 kB",
    },
    {
      "path": "./dist/main.*.js",
      "maxSize": "20 kB",
    },
    {
      "path": "./dist/vendor.*.js",
      "maxSize": "35 kB",
    }
  ]
}
複製程式碼

(4)執行 bundlesize

npx bundlesize
複製程式碼

或者用 npm 執行:

// package.json
{
  "scripts": {
    "check-size": "bundlesize"
  }
}
複製程式碼

通過  webpack-bundle-analyzer 分析包的大小

webpack-bundle-analyzer 能夠掃描 bundle 並對其內部內容進行視覺化呈現,從而可以發現大型的或者不必要的依賴項。

npm install webpack-bundle-analyzer --save-dev
複製程式碼
// webpack.config.js
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

module.exports = {
  plugins: [
    new BundleAnalyzerPlugin(),
  ],
};
複製程式碼

執行生產構建,該外掛會在瀏覽器中開啟視覺化頁面。

預設情況下,統計頁面顯示的是已解析檔案的大小(當檔案出現在包中時)。您可能想比較 gzip 之後的大小,因為它更接近實際使用者體驗,可以使用左邊的邊欄來切換大小。

對於報告,我們需要關注的點有:

  • 大型依賴項:為什麼這麼大?是否有更小的替代方案(例如,用 Preact 代替 React)?您是否使用了該庫包含的所有程式碼(例如,Moment.js 包含了許多 經常不使用且可能被刪除的地區設定)?

  • 重複的依賴關係:您是否看到同一個庫在多個檔案中重複出現?(在 webpack 4 中使用 optimization.splitChunks.chunks 將重複的依賴關係移動到一個公共檔案)。或者某個包具有相同庫的多個版本?

  • 相似的依賴關係:是否有類似的庫可以做大致相同的工作?(例如,momentdate-fns,或 lodashlodash-es),試著只用一個工具。

四、總結

(1)削減不必要的位元組。壓縮所有內容,刪除未使用的程式碼,明智地新增依賴項; (2)按路由拆分程式碼。只載入現在真正需要的東西,稍後再載入其他東西; (3)快取程式碼。應用程式的某些部分(如第三方庫)更新的頻率低於其他部分,將這些部分分離到檔案中,以便只在必要時重新下載; (4)追蹤程式碼大小。使用像 webpack-dashboard 和 webpack-bundle-analyzer 這樣的工具來了解你的應用程式有多大。

參考

相關文章