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

豪西恩吉德
Houssein Djirdeh

在本程式碼研究室中,改善這個簡易應用程式的效能,讓使用者可以對隨機的貓進行評分。瞭解如何將轉譯的程式碼數量降到最低,藉此將 JavaScript 套件最佳化。

應用程式螢幕截圖

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

測量

建議您先檢查網站,再添加任何最佳化做法:

  1. 如要預覽網站,請按下「View App」,然後按下「Fullscreen」全螢幕
  2. 按下「Control + Shift + J 鍵」(或在 Mac 上按下「Command+Option+J」) 鍵開啟開發人員工具。
  3. 按一下 [網路] 分頁標籤。
  4. 勾選「Disable cache」核取方塊。
  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,您也可以使用其 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 編譯器。這樣就能在專案根目錄的 .babelrc 中定義所有 Babel 設定。
  • 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 不同的模組 Bundler,請瞭解如何在應用程式中加入 Babel

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

"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 的「最近 2 個版本視為有害」一文。

使用 <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>

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

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

搭配 Babel 使用 ES 模組

如要讓這兩個應用程式版本各自有 @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
}

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

webpack.config.js 檔案的開頭加入 entrycssRulecorePlugins 物件。這些模組可在提供給瀏覽器的模組和舊版指令碼之間共用。

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>

此處最後需要將 modulenomodule 屬性分別新增至模組和舊版指令碼,請在 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 指令碼的輸出內容。雖然目前有解決方法,另外有自行建立的外掛程式 (例如 BabelMultiTargetPluginHTMLWebpackMultiBuildPlugin),但為了教學目的,手動新增模組指令碼元素較為簡單。

請將以下內容新增至檔案結尾的 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 模組如何傳送兩個不同的轉譯版本應用程式,進一步改善效能。您已經充分瞭解這兩種技術如何大幅縮減套件的大小,開始學習並進行最佳化!