啟用新版 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"
欄位參照的模組暗示 Node 版本至少為 12.8,且支援 ES2019。也就是說,使用 "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 以轉譯 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 套件新增至任何專案。Devolution 是獨立工具,可轉換建構系統的輸出內容,產生舊版 JavaScript 變化版本,讓套件和轉換作業假設採用新式輸出目標。