啟用新版 JavaScript 依附元件和輸出內容,改善效能。
超過 90% 的瀏覽器都能執行新版 JavaScript,但舊版 JavaScript 的盛行,仍是目前網站上效能問題的主要來源。
新版 JavaScript
新式 JavaScript 的特色並非以特定 ECMAScript 規格版本編寫的程式碼,而是所有新式瀏覽器支援的語法。Chrome、Edge、Firefox 和 Safari 等新世代網路瀏覽器佔比90% 以上的瀏覽器市場,而採用相同基礎轉譯引擎的不同瀏覽器可額外創造 5%。這表示全球網路流量中有 95% 來自支援過去 10 年內最廣泛使用的 JavaScript 語言功能的瀏覽器,包括:
- 課程 (ES2015)
- 箭頭函式 (ES2015)
- 發電機 (ES2015)
- 區塊範圍 (ES2015)
- 解構 (ES2015)
- Rest 和 spread 參數 (ES2015)
- 物件縮寫 (ES2015)
- Async/await (ES2017)
新版語言規格中的功能通常在各款新式瀏覽器中不支援一致性。舉例來說,許多 ES2020 和 ES2021 功能只在瀏覽器市場的 70% 中獲得支援,儘管大多數瀏覽器仍舊支援,但其不夠安全直接依賴這些功能。也就是說,雖然「新式」JavaScript 是不斷變動的目標,但 ES2017 的瀏覽器相容性最廣泛,同時包含大多數常用的新式語法功能。換句話說,ES2017 最接近目前的現代語法。
舊版 JavaScript
舊版 JavaScript 是指避免使用上述所有語言功能的程式碼。大多數開發人員都使用現代語法編寫原始碼,但將所有內容編譯為舊版語法,以增加瀏覽器支援範圍。編譯為舊版語法確實能增加瀏覽器的支援能力,但效果通常比我們發現的還要小。在許多情況下,支援服務會從約 95% 增加到 98%,但成本卻高昂:
舊版 JavaScript 通常比同等的現代程式碼大約大 20%,速度也較慢。工具缺陷和設定錯誤通常會進一步擴大這項差距。
安裝的程式庫可占一般正式版 JavaScript 程式碼的 90%。由於 polyfill 和輔助程式重複,程式庫程式碼會產生更高的舊版 JavaScript 額外負擔,但您可以透過發布新版程式碼來避免這種情況。
npm 上的新型 JavaScript
最近,Node.js 已將 "exports"
欄位標準化,以定義套件的進入點:
{
"exports": "./index.js"
}
"exports"
欄位參照的模組隱含支援 ES2019 的節點版本至少為 12.8。也就是說,使用 "exports"
欄位參照的任何模組都可以以新型 JavaScript 編寫。套件使用者必須假設含有 "exports"
欄位的模組包含新型程式碼,並視需要進行轉譯。
僅限現代
如果想發布含有新型程式碼的套件,並交由取用者把該檔案當做依附元件處理,請自行使用 "exports"
欄位處理。
{
"name": "foo",
"exports": "./modern.js"
}
現代版,搭配舊版備用方案
請使用 "exports"
欄位搭配 "main"
,以便使用新式程式碼發布套件,但也為舊版瀏覽器納入 ES5 + CommonJS 備用方案。
{
"name": "foo",
"exports": "./modern.js",
"main": "./legacy.cjs"
}
以舊版備用廣告和 ESM 套裝組合最佳化的方式進行現代化
除了定義備用 CommonJS 進入點之外,"module"
欄位也可用來指向類似的舊版備用套件,但使用 JavaScript 模組語法 (import
和 export
)。
{
"name": "foo",
"exports": "./modern.js",
"main": "./legacy.cjs",
"module": "./module.js"
}
許多 bundler (例如 webpack 和 Rollup) 都會使用這個欄位來發揮模組功能的優勢,並啟用樹狀圖搖晃功能。這仍是舊版套件,除了 import
/export
語法外,不含任何新式程式碼,因此請使用這種方法,搭配仍針對套件最佳化的舊版備用方案,發布新式程式碼。
應用程式中的新式 JavaScript
第三方依附元件是網頁應用程式中典型的實際 JavaScript 程式碼的絕大多數。雖然 npm 依附元件一直以來都是以舊版 ES5 語法發布,但這不再是安全假設,且因為依附元件更新會破壞應用程式中的瀏覽器支援。
隨著越來越多 npm 套件轉移至新式 JavaScript,請務必確保建構工具已設定好來處理這些套件。您所依賴的部分 npm 套件很可能已使用新式語言功能。您可以使用多種方法,從 npm 使用新式程式碼,且不會在舊版瀏覽器中破壞應用程式,但一般來說,您可以讓建構系統將依附元件轉譯為與原始碼相同的語法目標。
webpack
從 webpack 5 起,您可以設定 Webpack 產生套件和模組程式碼時要使用的語法。這不會轉譯程式碼或依附元件,只會影響 webpack 產生的「glue」程式碼。如要指定瀏覽器支援目標,請在專案中新增瀏覽器清單設定,或直接在 webpack 設定中執行此操作:
module.exports = {
target: ['web', 'es2017'],
};
您也可以設定 webpack,讓系統在指定新式 ES 模組環境時,產生經過最佳化的套件,藉此省略不必要的包裝函式。這也會將 webpack 設定為使用 <script type="module">
載入程式碼分割套件。
module.exports = {
target: ['web', 'es2017'],
output: {
module: true,
},
experiments: {
outputModule: true,
},
};
您可以使用多種 webpack 外掛程式,在編譯及發布新式 JavaScript 的同時,仍支援舊版瀏覽器,例如 Optimize Plugin 和 BabelEsmPlugin。
最佳化工具
最佳化外掛程式是 webpack 外掛程式,可將最終已組合的程式碼從新式 JavaScript 轉換為舊版 JavaScript,而非個別來源檔案。這是一個自給自足的設定,可讓 webpack 設定假設所有內容都是新式 JavaScript,且沒有針對多個輸出或語法進行特殊分支。
最佳化工具外掛程式是在套件 (而非個別模組) 上運作,因此會同樣處理應用程式的程式碼和依附元件。這樣一來,您就能安全地使用 npm 中的新式 JavaScript 依附元件,因為這些程式碼會經過整合並轉譯為正確的語法。這項方法也比傳統的解決方案 (涉及兩個編譯步驟) 更快,同時仍可為新一代和舊版瀏覽器產生個別套件。這兩組套件設計為使用 module/nomodule 模式載入。
// webpack.config.js
const OptimizePlugin = require('optimize-plugin');
module.exports = {
// ...
plugins: [new OptimizePlugin()],
};
Optimize Plugin
比自訂 webpack 設定更快、更有效率,後者通常會將新版和舊版程式碼分開封裝。也可以為您處理 Babel 執行的作業,並使用 Terser 針對現代與舊版輸出內容提供最佳設定,藉此壓縮套裝組合。最後,產生的舊版套件所需的 polyfill 會擷取至專用指令碼,因此不會在較新的瀏覽器中重複或不必要地載入。
BabelEsmPlugin
BabelEsmPlugin 是 webpack 外掛程式,可與 @babel/preset-env 搭配運作,產生現有套件的新版,以便將經過較少轉譯的程式碼傳送至新版瀏覽器。這是模組/nomodule 最熱門的現成解決方案,由 Next.js 和 Preact CLI 使用。
// webpack.config.js
const BabelEsmPlugin = require('babel-esm-plugin');
module.exports = {
//...
module: {
rules: [
// your existing babel-loader configuration:
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env'],
},
},
},
],
},
plugins: [new BabelEsmPlugin()],
};
BabelEsmPlugin
支援多種 webpack 設定,因為它會執行兩個幾乎獨立的應用程式版本。對於大型應用程式,兩次編譯可能會耗費一點額外時間,但這項技巧可讓 BabelEsmPlugin
與現有的 webpack 設定完美整合,是目前最方便的選項之一。
將 babel-loader 設為 transpile node_modules
如果您使用 babel-loader
時沒有使用上述兩個外掛程式之一,則必須執行一項重要步驟,才能使用新式 JavaScript npm 模組。定義兩個個別的 babel-loader
設定後,系統就能自動將 node_modules
中的新式語言功能編譯為 ES2017,同時使用 Babel 外掛程式和專案設定中定義的預設值,轉譯您自己的第一方程式碼。這不會為模組/非模組設定產生新式和舊版套件,但可讓您安裝及使用含有新式 JavaScript 的 npm 套件,而不會破壞舊版瀏覽器。
webpack-plugin-modern-npm 會使用這項技巧,編譯 package.json
中含有 "exports"
欄位的 npm 依附元件,因為這些可能包含新式語法:
// webpack.config.js
const ModernNpmPlugin = require('webpack-plugin-modern-npm');
module.exports = {
plugins: [
// auto-transpile modern stuff found in node_modules
new ModernNpmPlugin(),
],
};
或者,您也可以在 webpack 設定中手動實作這項技巧,方法是在解析模組時,檢查模組的 package.json
中是否有 "exports"
欄位。為了簡化說明,我們省略快取,自訂實作可能如下所示:
// webpack.config.js
module.exports = {
module: {
rules: [
// Transpile for your own first-party code:
{
test: /\.js$/i,
loader: 'babel-loader',
exclude: /node_modules/,
},
// Transpile modern dependencies:
{
test: /\.js$/i,
include(file) {
let dir = file.match(/^.*[/\\]node_modules[/\\](@.*?[/\\])?.*?[/\\]/);
try {
return dir && !!require(dir[0] + 'package.json').exports;
} catch (e) {}
},
use: {
loader: 'babel-loader',
options: {
babelrc: false,
configFile: false,
presets: ['@babel/preset-env'],
},
},
},
],
},
};
使用這個方法時,需要確保壓縮器支援現代語法。Terser 和 uglify-es 兩者都可以指定 {ecma: 2017}
來保留,在某些情況下,也會在壓縮和格式化期間產生 ES2017 語法。
匯總
Rollup 內建支援功能,可在單一版本中產生多組套件,並預設產生新式程式碼。因此,您可以設定 Rollup,讓它使用您可能已在使用的官方外掛程式,產生新版和舊版套件。
@rollup/plugin-babel
如果使用 Rollup,getBabelOutputPlugin()
方法 (由 Rollup 的官方 Babel 外掛程式提供) 會將程式碼轉換到產生的套件中,而非個別來源模組。Rollup 內建支援功能,可讓你在單一建構作業中產生多個套件組合,而每個套件都有自己的外掛程式。您可以使用此方法透過不同的 Babel 輸出外掛程式設定,針對現代與舊版產生不同的套件:
// rollup.config.js
import {getBabelOutputPlugin} from '@rollup/plugin-babel';
export default {
input: 'src/index.js',
output: [
// modern bundles:
{
format: 'es',
plugins: [
getBabelOutputPlugin({
presets: [
[
'@babel/preset-env',
{
targets: {esmodules: true},
bugfixes: true,
loose: true,
},
],
],
}),
],
},
// legacy (ES5) bundles:
{
format: 'amd',
entryFileNames: '[name].legacy.js',
chunkFileNames: '[name]-[hash].legacy.js',
plugins: [
getBabelOutputPlugin({
presets: ['@babel/preset-env'],
}),
],
},
],
};
其他建構工具
Rollup 和 webpack 可高度設定,這代表每個專案都必須更新其設定,才能在依附元件中啟用新型 JavaScript 語法。還有一些較高層級的建構工具,會優先採用慣例和預設值,而非設定,例如 Parcel、Snowpack、Vite 和 WMR。這些工具大多假設 npm 依附元件可能包含新式語法,並在正式建構時將其轉譯為適當的語法層級。
除了 webpack 和 Rollup 專用的外掛程式之外,您也可以使用退化功能,將含有舊版備用方案的現代 JavaScript 套件新增至任何專案。解析是一種獨立工具,可將建構系統的輸出內容轉換為舊版 JavaScript 變數,讓組合和轉換假設為現代輸出目標。