發布、傳輸和安裝現代 JavaScript 以實現更快的應用程序
超過 90% 的瀏覽器能夠運行現代 JavaScript,但傳統 JavaScript 的流行仍然是當今 Web 性能問題的最大原因之一。
當今的 Web 受到傳統 JavaScript 限制,沒有任何單一優化可以像使用 ES2017 語法編寫、發布和傳輸網頁或軟件包那樣提高性能。
現代 JavaScript
現代 JavaScript 的特征不是使用特定的 ECMAScript 規范版本編寫代碼,而是使用所有現代瀏覽器都支持的語法。Chrome、Edge、Firefox 和 Safari 等現代網絡瀏覽器占據瀏覽器市場的 90% 以上,依賴相同底層渲染引擎的其他瀏覽器占另外 5%。這意味著全球 95% 的 Web 流量所來自的瀏覽器支持過去 10 年來最廣泛使用的 JavaScript 語言特性,包括:
- 類 (ES2015)
- 箭頭函數 (ES2015)
- 生成器 (ES2015)
- 塊范圍 (ES2015)
- 解構 (ES2015)
- 剩余和展開參數 (ES2015)
- 對象速記 (ES2015)
- 異步/等待 (ES2017)
較新版本的語言規范中的特性在現代瀏覽器中獲得的支持通常不太一致。例如,許多 ES2020 和 ES2021 特性僅在 70% 的瀏覽器市場獲得支持 — 仍然是大多數瀏覽器,但還不夠安全,不能直接依賴這些特性。這意味著盡管“現代”JavaScript 是一個活動目標,但 ES2017 擁有最廣泛的瀏覽器兼容性,同時包含大多數常用的現代語法特性。換句話說,ES2017 目前最接近現代語法。
傳統 JavaScript
傳統 JavaScript 是明確避免使用上述所有語言特性的代碼。大多數開發人員使用現代語法編寫源代碼,但將所有內容編譯為傳統語法以增加瀏覽器支持。編譯為傳統語法確實會增加瀏覽器支持,但效果通常比我們想象的小。在許多情況下,支持度從 95% 左右增加到 98%,但同時產生了大量成本:
- 傳統 JavaScript 通常比等效的現代代碼大 20% 左右,而且速度更慢。工具缺陷和錯誤配置通常會進一步擴大這一差距。
- 安裝的庫占典型生產 JavaScript 代碼的 90%。庫代碼會由于polyfill 和 helper 重復而產生更高的傳統 JavaScript 開銷,而發布現代代碼可以避免這個問題。
npm 上的現代 JavaScript
Node.js 標準化了一個 "exports" 字段來定義軟件包的入口點:
"exports" 字段引用的模塊意味著 Node 版本至少為 12.8,它支持 ES2019。這意味著使用 "exports" 字段引用的任何模塊都可以使用現代 JavaScript 編寫。軟件包使用者必須假定具有 "exports" 字段的模塊包含現代代碼并在必要時進行轉換。
僅現代
如果要發布采用現代代碼的軟件包,并讓使用者在將其用作依賴項時處理轉換,則僅使用 ??"exports"?
? 字段。
不推薦這種方法。在完美的世界中,每個開發人員都已經將編譯系統配置為將所有依賴項 (node_modules) 轉換為所需語法。但是,目前情況并非如此,僅使用現代語法發布軟件包將使其無法在通過舊版瀏覽器訪問的應用程序中使用。
具有傳統回退的現代代碼
將 "exports" 字段與 "main" 一起使用,以便使用現代代碼發布軟件包,但還包括用于舊版瀏覽器的 ES5 + CommonJS 回退。
具有傳統回退的現代代碼和 ESM 捆綁程序優化
除了定義回退 CommonJS 入口點,還可以使用 "module" 字段指向類似的傳統回退捆綁包,但該捆綁包使用 JavaScript 模塊語法 (import 和 export)。
許多捆綁程序(如 webpack 和 Rollup)依賴該字段來利用模塊特性和實現搖樹優化。這仍然是一個傳統捆綁包,不包含除了 import/export 語法之外的任何現代代碼,所以使用這種方法來傳輸具有傳統回退、但仍然針對捆綁進行了優化的現代代碼。
應用程序中的現代 JavaScript
第三方依賴項構成了 Web 應用程序中絕大多數的典型生產 JavaScript 代碼。雖然 npm 依賴項在歷史上一直以 ES5 語法的形式發布,但這不再是一個安全假設,并且依賴項更新可能會破壞應用程序的瀏覽器支持。
隨著越來越多的 npm 包轉向現代 JavaScript,確保構建工具設置為能夠處理它們很重要。您所依賴的一些 npm 包很有可能已經在使用現代語言特性。有許多選擇可使用 npm 中的現代代碼而不會破壞應用程序在舊版瀏覽器中的體驗,但總體思路是讓編譯系統將依賴項轉換為與源代碼相同的目標語法。
webpack
從 webpack 5 開始,現在可以配置 webpack 在生成捆綁包和模塊的代碼時將使用的語法。這不會轉換您的代碼或依賴項,只影響由 webpack 生成的“粘附”代碼。要指定瀏覽器支持目標,請在您的項目中添加一個 browserslist 配置,或者直接在 webpack 配置中添加:
還可以將 webpack 配置為生成優化的捆綁包,當以現代 ES 模塊環境為目標時,這些捆綁包會省略不必要的包裝函數。這也將 webpack 配置為使用 <script type="module"> 加載代碼拆分捆綁包。
有許多 webpack 插件可以編譯和傳輸現代 JavaScript,同時仍然支持舊版瀏覽器,例如 Optimize Plugin 和 BabelEsmPlugin。
Optimize Plugin
Optimize Plugin 是一個 webpack 插件,它可以將最終的捆綁代碼從現代 JavaScript 轉換為傳統 JavaScript,而不是單獨的源文件。它是一個自包含設置,允許 webpack 配置假定所有內容都是現代 JavaScript,沒有針對多個輸出或語法的特殊分支。
由于 Optimize Plugin 針對捆綁包而不是單個模塊進行操作,因此它會平等處理應用程序代碼和依賴項。這樣便可以安全地使用 npm 中的現代 JavaScript 依賴項,因為它們的代碼將被捆綁并轉換為正確的語法。它還可以比涉及兩個編譯步驟的傳統解決方案更快,同時仍然為現代和舊版瀏覽器生成單獨的捆綁包。這兩套捆綁包設計為使用模塊/無模塊模式加載。
Optimize Plugin 可以比自定義 webpack 配置更快、更高效,后者通常單獨捆綁現代和傳統代碼。它還可以處理運行中的 Babel,并使用 Terser 以單獨的針對現代和傳統輸出優化的設置,使捆綁包最小化。最后,生成的傳統捆綁包所需的 polyfill 將提取到一個專用腳本中,這樣在較新的瀏覽器中不會復制或不必要地加載它們。
BabelEsmPlugin
BabelEsmPlugin 是一個 webpack 插件,它與 @babel/preset-env 一起工作來生成現有捆綁包的現代版本,以將更少的轉換代碼傳輸到現代瀏覽器。它是 Next.js 和 Preact CLI 使用最多的模塊/無模塊現成解決方案。
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 依賴項,因為它們可能包含現代語法:
或者,可以通過在解析模塊時檢查 package.json 中是否存在 "exports" 字段,在 webpack 配置中手動實現該技術。為簡潔起見而省略緩存,自定義實現可能如下所示:
使用此方法時,您需要確保縮小器支持現代語法。Terser 和 uglify-es 都有指定 {ecma: 2017} 的選項,以便在壓縮和格式化期間保留 ES2017 語法并在某些情況下生成該語法。
Rollup
Rollup 內部支持生成多組捆綁包作為單個版本的一部分,并默認生成現代代碼。因此,可以將 Rollup 配置為通過您可能已經在使用的官方插件生成現代和傳統捆綁包。
@rollup/plugin-babel
如果使用 Rollup,getBabelOutputPlugin() 方法(由 Rollup 的官方 Babel 插件提供)會轉換生成的捆綁包中的代碼,而不是單個源模塊。Rollup 內部支持生成多組捆綁包作為單個版本的一部分,每個捆綁包都有自己的插件。您可以通過不同的 Babel 輸出插件配置來傳遞各個捆綁包,從而生成不同的現代和傳統捆綁包:
其他構建工具
Rollup 和 webpack 是高度可配置的,這通常意味著每個項目都必須更新其配置以在依賴項中啟用現代 JavaScript 語法。還有更高級的構建工具更傾向于慣例和默認值,而不是配置,例如 Parcel、Snowpack、Vite 和 WMR。這些工具中的大多數假定 npm 依賴項可能包含現代語法,并在生產編譯時將它們轉換為適當的語法級別。
除了 webpack 和 Rollup 的專用插件,還可以使用 devolution 將具有傳統回退的現代 JavaScript 捆綁包添加到任何項目中。Devolution 是一個獨立的工具,可轉換編譯系統的輸出以生成傳統 JavaScript 變體,從而允許捆綁和轉換采用現代輸出目標。