發布、提供及安裝新式 JavaScript,加快應用程式速度

啟用新版 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 模組語法 (importexport)。

{
  "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.jsPreact 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'],
          },
        },
      },
    ],
  },
};

使用這種方法時,您必須確保縮減器支援新式語法。Terseruglify-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 語法。還有一些較高層級的建構工具,會優先採用慣例和預設值,而非設定,例如 ParcelSnowpackViteWMR。這些工具大多假設 npm 依附元件可能包含新式語法,並在正式建構時將其轉譯為適當的語法層級。

除了 webpack 和 Rollup 專用的外掛程式之外,您也可以使用退化功能,將含有舊版備用方案的現代 JavaScript 套件新增至任何專案。Devolution 是獨立工具,可轉換建構系統的輸出內容,產生舊版 JavaScript 變化版本,讓套件和轉換作業假設採用新式輸出目標。