為新型瀏覽器提供新程式碼,加快網頁載入速度

Houssein Djirdeh
Houssein Djirdeh

在本程式碼研究室中,您將提升這個簡單應用程式的效能,讓使用者能為隨機顯示的貓咪評分。瞭解如何盡量減少轉譯的程式碼,藉此最佳化 JavaScript 組合。

應用程式螢幕截圖

在範例應用程式中,您可以選取文字或表情符號,表達對每隻貓咪的喜愛程度。按一下按鈕後,應用程式會在目前的貓咪圖片下方顯示按鈕的值。

評估

建議您先檢查網站,再進行任何最佳化:

  1. 如要預覽網站,請按下「View App」,然後按下「Fullscreen」全螢幕。
  2. 按下 `Control+Shift+J` (在 Mac 上為 `Command+Option+J`) 開啟開發人員工具。
  3. 按一下 [網路] 分頁標籤。
  4. 勾選「停用快取」核取方塊。
  5. 重新載入應用程式。

原始套件大小要求

這個應用程式使用了超過 80 KB!現在來看看套件中是否有未使用的部分:

  1. 按下 Control+Shift+P (在 Mac 上為 Command+Shift+P) 開啟「Command」選單。 指令選單

  2. 輸入 Show Coverage 並按下 Enter,即可顯示「涵蓋率」分頁。

  3. 在「Coverage」分頁中,按一下「Reload」,即可在擷取涵蓋範圍時重新載入應用程式。

    重新載入應用程式並啟用程式碼涵蓋率

  4. 查看主套件的程式碼使用量與載入量:

    套件的程式碼涵蓋率

超過一半的套件 (44 KB) 甚至未被使用。這是因為其中許多程式碼都包含 polyfill,以確保應用程式能在舊版瀏覽器中運作。

使用 @babel/preset-env

JavaScript 語言的語法符合 ECMAScript 標準,或 ECMA-262。每年都會發布新版規格,其中包含已通過提案程序的新功能。每個主要瀏覽器對這些功能的支援程度不盡相同。

應用程式會使用下列 ES2015 功能:

此外,我們也使用了下列 ES2017 功能:

歡迎深入研究 src/index.js 中的原始碼,瞭解所有這些項目的使用方式。

最新版 Chrome 支援所有這些功能,但其他不支援這些功能的瀏覽器呢?應用程式中包含的 Babel 是最熱門的程式庫,用於將包含較新語法的程式碼編譯成舊版瀏覽器和環境可理解的程式碼。具體做法有兩種:

  • Polyfill 包含模擬較新的 ES2015+ 函式,因此即使瀏覽器不支援,也能使用其 API。以下是 Array.includes 方法的 polyfill 範例。
  • 外掛程式用於將 ES2015 程式碼 (或更新版本) 轉換為舊版 ES5 語法。由於這些是語法相關變更 (例如箭頭函式),因此無法使用 polyfill 模擬。

查看 package.json,瞭解其中包含哪些 Babel 程式庫:

"dependencies": {
  "@babel/polyfill": "^7.0.0"
},
"devDependencies": {
  //...
  "babel-loader": "^8.0.2",
  "@babel/core": "^7.1.0",
  "@babel/preset-env": "^7.1.0",
  //...
}
  • @babel/core 是核心 Babel 編譯器。這樣一來,所有 Babel 設定都會在專案根目錄的 .babelrc 中定義。
  • babel-loader 在 webpack 建構程序中包含 Babel。

現在請查看 webpack.config.js,瞭解 babel-loader 如何做為規則納入:

module: {
  rules: [
    //...
    {
      test: /\.js$/,
      exclude: /node_modules/,
      loader: "babel-loader"
    }
  ]
},
  • @babel/polyfill 為任何較新的 ECMAScript 功能提供所有必要的 polyfill,以便在不支援這些功能的環境中運作。已在 src/index.js. 的最上方匯入
import "./style.css";
import "@babel/polyfill";
  • @babel/preset-env 會識別所選目標瀏覽器或環境需要哪些轉換和 polyfill。

請查看 Babel 設定檔 .babelrc,瞭解其加入方式:

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": "last 2 versions"
      }
    ]
  ]
}

這是 Babel 和 webpack 設定。如果您使用的模組繫結器不是 webpack,請參閱這篇文章,瞭解如何在應用程式中加入 Babel。

.babelrc 中的 targets 屬性會識別指定的瀏覽器。@babel/preset-env 與 browserslist 整合,因此您可以在 browslist 說明文件中,找到可用於這個欄位的相容查詢完整清單。

"last 2 versions" 值會為每個瀏覽器的最新兩個版本轉譯應用程式中的程式碼。

偵錯

如要完整查看所有瀏覽器的 Babel 目標,以及所有包含的轉換和 polyfill,請在 .babelrc: 中新增 debug 欄位

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": "last 2 versions",
        "debug": true
      }
    ]
  ]
}
  • 按一下「工具」。
  • 按一下「Logs」(記錄檔)。

重新載入應用程式,並查看編輯器底部的 Glitch 狀態記錄。

指定瀏覽器

Babel 會將編譯程序的許多詳細資料記錄到控制台,包括程式碼編譯的所有目標環境。

指定瀏覽器

請注意,這份清單包含已停用的瀏覽器,例如 Internet Explorer。這會造成問題,因為不支援的瀏覽器不會新增功能,而 Babel 會繼續為這些瀏覽器轉譯特定語法。如果使用者並非透過這個瀏覽器存取網站,套件大小就會不必要地增加。

Babel 也會記錄使用的轉換外掛程式清單:

使用的外掛程式清單

這份清單很長!這些是 Babel 需要使用的所有外掛程式,可將任何 ES2015+ 語法轉換為所有目標瀏覽器的舊語法。

不過,Babel 不會顯示任何使用的特定 polyfill:

未新增任何 polyfill

這是因為整個 @babel/polyfill 會直接匯入。

個別載入 polyfill

根據預設,當 @babel/polyfill 匯入檔案時,Babel 會包含完整 ES2015+ 環境所需的所有 polyfill。如要匯入目標瀏覽器所需的特定 polyfill,請在設定中加入 useBuiltIns: 'entry'。

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": "last 2 versions",
        "debug": true
        "useBuiltIns": "entry"
      }
    ]
  ]
}

重新載入應用程式。現在,你可以查看所有包含的特定 polyfill:

匯入的 Polyfill 清單

雖然現在只包含 "last 2 versions" 所需的 polyfill,但清單還是很長!這是因為系統仍會納入目標瀏覽器所需的 polyfill,以支援每項新功能。將屬性值變更為 usage,只納入程式碼中使用的功能所需的值。

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": "last 2 versions",
        "debug": true,
        "useBuiltIns": "entry"
        "useBuiltIns": "usage"
      }
    ]
  ]
}

這樣一來,系統就會在需要時自動加入 polyfill。這表示您可以移除 src/index.js. 中的 @babel/polyfill 匯入項目

import "./style.css";
import "@babel/polyfill";

現在,系統只會納入應用程式所需的 polyfill。

自動納入的 Polyfill 清單

應用程式套件大小大幅縮減。

套件大小縮減至 30.1 KB

縮減支援的瀏覽器清單

包含的瀏覽器目標數量仍然相當龐大,而且使用 Internet Explorer 等已停用瀏覽器的使用者並不多。將設定更新為下列內容:

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": "last 2 versions",
        "targets": [">0.25%", "not ie 11"],
        "debug": true,
        "useBuiltIns": "usage",
      }
    ]
  ]
}

查看擷取套件的詳細資料。

套件大小為 30.0 KB

由於應用程式非常小,因此這些變更不會造成太大差異。不過,建議您使用瀏覽器市占率百分比 (例如 ">0.25%"),並排除您確定使用者不會使用的特定瀏覽器。如要進一步瞭解,請參閱 James Kyle 撰寫的「Last 2 versions」considered harmful 一文。

使用 <script type="module">

但仍有改進空間。雖然已移除許多未使用的 polyfill,但仍有許多不必要的 polyfill 會傳送至某些瀏覽器。使用模組後,您就能編寫較新的語法,並直接傳送至瀏覽器,而不必使用任何不必要的 Polyfill。

JavaScript 模組是所有主要瀏覽器都支援的新功能。您可以使用 type="module" 屬性建立模組,定義從其他模組匯入及匯出的指令碼。例如:

// math.mjs
export const add = (x, y) => x + y;

<!-- index.html -->
<script type="module">
  import { add } from './math.mjs';

  add(5, 2); // 7
</script>

許多較新的 ECMAScript 功能已在支援 JavaScript 模組的環境中支援 (而非需要 Babel)。這表示您可以修改 Babel 設定,將兩個不同版本的應用程式傳送至瀏覽器:

  • 這個版本適用於支援模組的新版瀏覽器,其中包含一個基本上未經轉譯但檔案大小較小的模組
  • 包含較大轉譯指令碼的版本,可在任何舊版瀏覽器中運作

搭配使用 ES 模組和 Babel

如要為兩個版本的應用程式分別設定 @babel/preset-env,請移除 .babelrc 檔案。只要為每個應用程式版本指定兩種不同的編譯格式,即可將 Babel 設定新增至 webpack 設定。

首先,請在 webpack.config.js 中新增舊版指令碼的設定:

const legacyConfig = {
  entry,
  output: {
    path: path.resolve(__dirname, "public"),
    filename: "[name].bundle.js"
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: "babel-loader",
        options: {
          presets: [
            ["@babel/preset-env", {
              useBuiltIns: "usage",
              targets: {
                esmodules: false
              }
            }]
          ]
        }
      },
      cssRule
    ]
  },
  plugins
}

請注意,系統並未使用 "@babel/preset-env" 的 targets 值,而是使用值為 false 的 esmodules。這表示 Babel 包含所有必要的轉換和 polyfill,可指定所有尚未支援 ES 模組的瀏覽器。

將 entry、cssRule 和 corePlugins 物件新增至 webpack.config.js 檔案的開頭。這些都會在模組和提供給瀏覽器的舊版指令碼之間共用。

const entry = {
  main: "./src"
};

const cssRule = {
  test: /\.css$/,
  use: ExtractTextPlugin.extract({
    fallback: "style-loader",
    use: "css-loader"
  })
};

const plugins = [
  new ExtractTextPlugin({filename: "[name].css", allChunks: true}),
  new HtmlWebpackPlugin({template: "./src/index.html"})
];

現在,請以類似方式為定義 legacyConfig 的模組指令碼建立設定物件:

const moduleConfig = {
  entry,
  output: {
    path: path.resolve(__dirname, "public"),
    filename: "[name].mjs"
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: "babel-loader",
        options: {
          presets: [
            ["@babel/preset-env", {
              useBuiltIns: "usage",
              targets: {
                esmodules: true
              }
            }]
          ]
        }
      },
      cssRule
    ]
  },
  plugins
}

主要差異在於輸出檔案名稱使用 .mjs 副檔名。這裡的 esmodules 值設為 true,表示輸出至這個模組的程式碼是較小、編譯程度較低的指令碼,在這個範例中不會經過任何轉換,因為使用的所有功能都已在支援模組的瀏覽器中支援。

在檔案最尾端,將兩項設定匯出至單一陣列。

module.exports = [
  legacyConfig, moduleConfig
];

現在,這會為支援的瀏覽器建構較小的模組,並為舊版瀏覽器建構較大的轉譯指令碼。

支援模組的瀏覽器會忽略含有 nomodule 屬性的指令碼。相反地,不支援模組的瀏覽器會忽略含有 type="module" 的指令碼元素。這表示您可以加入模組,以及編譯的備用方案。理想情況下,應用程式的兩個版本應位於 index.html,如下所示:

<script type="module" src="main.mjs"></script>
<script nomodule src="main.bundle.js"></script>

支援模組的瀏覽器會擷取並執行 main.mjs,並忽略 main.bundle.js.。不支援模組的瀏覽器則相反。

請注意,與一般指令碼不同,模組指令碼預設一律會延遲。如要延遲執行對應的 nomodule 指令碼,並在剖析後才執行,則須新增 defer 屬性:

<script type="module" src="main.mjs"></script>
<script nomodule src="main.bundle.js" defer></script>

最後,您需要分別在模組和舊版指令碼中新增 module 和 nomodule 屬性,並在 webpack.config.js 的最上方匯入 ScriptExtHtmlWebpackPlugin:

const path = require("path");

const webpack = require("webpack");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const ScriptExtHtmlWebpackPlugin = require("script-ext-html-webpack-plugin");

現在,請更新設定中的 plugins 陣列,加入這個外掛程式:

const plugins = [
  new ExtractTextPlugin({filename: "[name].css", allChunks: true}),
  new HtmlWebpackPlugin({template: "./src/index.html"}),
  new ScriptExtHtmlWebpackPlugin({
    module: /\.mjs$/,
    custom: [
      {
        test: /\.js$/,
        attribute: 'nomodule',
        value: ''
    },
    ]
  })
];

這些外掛程式設定會為所有 .mjs 指令碼元素新增 type="module" 屬性,並為所有 .js 指令碼模組新增 nomodule 屬性。

HTML 文件中的放送模組

最後,您需要將舊版和新版指令碼元素輸出至 HTML 檔案。很遺憾,建立最終 HTML 檔案的外掛程式 HTMLWebpackPlugin 目前不支援模組和 nomodule 指令碼的輸出。雖然有其他解決方法和獨立外掛程式可以解決這個問題,例如 BabelMultiTargetPlugin 和 HTMLWebpackMultiBuildPlugin,但本教學課程採用更簡單的方法,即手動新增模組指令碼元素。

在檔案結尾的 src/index.js 中加入以下內容:

    ...
    </form>
    <script type="module" src="main.mjs"></script>
  </body>
</html>

現在,請在支援模組的瀏覽器 (例如最新版 Chrome) 中載入應用程式。

透過網路為新版瀏覽器擷取 5.2 KB 模組

系統只會擷取模組,且由於模組大部分未經轉譯,因此套件大小會小得多!瀏覽器會完全忽略另一個指令碼元素。

如果您在舊版瀏覽器上載入應用程式,系統只會擷取較大、經過轉譯的指令碼,其中包含所有必要的 Polyfill 和轉換。以下是在舊版 Chrome (第 38 版) 上發出的所有要求螢幕截圖。

為舊版瀏覽器擷取 30 KB 指令碼

結論

您現在已瞭解如何使用 @babel/preset-env,只為目標瀏覽器提供必要的 polyfill。您也瞭解 JavaScript 模組如何透過提供兩個不同的轉譯版本,進一步提升效能。充分瞭解這兩種技術如何大幅縮減套件大小後,就可以開始進行最佳化了!