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