在本程式碼研究室中,您將改善這個簡單應用程式的效能,讓使用者可以隨機評估貓咪。瞭解如何盡量減少轉譯的程式碼量,進而最佳化 JavaScript 套件。
在範例應用程式中,您可以選取字詞或表情符號,表達對每隻貓的喜愛程度。點選按鈕後,應用程式會在目前的貓咪圖片下方顯示按鈕的值。
測量
建議您先檢查網站,再新增任何最佳化項目:
- 如要預覽網站,請按下「查看應用程式」,然後按下「全螢幕」圖示
。
- 按下 `Control+Shift+J` 鍵 (在 Mac 上為 `Command+Option+J` 鍵) 開啟開發人員工具。
- 按一下 [網路] 分頁標籤。
- 選取「停用快取」核取方塊。
- 重新載入應用程式。
這個應用程式使用的空間超過 80 KB!如要找出未使用的套件部分,請按照下列步驟操作:
按下
Control+Shift+P
鍵 (在 Mac 上為Command+Shift+P
鍵) 開啟「Command」(指令) 選單。輸入
Show Coverage
並按下Enter
,即可顯示「涵蓋範圍」分頁標籤。在「涵蓋範圍」分頁中,按一下「重新載入」,即可在擷取涵蓋範圍時重新載入應用程式。
請查看主要套件的使用程式碼量與載入程式碼量:
超過一半的套件 (44 KB) 甚至未被使用。這是因為其中的許多程式碼都包含 Polyfill,可確保應用程式在舊版瀏覽器中正常運作。
使用 @babel/preset-env
JavaScript 語言的語法符合稱為 ECMAScript 或 ECMA-262 的標準。每年都會發布新版規格,其中包含通過提案程序的新功能。每個主要瀏覽器支援這些功能時,都處於不同階段。
應用程式使用下列 ES2015 功能:
此外,也使用下列 ES2017 功能:
歡迎深入瞭解 src/index.js
中的原始碼,看看這些內容的使用方式。
最新版 Chrome 支援所有這些功能,但其他不支援這些功能的瀏覽器呢?應用程式內含的 Babel 是最常用的程式庫,可將含有新語法的程式碼編譯為舊版瀏覽器和環境可解讀的程式碼。Google 助理會透過以下兩種方式達成這個目標:
- 系統會納入 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 整合,因此您可以在瀏覽器清單說明文件中,找到可用於這個欄位的相容查詢完整清單。
"last 2 versions"
值會轉譯應用程式中的程式碼,適用於每個瀏覽器的最近兩個版本。
偵錯
如要完整查看瀏覽器的所有 Babel 目標,以及所有包含的轉換和 Polyfill,請將 debug
欄位新增至 .babelrc:
{
"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,但清單還是很長!這是因為系統仍會納入每個較新功能所需的目標瀏覽器 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 並非某些瀏覽器所需。使用模組後,即可直接編寫較新的語法並傳送至瀏覽器,完全不需要任何不必要的 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
}
請注意,系統並未將 targets
值用於 "@babel/preset-env"
,而是使用 esmodules
,並將值設為 false
。也就是說,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 模組如何透過傳送兩個不同的應用程式轉譯版本,進一步提升效能。充分瞭解這兩種技術如何大幅縮減套件大小後,即可開始進行最佳化!