使用Webpack4優化Web性能

利用 Webpack 來優化 Web 性能屬於加載性能優化 的一部分: ☛ Web Performance Optimization with webpackcss

本文目錄:

  • 減小前端資源體積
  • 使用長期緩存
  • 監控和分析應用程序
  • 總結

1、減小前端資源體積

一、webpack 4 開啓 production 模式

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

能夠在 webpack 中指定:前端

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

或者 package.json 中配置:react

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

使用 bundle-level minifier 和 loader options 壓縮代碼。webpack

  • Bundle-level minification

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

在 webpack 4 中,production 模式下會自動執行 bundle-level 的壓縮,底層使用了 the UglifyJS minifier。(若是不想開啓壓縮,能夠採用 development 模式或者設置 optimization.minimize 爲 false)github

  • Loader-specific options

經過 loader 層面的選項配置來對代碼進行壓縮,是爲了壓縮 bundle-level minifier 沒法壓縮的內容,好比,經過 css-loader 編譯後的文件,會成爲字符串,就沒法被 minifier 壓縮。所以,要進一步壓縮文件內容,可進行以下配置:web

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

當使用 ES 模塊時, webpack 可以進行 tree-shaking。npm

tree-shaking 是指 bundler 遍歷整個依賴關係樹,檢查使用了哪些依賴關係,並刪除未使用的依賴關係。所以,若是使用ES模塊語法,webpack 能夠消除未使用的代碼。json

★ 注意:在 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

四、壓縮圖片資源

針對具體的依賴項進行優化(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',
      },
    ],
  },
};
複製代碼
五、優化第三方依賴

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

好比:moment.js 刪除未使用的地區、react-router 移除未使用的模塊,生產環境去除 react propTypes 聲明等。

六、對於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性能優化

七、若是以爲有意義的話,使用 externals

具體請參考:webpack-configuration-externals

2、使用長期緩存

一、文件名輸出

緩存包(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

二、提取第三方庫和樣板代碼

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

三、代碼懶加載

單頁應用中,使用 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

四、模塊標識符

使模塊標識符更穩定

在 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
複製代碼

3、監控和分析應用程序

在開發階段使用 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),試着只用一個工具。

4、總結

(1)削減沒必要要的字節。壓縮全部內容,刪除未使用的代碼,明智地添加依賴項; (2)按路由拆分代碼。只加載如今真正須要的東西,稍後再加載其餘東西; (3)緩存代碼。應用程序的某些部分(如第三方庫)更新的頻率低於其餘部分,將這些部分分離到文件中,以便只在必要時從新下載; (4)追蹤代碼大小。使用像 webpack-dashboard 和 webpack-bundle-analyzer 這樣的工具來了解你的應用程序有多大。

參考

相關文章
相關標籤/搜索