在2019使用差別化服務

原文地址:Doing Differential Serving in 2019
原文做者:Jeremy Wagne
譯文出自:FE-star/speed
譯者:smallbonelu
校對者:[]
本文連接:[]javascript

若是你正在閱讀本文,那麼你可能就是那種一直在尋找務實,前瞻性思惟方式來提升網站速度的人。因此,當我讀了由菲爾·沃爾頓寫的一個被稱爲差別化服務的指南後,我很感興趣。若是你尚未據說過這種技術,其實就是你能夠爲你的站點編譯和提供兩個獨立的JavaScript捆綁包:html

  1. 一個捆綁包包含全部的Babel-fied轉換和polyfills,適用於全部瀏覽器 - 只提供給實際須要它們的舊版瀏覽器。這多是你已經生成的包。
  2. 第二個捆綁包具備與第一個包相同的功能,但幾乎沒有作轉換或polyfills。此包僅供可使用它們的現代瀏覽器使用。

咱們使用Babel來轉換腳本,以便咱們能夠在任何地方使用它們,可是咱們這樣作會有一些危險。它在大多數配置中添加的額外代碼對於現代瀏覽器上的用戶來講一般不是必要的。經過一些努力,能夠更改構建過程,以減小咱們在現代瀏覽器上發送給大量用戶的代碼量,同時保持對遺留客戶端(例如IE11)的兼容性。差別化服務的目的不只僅是改善傳送時間 - 這確定對傳送時間有所幫助。它還能夠經過減小瀏覽器須要處理的腳本數量來幫助減小主線程的阻塞,這是一個資源密集型處理過程。java

在本指南中,你將瞭解如何在2019年的構建管道中設置差別化服務,從設置Babel到你須要在webpack中進行哪些調整,以及完成全部這些工做的好處。node

設置你的Babel配置

輸出同一應用程序的多個構建版本涉及每一個目標的Babel配置。畢竟,單個項目中的多個Babel配置並不罕見。一般經過將每一個單獨的配置對象放在env對象鍵下來完成。如下是爲差別化服務設置的Babel配置中的內容:webpack

// babel.config.js
module.exports = {
  env: {
    // 這是咱們將用於爲舊版瀏覽器生成捆綁包的配置
    legacy: {
      presets: [
        [
          "@babel/preset-env", {
            modules: false,
            useBuiltIns: "entry",
            // 這應該合理地針對舊瀏覽器.
            targets: "> 0.25%, last 2 versions, Firefox ESR"
          }
        ]
      ],
      plugins: [
        "@babel/plugin-transform-runtime",
        "@babel/plugin-syntax-dynamic-import"
      ]
    },
    // 這是用來爲現代瀏覽器生成包的配置.
    modern: {
      presets: [
        [
          "@babel/preset-env", {
            modules: false,
            targets: {
              // 這將針對支持ES模塊的瀏覽器.
              esmodules: true
            }
          }
        ]
      ],
      plugins: [
        "@babel/plugin-transform-runtime",
        "@babel/plugin-syntax-dynamic-import"
      ]
    }
  }
};  
複製代碼

你會注意到有兩種配置:modernlegacy。這些都控制着Babel如何轉換每一個包。諷刺的是,將polyfills並添加沒必要要的變換到咱們的代碼的工具與咱們能夠用來發送更少代碼的是同一個工具。在Phil的原始文章中,他使用Babel 6 babel-preset-env來實現這一目標。如今Babel 7發佈了,我改用了@babel/preset-envgit

首先要注意的是,@babel/preset-env每一個配置中使用的選項都不一樣。對於legacy選項,咱們將browserslist查詢傳遞給適用於舊版瀏覽器的targets選項。咱們也告訴preset包括來自@babel/polyfilluseBuiltIns選項的polyfills。除了presets,咱們還包括一些必要的插件。github

注意:useBuiltIns除了false以外還接受兩個值。這些值是*entryusage*文檔很好地解釋了它們的不一樣之處,但值得注意的是"usage"是實驗性的。與"entry"相比,**它一般會產生更小的包,但我發現我須要指定"entry"才能讓腳本在IE 11中運行。web

對於modern配置而言,配置看起來基本相同,除了價值targets不一樣。我使用esmodules選項傳遞一個對象,而不是傳遞一個browserslist查詢。當設置爲true時,@babel/preset-env將使用較少的轉換,由於預設的目標是本機支持ES模塊和async/ await或其餘現代功能的瀏覽器。useBuiltIns選項也刪除了,由於項目中使用的全部功能都不須要pollyfill。也就是說,若是你使用的時現代瀏覽器都不能很好地支持的前沿功能,你的應用程序可能須要一些polyfill。若是你的應用程序違反此設置,請進行適當地useBuiltIns設置。正則表達式

配置webpack以進行差別化服務

webpack - 以及大多數其餘打包工具 - 將提供一種稱爲多編譯器模式的功能。此功能對於差別化服務相當重要。多編譯器模式容許你傳遞多個配置對象的數組以吐出多組包:shell

// webpack.config.js
modules.exports = [{
  // Object config one
}, {
  // Object config two
}]; 
複製代碼

這很重要,由於咱們能夠傳遞兩個使用相同入口點的獨立配置。咱們還能夠根據須要調整每一個配置中的規則。

不過,這提及來容易作起來難。webpack有時很是複雜,當你處理多種配置時,沒有比這更復雜的了。可是,這並不是不可能,因此讓咱們找出實現目標所需的條件吧。

從常見配置開始

由於你正在對相同的入口點進行單獨構建,因此你的配置將有不少共同之處。通用配置是管理這些類似性的便捷方式:

// webpack.config.js
const commonConfig = {
  // `devMode` 是 process.env.NODE_ENV !== "production"的結果
  mode: devMode ? "development" : "production",
  entry: path.resolve(__dirname, "src", "index.js"),
  plugins: [
    // 在兩種配置中常見的插件
  ]
}; 
複製代碼

從這裏開始,你能夠編寫單獨的webpack配置並使用擴展語法將經常使用配置合併到每一個配置中:

// webpack.config.js
const legacyConfig = {
  name: "client-legacy",
  output: path.resolve(__dirname, "src", "index.js"),
  module: {
    rules: [
      // loader...
    ]
  },
  // 使用擴展語法將經常使用配置合併到這個對象中.
  ...commonConfig
};

const modernConfig = {
  name: "client-modern",
  // 注意使用.mjs擴展名
  output: path.resolve(__dirname, "src", "index.mjs"),
  module: {
    rules: [
      // loader...
    ]
  },
  // 同上.
  ...commonConfig
};

module.exports = [legacyConfig, modernConfig];
複製代碼

這裏要說的是,若是將兩種配置之間的共同點與公共對象結合起來,能夠最大限度地減小必須編寫的配置行。從那裏,你只關注每一個目標之間的關鍵差別。

管理兩種配置

既然你知道如何管理每一個配置之間的共同點,那麼你須要知道如何管理不一樣的配置。當你編譯指向不一樣目標的公共入口點時,管理loader和插件會變得棘手。若是你處理的不只僅是JavaScript assets,尤爲如此。這是我但願會對你有所幫助的一些指導。

babel-loader

能夠說,你在任何webpack配置中看到的最多見的loader是babel-loader。對於咱們想要實現的目標,你須要在你的新版和舊版配置對象中使用babel-loader,儘管配置稍有不一樣。babel-loader對於舊版瀏覽器目標,配置會像下面這樣:

// webpack.config.js
const legacyConfig = {
  // ...
  module: {
    rules: [
      {
        test: /\.js$/i,
        // 確保你的第三方庫打包到獨立的chunk中
        // 不然這個排除模式也許會破壞你在客戶端的構建
        exclude: /node_modules/i,
        use: {
          loader: "babel-loader",
          options: {
            envName: "legacy" // 指向babel.config.js中的env.legacy
          }
        }
      },
      // 其餘loader...
    ]
  },
  // ...
};
複製代碼

在現代瀏覽器目標中,惟一的區別是咱們將test正則表達式的值改成/\.m?js$/i來包含在npm包中的ES模塊文件擴展名(.mjs)。咱們還須要將options.envName的值修改成"modern"options.envName指向在前面示例中babel.config.js中包含的單獨配置。

// webpack.config.js
const modernConfig = {
  // ...
  module: {
    rules: [
      {
        test: /\.m?js$/i,
        exclude: /node_modules/i,
        use: {
          loader: "babel-loader",
          options: {
            envName: "modern" // 指向babel.config.js中的env.modern
          }
        }
      },
      // Other loaders...
    ]
  },
  // ...
};
複製代碼

其餘loader和插件

根據你的項目,你可能有其餘loader或插件來處理JavaScript之外的assets類型。如何爲每一個目標瀏覽器處理它們取決於你的項目需求,但這裏有一些建議。

  • 可能不須要對其餘loaders進行任何更改。須要記住的是,webpack不只能夠管理JavaScript,還能夠管理大量其餘內容。 CSS,圖像,字體,基本上每個都須要你安裝一個loader。所以,爲每一個目標瀏覽器輸出相同的內容(或者至少保持對assets的相同引用)很是重要。
  • 某些loaders容許你禁用文件發射。這在差別化服務構建中頗有用。例如,假設你用file-loader處理導入非JavaScript資源。在現代瀏覽器的配置中,你能夠告訴file-loader輸出文件,而在舊版瀏覽器的配置中,你能夠指定emitFile:false以防止文件被寫入磁盤兩次。這可能有助於提升速度。
  • null-loader 對於使用多個配置來控制文件的加載和發送也多是有用的。
  • 當心哈希版本的assets。假設你使用圖像優化加載程序(例如image-webpack-loader)來優化圖像。你可能須要在兩種配置中使用該loader,由於一個assets圖將包含對未優化圖像的引用,另外一個將包含對優化圖像的引用。因爲每一個構建的文件內容不一樣,所以文件哈希也會不一樣。結果是一組用戶將得到未優化的圖像assets,而其他用戶將得到優化的圖像assets。
  • 插件是另外一個徹底不一樣的東西,在差別化服務設置中使用它們的最佳指導方式各不相同。例如,若是你使用copy-webpack-plugin將文件或整個目錄從src複製到到dist,你只須要它在一個配置中,而不是兩個。也就是說,在兩種配置中使用相同的插件不會致使問題,但可能會影響構建速度。
  • 若是你的loader和插件配置開始有點雜亂,npm腳本能夠是一個很好的替代品。對於簡單的項目,我常常經過npm在個人項目本地安裝圖像優化二進制文件(例如pngquant-bin),並在構建完成後在npm腳本中使用npx來完成這項工做。這減小了個人webpack配置中的混亂,這是一個讓人樂於接受的改變。
  • 若是你正在使用assets-webpack-plugin爲兩個版本生成assets清單,那麼事情就會變得複雜。你須要建立單個實例以傳遞到每一個配置的插件數組並遵循此建議。在個人一個項目中,我在Node腳本中使用assets-webpack-plugin將腳本引用注入到生成的HTML中(稍後會詳細介紹)。

這些要點的要點是,你應該在構建之間保持assets引用的並行性,而且一般須要避免屢次將相同的assets寫入磁盤。可是,在可能已經很複雜的構建環境中也作了合理和方便的事情。

管理你的uglifier

直到最近, uglify-js仍是webpack的默認uglifier。在4.26.0版本terser成爲了默認設置。若是你使用的是版本4.26.0或更高版本,那麼對你來講是好消息 - 你已經完成全部設置,你無需再作任何額外的構建工做!

可是,若是你使用的是早期版本,則terser不是 默認的uglifier,而是uglify-js,你須要在你的新的配置中使用terser。這是由於uglify-js沒法理解ES5以外的JavaScript語法。它沒法識別像箭頭函數,async/ await,等等這些東西。

對於你的舊配置,你不須要作任何事情,由於它應該已經構建好了。可是,對於你新的配置,你須要爲你的項目npm install terser-webpack-plugin。而後你須要添加terser-webpack-pluginoptimization.minimizer數組:

// webpack.config.js
const TerserWebpackPlugin = require("terser-webpack-plugin");

const modernConfig = {
  // ...
  optimization: {
    minimizer: [
      new TerserWebpackPlugin({
        test: /\.m?js$/i, // 若是你要輸出.mjs文件,就須要這個
        terserOptions: {
          ecma: 6 // 也能夠是7或8
        }
      })
    ]
  }
  // ...
};
複製代碼

在咱們新的配置中,咱們輸出帶.mjs擴展名的文件。爲了讓terser可以識別和修改這些文件,咱們須要相應地修改test正則表達式。咱們還須要將ecma選項設置爲6(儘管78也是有效值)。

將腳本引用注入HTML

你也許使用html-webpack-plugin來爲你的應用程序外殼(app shell)標籤處理生成的HTML文件,這是有充分理由的。它是一個靈活的插件,能夠處理大量關於在HTML模板中插入<link><script>標記的繁忙工做。不幸的是,它的<script>標籤插入方式不支持差別化服務。你能夠自行決定如何將這些腳本引用添加到HTML文件中。

幸運的是,只須要稍微動動腦就能夠解決這個問題。對於我使用差別化服務的項目,我使用assets-webpack-plugin用來收集webpack生成的assets,以下所示:

// webpack.config.js
const AssetsWebpackPlugin = require("assets-webpack-plugin");

const assetsWebpackPluginInstance = new AssetsWebpackPlugin({
  filename: "assets.json",
  update: true,
  fileTypes: [
    "js",
    "mjs"
  ]
});
複製代碼

從這裏開始,我將舊的和新的配置中的這個assets-webpack-plugin實例添加到plugins數組中。我已經使用如下配置插件實例選項來適配個人項目:

  • filename 指示應將assets JSON文件輸出到的位置。
  • 我設置updatetrue,它告訴插件爲新的和舊的配置重用相同的assets JSON文件。
  • 我更新fileTypes以確保在assets.json中包含新的配置生成的.mjs文件

這裏開始,它就是觸摸hacky的地方。爲了讓<script>在個人HTML文件中引用想要的模式,我使用了一個在webpack完成後運行的npm腳本。此腳本讀取由assets-webpack-plugin生成的assets.json文件並在正確的標記中進行操做。

但願html-webpack-plugin可以原生支持這一點,由於我我的不但願使用這種方法。你也許須要設計本身的臨時解決方案。

這樣值得嗎?

這篇文章你已經讀了一半了,我確信這個問題仍然存在:這種技術是否值得?個人回答是一個響亮的確定。我在個人網站上使用差別化服務,我認爲這些好處不言自明:

全部的JS assets gzip(level 9) Brotli(level 11)
Legacy 112.14 KB 38.6 KB 33.58 KB
Modern 34.23 KB 12.94 KB 12.12 KB

對於個人網站來講,JavaScript大小減小了近70%。公平地說,隨着打包規模的擴大,我注意到差別化服務所帶來的節省明顯減小了。在個人工做中,我常常遇到超過300 KB的捆綁包,我已經看到接近10%的東西,但這仍然是一個顯着的減小!對我有利的大部分緣由是個人特定項目須要至關大量的polyfill用於傳統打包,而在不多甚至沒有polyfill或現代打包的變換中個人項目就能夠之間跳過這一步。

查看壓縮統計數據並說它可有可無也可能很誘人,但你必須始終牢記壓縮只會下降給定assets的傳輸時間。壓縮對解析/編譯/執行時間沒有影響。若是使用Brotli將100 KB JavaScript文件壓縮到30 KB,是的,用戶將很快收到它,但該文件仍然至關於100 KB的JavaScript。

在具備較低處理能力和內存的設備上,這是一個相當重要的區別。我常常在諾基亞2 Android手機上進行測試,差別化服務對加載性能的影響很明顯。如下是我在實施差別化服務以前訪問個人我的網站的Chrome設備中的性能跟蹤:

Chrome的DevTools中的網站性能跟蹤顯示在實施差別化服務以前的大量腳本活動。

Chrome的DevTools中的網站性能跟蹤顯示在實施差別化服務以前的大量腳本活動。

下面是在差別化服務部署到位後它在同一設備上的表現:

Chrome的DevTools中的網站性能跟蹤顯示差別化服務實施後腳本活動量大大減小。

Chrome的DevTools中的網站性能跟蹤顯示差別化服務實施後腳本活動量大大減小。

腳本活動減小約66%是可靠的。當網站更快地得到互動時,它們對每一個人來講都更加實用和愉快。

結論

差別化服務是好東西。若是來自HTTPArchive的這種趨勢是任何指標,那麼生產中的大多數網站仍然會傳送大量的polyfilled和轉換後的遺留JS,這是不少用戶根本不須要的。若是咱們須要在舊版瀏覽器上支持用戶,咱們應該認真考慮這種左右開弓的方法來提供JavaScript。

若是不出意外,這應該會在將來觀察JavaScript的打包大小,以及如何保持對JavaScript工具在減小咱們發送給用戶的代碼方面的前瞻性態度上發揮做用。根據你的受衆,你甚至可能不須要提供兩個不一樣的捆綁包,但展現的配置可能會讓你瞭解如何發送比當前更少的代碼。

值得注意的是,JavaScript工具的狀態常常發生變化。感受很是像一個「快速移動和破壞東西」的空間。例如,webpack 5的alpha已經存在,而且須要進行大量更改。假設某些事情可能會壞掉並非不合理的。有趣的是,我仍然在個人工做中看到webpack 3上的項目。升級某些項目可能須要比其餘項目更多時間。這種技術 - 如文檔所述 - 在將來仍然有用。

若是你有興趣看到我如今使用此技術的網站,請查看此repo。但願你能從個人項目中學到儘量多的東西。

資源

相關文章
相關標籤/搜索