在本程式碼研究室中,改善這個簡單的應用程式效能,讓使用者能夠為隨機貓咪評分。瞭解如何盡量減少要轉譯的程式碼,藉此最佳化 JavaScript 套件。
在範例應用程式中,您可以選取特定字詞或表情符號,表達您喜歡每隻貓的數量。您只要按一下按鈕,應用程式就會在目前的貓咪圖片下方顯示按鈕的值。
測量
建議您先檢查網站,再加入任何最佳化做法:
- 如要預覽網站,請按下「查看應用程式」,然後按下「全螢幕」圖示 。
- 按下 `Control+Shift+J 鍵 (在 Mac 上為 Command+Option+J 鍵) 開啟開發人員工具。
- 按一下 [網路] 分頁標籤。
- 勾選「停用快取」核取方塊。
- 重新載入應用程式。
這個應用程式使用超過 80 KB!現在該檢查套件的某些部分是否沒有使用:
按下
Control+Shift+P
(或在 Mac 上為Command+Shift+P
) 開啟「Command」選單。輸入
Show Coverage
並點選Enter
,即可顯示「涵蓋範圍」分頁。在「Coverage」分頁中,按一下「Reload」即可重新載入應用程式,同時擷取涵蓋範圍。
請查看使用了多少程式碼,以及主要套件載入多少內容:
沒有使用超過一半的套件 (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
與瀏覽器清單整合,因此您可以在瀏覽器清單說明文件中,查看可用於這個欄位的完整相容查詢清單。
"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:
這是因為系統正在直接匯入整個 @babel/polyfill
。
個別載入 polyfill
根據預設,將 @babel/polyfill
匯入檔案時,Babel 會包含完整 ES2015 以上版本環境所需的每個 polyfill。如要匯入目標瀏覽器所需的特定 polyfill,請在設定中新增 useBuiltIns: 'entry'
。
{
"presets": [
[
"@babel/preset-env",
{
"targets": "last 2 versions",
"debug": true
"useBuiltIns": "entry"
}
]
]
}
重新載入應用程式。現在可以看到所有包含的具體 polyfill:
雖然現在只包含 "last 2 versions"
所需的 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。
應用程式套件的大小大幅縮減。
縮小支援的瀏覽器清單
瀏覽器目標數量仍很大,且許多使用者會使用已停用的瀏覽器 (例如 Internet Explorer)。請將設定更新為以下項目:
{
"presets": [
[
"@babel/preset-env",
{
"targets": "last 2 versions",
"targets": [">0.25%", "not ie 11"],
"debug": true,
"useBuiltIns": "usage",
}
]
]
}
接著查看已擷取套件的詳細資料。
由於應用程式規模很小,因此這些變更完全沒有太大差異。不過,建議您使用瀏覽器市佔率百分比 (例如 ">0.25%"
),並排除您認為使用者並未使用的特定瀏覽器。詳情請參閱 James Kyle 的「最後 2 個版本視為有害」文章。
使用 <script type="module">
還有更多改進空間。雖然我們移除了許多未使用的 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
}
請注意,系統會使用 esmodules
的值為 false
,而不是使用 "@babel/preset-env"
的 targets
值。也就是說,Babel 包含所有必要的轉換和 polyfill,可以指定每個尚未支援 ES 模組的瀏覽器。
在 webpack.config.js
檔案的開頭加入 entry
、cssRule
和 corePlugins
物件。這些都是在提供給瀏覽器的模組和舊版指令碼之間共用。
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) 中載入應用程式。
只擷取模組,套件大小更小,因為其主要未經轉譯!瀏覽器完全會忽略其他指令碼元素。
如果在舊版瀏覽器中載入應用程式,系統只會擷取內含所有必要 polyfill 和轉換作業的較大轉譯指令碼。以下螢幕截圖是針對舊版 Chrome (38 版) 提出的所有要求。
結語
您現在已瞭解如何使用 @babel/preset-env
,只為目標瀏覽器提供必要的 polyfill。您也知道 JavaScript 模組如何傳送兩個不同轉譯版本的應用程式,進一步提升效能。瞭解這兩種技術如何大幅縮減套件大小後,接著即可立刻開始調整並進行最佳化調整!