[譯] 如何根據瀏覽器的現代、過時進行包的分發

西樓聽雨發表於2018-11-24

原文Smart Bundling: How To Serve Legacy Code Only To Legacy Browsers
作者shubham kanodia 發表時間:october 15, 2018
譯者:西樓聽雨 發表時間: 2018/11/24 (轉載請註明出處)

A website today receives a large chunk of its traffic from evergreen browsers — most of which have good support for ES6+, new JavaScript standards, new web platform APIs and CSS attributes. However, legacy browsers still need to be supported for the near future — their usage share is large enough not to be ignored, depending on your user base.

現今的網站,很大一部分流量都是來自“常青瀏覽器”(指自動更新、時刻保持與最新技術同步的瀏覽器,如 Chrome、Firefox,這裡就是指現代化的瀏覽器——譯註),這些瀏覽器大部分都對 ES6+、新 JavaScript 標準、新的 Web 平臺 API 及 CSS 屬性有良好的支援。然而,那些過時的瀏覽器在近期還是需要被支援——他們所佔有的比例還不足以被忽視,主要是看你的目標客戶群體是哪些。

A quick look at caniuse.com’s usage table reveals that evergreen browsers occupy a lion’s share of the browser market — more than 75%. In spite of this, the norm is to prefix CSS, transpile all of our JavaScript to ES5, and include polyfills to support every user we care about.

概覽一下 caniuse.com 網站上揭示的瀏覽器使用情況表,可以看到“常青類瀏覽器”佔了極大的一塊瀏覽器市場份額——超過了 75%。儘管如此,我們的標準做法還是會為我們所關心的使用者加上 CSS 字首,把 JavaScript 轉譯為 ES5 程式碼,以及引入墊片庫。

While this is understandable from a historical context — the web has always been about progressive enhancement — the question remains: Are we slowing down the web for the majority of our users in order to support a diminishing set of legacy browsers?

從歷史的角度看這是可以理解的——Web 其實總是在漸進式地增強的——但問題是:為了支援正在快速衰減的過時瀏覽器,我們真的需要把 Web 的速度降低而影響到我們大多數使用者的嗎?

Transpilation to ES5, web platform polyfills, ES6+ polyfills, CSS prefixing

The Cost Of Supporting Legacy Browsers

支援過時瀏覽器的代價

Let’s try to understand how different steps in a typical build pipeline can add weight to our front-end resources:

我們先來看看,一個典型構建過程中的各個步驟是如何把我們的前端資源的體積加大的:

TRANSPILING TO ES5

轉譯為 ES5

To estimate how much weight transpiling can add to a JavaScript bundle, I took a few popular JavaScript libraries originally written in ES6+ and compared their bundle sizes before and after transpilation:

為了評估出轉譯步驟會對 JavaScript 打包後的體積的增加有多大影響,我找了幾個用 ES6+ 寫的流行的JavaScript 庫,對比了他們在轉譯前後的打包後的體積:

JS 庫 體積 (精簡後的 ES6) 體積 (精簡後的 ES5) 差異
TodoMVC 8.4 KB 11 KB 24.5%
Draggable 53.5 KB 77.9 KB 31.3%
Luxon 75.4 KB 100.3 KB 24.8%
Video.js 237.2 KB 335.8 KB 29.4%
PixiJS 370.8 KB 452 KB 18%

On average, untranspiled bundles are about 25% smaller than those that have been transpiled down to ES5. This isn’t surprising given that ES6+ provides a more compact and expressive way to represent the equivalent logic and that transpilation of some of these features to ES5 can require a lot of code.

總體來看,未經轉譯的包相較轉譯後的小了大約 25%。這一點沒什麼意外的,因為 ES6+ 擁有更簡約的和表現力的方式來表達同等的邏輯,而這些需要轉譯的特性中某些則需要許多的程式碼來實現。

ES6+ POLYFILLS

ES6+ 墊片庫

While Babel does a good job of applying syntactical transforms to our ES6+ code, built-in features introduced in ES6+ — such as Promise, Map and Set, and new array and string methods — still need to be polyfilled. Dropping in babel-polyfill as is can add close to 90 KB to your minified bundle.

雖然 Babel 可以很好地將 ES6+ 程式碼進行語法轉換,但 ES6+ 自帶的一些特性——如 PromiseMapSet,以及陣列和字串的一些新方法——仍然需要加上墊片庫。如果放入 babel-polyfill 這個庫的話,在精簡後的程式碼將增加將近 90KB 的大小。

WEB PLATFORM POLYFILLS

WEb 平臺墊片庫

Modern web application development has been simplified due to the availability of a plethora of new browser APIs. Commonly used ones are fetch, for requesting for resources, IntersectionObserver, for efficiently observing the visibility of elements, and the URLspecification, which makes reading and manipulation of URLs on the web easier.

由於新瀏覽器 API 的過剩,現代 Web 應用的開發已經變得簡單了。常用的就是 fetch(用於請求資源),IntersectionObserver (用於高效地監測元素可見性),以及 URL 規範(方便了 Web 中對 URL 的讀取和操作)。

Adding a spec-compliant polyfill for each of these features can have a noticeable impact on bundle size.

對於這些特性,如果為他們新增墊片庫的話,會對打包後的體積造成可觀的影響。

CSS PREFIXING

CSS 字首

Lastly, let’s look at the impact of CSS prefixing. While prefixes aren’t going to add as much dead weight to bundles as other build transforms do — especially because they compress well when Gzip’d — there are still some savings to be achieved here.

最後我們來看下新增 CSS 字首的影響。雖然相比其他轉換,字首不會對打包體積有非常嚴重的影響——特別是其經 Gzip 壓縮後——但仍還是有一些可以節省的空間。

體積 (精簡了的, 為最近 5個版本的瀏覽器附加了字首的) 體積 (精簡了的, 為最新瀏覽器附加了字首了的) 差異
Bootstrap 159 KB 132 KB 17%
Bulma 184 KB 164 KB 10.9%
Foundation 139 KB 118 KB 15.1%
Semantic UI 622 KB 569 KB 8.5%

A Practical Guide To Shipping Efficient Code

實用性的高效程式碼分發指導

It’s probably evident where I’m going with this. If we leverage existing build pipelines to ship these compatibility layers only to browsers that require it, we can deliver a lighter experience to the rest of our users — those who form a rising majority — while maintaining compatibility for older browsers.

如果我們可以利用現有的構建流程來實現只為有需要的瀏覽器分發相容性層,那麼我們就可以為我們的其他使用者(指正在不斷上升的使用者群體)帶來更輕快的體驗,同時還兼顧了舊瀏覽器的相容性。

The modern bundle is smaller than the legacy bundle because it forgoes some compatibility layers.

This idea isn’t entirely new. Services such as Polyfill.io are attempts to dynamically polyfill browser environments at runtime. But approaches such as this suffer from a few shortcomings:

這並不是什麼新出現的想法。像 Polyfill.io 這類服務正在嘗試的就是根據瀏覽器執行時環境來動態加入墊片。但是這類方式有以下缺陷:

  • The selection of polyfills is limited to those listed by the service — unless you host and maintain the service yourself.

    墊片的選擇侷限於服務本身所擁有的墊片——除非你自己架設並維護這個服務。

  • Because the polyfilling happens at runtime and is a blocking operation, page-loading time can be significantly higher for users on old browsers.

    因為墊片的引入過程發生在執行時,是一種阻塞操作,在老的瀏覽器中會造成使用者的頁面載入時間嚴重升高。

  • Serving a custom-made polyfill file to every user introduces entropy to the system, which makes troubleshooting harder when things go wrong.

    引入一個自制的墊片庫檔案會增加這套系統的不穩定性,當出現故障時,會使得問題的解決變得困難。

Also, this doesn’t solve the problem of weight added by transpilation of the application code, which at times can be larger than the polyfills themselves.

另外,這並不能解決轉譯對我們應用程式碼體積增加造成影響的問題,這個影響有時甚至可能比墊片本身還大。

Let see how we can solve for all of the sources of bloat we’ve identified till now.

下面我們來看下我們可以怎樣解決目前我們所列出來的導致體積增加的問題。

Tools We’ll Need

我們將用到的工具

  • Webpack This will be our build tool, although the process will remain similar to that of other build tools, like Parcel and Rollup.

    我們將用它作為構建工具——其他構建工具,如 Parcel 、Rollup 與此類似。

  • Browserslist With this, we’ll manage and define the browsers we’d like to support.

    我們將用其來定義我們想要支援的瀏覽器。

  • And we’ll use some Browserslist support plugins.

    另外我們還會用到 Browserlist 的一些輔助外掛。

1. Defining Modern And Legacy Browsers

定義瀏覽器的“現代”和“過時”

First, we’ll want to make clear what we mean by “modern” and “legacy” browsers. For ease of maintenance and testing, it helps to divide browsers into two discrete groups: adding browsers that require little to no polyfilling or transpilation to our modern list, and putting the rest on our legacy list.

首先,我們先來劃分清楚瀏覽器的“現代”和“過時”的含義。為了方便維護和測試,把瀏覽器分為具體的兩類會很有幫助:不需要墊片和轉譯的劃入“現代”一組;其餘的劃入“過時”一組。

Firefox >= 53; Edge >= 15; Chrome >= 58; iOS >= 10.1

A Browserslist configuration at the root of your project can store this information. “Environment” subsections can be used to document the two browser groups, like so:

這些資訊可以儲存在位於專案根目錄的 Browserslist 的配置檔案中。該檔案中的 “Environment”(環境)部分就是用於描述這兩類瀏覽器的位置,像這樣:

[modern]
Firefox >= 53
Edge >= 15
Chrome >= 58
iOS >= 10.1

[legacy]
> 1%
複製程式碼

The list given here is only an example and can be customized and updated based on your website’s requirements and the time available. This configuration will act as the source of truth for the two sets of front-end bundles that we will create next: one for the modern browsers and one for all other users.

上面列出的只是一個示例,你可以基於你網站的需要來自定義。這段配置就是接下來我們要建立的兩組前端包的源頭依據:一個針對現代瀏覽器,另一個針對所有其他使用者。

2. ES6+ Transpiling And Polyfilling

ES6+ 轉譯和墊片

To transpile our JavaScript in an environment-aware manner, we’re going to use babel-preset-env.

為了將我們的 JavaScript 以環境相關的方式來進行轉譯,我們會使用 babel-preset-env

Let’s initialize a .babelrc file at our project’s root with this:

我們先在專案根目錄中初始化 .babelrc 檔案:

{
  "presets": [
    ["env", { "useBuiltIns": "entry"}]
  ]
}
複製程式碼

Enabling the useBuiltIns flag allows Babel to selectively polyfill built-in features that were introduced as part of ES6+. Because it filters polyfills to include only the ones required by the environment, we mitigate the cost of shipping with babel-polyfill in its entirety.

開啟 useBuiltIns 標識,可以讓 Babel 選擇性地引入 ES6+ 自帶特性的墊片。由於它可以進行過濾,只把環境所需要的墊片引入進來,所以我們可以避免整體引入 babel-polyfill 的代價。

For this flag to work, we will also need to import babel-polyfill in our entry point.

要讓這個標識起作用,我們還需要在我們的入口檔案中把 babel-polyfill 匯入。

// In
import "babel-polyfill";
複製程式碼

Doing so will replace the large babel-polyfill import with granular imports, filtered by the browser environment that we’re targeting.

這樣就可以根據目標瀏覽器環境把 babel-polyfill 這個大塊的匯入替換成小粒度的匯入:

// 轉換後的匯入
import "core-js/modules/es7.string.pad-start";
import "core-js/modules/es7.string.pad-end";
import "core-js/modules/web.timers";
…
複製程式碼

3. Polyfilling Web Platform Features

為 Web 平臺特性引入墊片

To ship polyfills for web platform features to our users, we will need to create two entry points for both environments:

為了給我們的使用者引入 Web 平臺的墊片,我們需要為兩種環境分別建立兩個入口點:

require('whatwg-fetch');
require('es6-promise').polyfill();
// … 其他墊片
複製程式碼

以及這個:

// polyfills for modern browsers (if any)
// 針對現代瀏覽器的墊片
require('intersection-observer');
複製程式碼

This is the only step in our flow that requires some degree of manual maintenance. We can make this process less error-prone by adding eslint-plugin-compat to the project. This plugin warns us when we use a browser feature that hasn’t been polyfilled yet.

這是我們的這個流程中唯一需要某種程度上手動維護的地方。我們可以把 eslint-plugin-compat 加入到專案中來減少這個過程發成錯誤的可能性。這個外掛會在我們使用到還沒有加入墊片的瀏覽器特性時發出警告。

4. CSS Prefixing

新增 CSS 字首

Finally, let’s see how we can cut down on CSS prefixes for browsers that don’t require it. Because autoprefixer was one of the first tools in the ecosystem to support reading from a browserslistconfiguration file, we don’t have much to do here.

最後,我們來看下如何為哪些不需要用到 CSS 字首的的瀏覽器踢掉它們。autoprefixer 是生態中出現的第一款這類工具,它支援從 browserslist 中讀取配置檔案,所以如果使用它的話,我們就不需要再多做什麼。

Creating a simple PostCSS configuration file at the project’s root should suffice:

在我們專案根目錄中建立一個 PostCSS 的配置檔案就夠了:

module.exports = {
  plugins: [ require('autoprefixer') ],
}
複製程式碼

Putting It All Together

拼接起來

Now that we’ve defined all of the required plugin configurations, we can put together a webpack configuration that reads these and outputs two separate builds in dist/modern and dist/legacy folders.

現在我們已經定義好了所有需要用到的外掛的配置,我們可以將把他們和 webpack 的配置放到一起,讓其讀取並分別在 dist/moderndist/legacy 中輸出兩個單獨版本。

const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const isModern = process.env.BROWSERSLIST_ENV === 'modern'
const buildRoot = path.resolve(__dirname, "dist")

module.exports = {
  entry: [
    isModern ? './polyfills.modern.js' : './polyfills.legacy.js',
    "./main.js"
  ],
  output: {
    path: path.join(buildRoot, isModern ? 'modern' : 'legacy'),
    filename: 'bundle.[hash].js',
  },
  module: {
    rules: [
      { test: /\.jsx?$/, use: "babel-loader" },
      {
        test: /\.css$/,
        use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader']
      }
    ]},
    plugins: {
      new MiniCssExtractPlugin(),
      new HtmlWebpackPlugin({
      template: 'index.hbs',
      filename: 'index.html',
    }),
  },
};
複製程式碼

To finish up, we’ll create a few build commands in our package.json file:

然後我們再在我們的 package.json 中建立幾條構建命令就可以了:

"scripts": {
  "build": "yarn build:legacy && yarn build:modern",
  "build:legacy": "BROWSERSLIST_ENV=legacy webpack -p --config webpack.config.js",
  "build:modern": "BROWSERSLIST_ENV=modern webpack -p --config webpack.config.js"
}
複製程式碼

That’s it. Running yarn build should now give us two builds, which are equivalent in functionality.

好了。現在執行 yarn build 命令,我們應該可以得到兩種版本了,他們在功能上是同等的。

Serving The Right Bundle To Users

將對應的包分發的對應的使用者

Creating separate builds helps us achieve only the first half of our goal. We still need to identify and serve the right bundle to users.

建立另外一個單獨包版本還只是達成了我們目標的一半。我們還需要對使用者進行識別並分發相應的包。

Remember the Browserslist configuration we defined earlier? Wouldn’t it be nice if we could use the same configuration to determine which category the user falls into?

還記前面我們定義的 Browserslist 配置嗎?如果我們在鑑別使用者所屬的瀏覽器分類時可以直接基於這個現有的配置來是不是很不錯呢?

Enter browserslist-useragent. As the name suggests, browserslist-useragent can read our browserslist configuration and then match a user agent to the relevant environment. The following example demonstrates this with a Koa server:

這就要講到 browserslist-useragent 了。從他的名字就可以看出,他可以讀取我們的 browsers

list 配置,並通過 user agent 來匹配對應的環境。下面這個例子使用的是 Koa 服務來對他進行的一個演示:

const Koa = require('koa')
const app = new Koa()
const send = require('koa-send')
const { matchesUA } = require('browserslist-useragent')
var router = new Router()

app.use(router.routes())

router.get('/', async (ctx, next) => {
  const useragent = ctx.get('User-Agent')  
  const isModernUser = matchesUA(useragent, {
      env: 'modern',
      allowHigherVersions: true,
   })
   const index = isModernUser ? 'dist/modern/index.html', 'dist/legacy/index.html'
   await send(ctx, index);
});
複製程式碼

Here, setting the allowHigherVersions flag ensures that if newer versions of a browser are released — ones that are not yet a part of Can I Use’s database — they will still report as truthy for modern browsers.

上面的 allowHigherVersions 標識是用來確保在出現新的瀏覽器版本時——就是那些還沒在 Can I Use 網站的資料庫中的瀏覽器——他們仍然能夠正確地報導為現代瀏覽器。

One of browserslist-useragent’s functions is to ensure that platform quirks are taken into account while matching user agents. For example, all browsers on iOS (including Chrome) use WebKit as the underlying engine and will be matched to the respective Safari-specific Browserslist query.

browserslist-useragent 的其中一個功能是可以考慮到一些平臺怪異點。如,所有 iOS 上的瀏覽器(包括 Chrome)都是使用 WebKit 作為低層引擎的,所以就會匹配到對應的 Safari 的 Browserslist 的條件。

It might not be prudent to rely solely on the correctness of user-agent parsing in production. By falling back to the legacy bundle for browsers that aren’t defined in the modern list or that have unknown or unparseable user-agent strings, we ensure that our website still works.

在生產環境中僅僅依賴於 user-agent 的解析可能還比較不嚴謹;但對於那些不在“現代”列表中的瀏覽器,或者那些未知的及 user-agent 不能正常解析的瀏覽器,我們仍然可以通過用過時版本的包來替代,以此確保這種情況下我們的網站仍能工作。

Conclusion: Is It Worth It?

總結:這樣做是否值得?

We have managed to cover an end-to-end flow for shipping bloat-free bundles to our clients. But it’s only reasonable to wonder whether the maintenance overhead this adds to a project is worth its benefits. Let’s evaluate the pros and cons of this approach:

上面我們忙著講解一次“端到端”的包分發過程;但只有我們思考了這樣做所帶來的好處是否值得其給專案的維護所需的成本時才有理由進行應用。下面我們來評估一下這種方法的正面和負面:

1. MAINTENANCE AND TESTING

維護和測試

One is required to maintain only a single Browserslist configuration that powers all of the tools in this pipeline. Updating the definitions of modern and legacy browsers can be done anytime in the future without having to refactor supporting configurations or code. I’d argue that this makes the maintenance overhead almost negligible.

Browserslist 的配置檔案是所有其他工具的基礎,而我們只需維護它這一個檔案,可以在未來的任意時刻更新“現代”和“過時”瀏覽器的定義,而不需要重構其他相關配置和程式碼。在我看來,它所帶來的維護成本完全可以忽略不計。

There is, however, a small theoretical risk associated with relying on Babel to produce two different code bundles, each of which needs to work fine in its respective environment.

不過,理論上還存在一個依賴 Babel 生成兩個不同程式碼包的風險——兩種包都需要在對應環境下正常工作。

While errors due to differences in bundles might be rare, monitoring these variants for errors should help to identify and effectively mitigate any issues.

雖然由這兩包之間的不同導致問題產生的情況應該是極少的,但對他們之間的區別進行監控還是有助於識別並高效地消除問題的。

2. BUILD TIME VS. RUNTIME

構建時對比執行時

Unlike other techniques prevalent today, all of these optimizations occur at build time and are invisible to the client.

與現在流行的其他技術不一樣,所有的這些優化都是發生在構建時階段,對客戶端來說是不可見的。

3. PROGRESSIVELY ENHANCED SPEED

漸進式的速度增強

The experience of users on modern browsers becomes significantly faster, while users on legacy browsers continue to get served the same bundle as before, without any negative consequences.

現代瀏覽器的使用者感受到的是明顯更快的體驗,而過時瀏覽器的使用者則繼續接受到的是之前一樣的包,不會產生任何副作用。

4. USING MODERN BROWSER FEATURES WITH EASE

輕鬆使用現代瀏覽器的特性

We often avoid using new browser features due to the size of polyfills required to use them. At times, we even choose smaller non-spec-compliant polyfills to save on size. This new approach allows us to use spec-compliant polyfills without worrying much about affecting all users.

考慮到使用墊片的尺寸,我們通常會避免使用新瀏覽器特性。或者有時候,我們會選擇那些尺寸更小的但不完全符合規範的墊片來節省體積。這種新的方式可以讓我們使用符合規範的墊片的同時又不用擔心對所有使用者都造成影響。

Differential Bundle Serving In Production

生產環境中情況

Given the significant advantages, we adopted this build pipeline when creating a new mobile checkout experience for customers of Urban Ladder, one of India’s largest furniture and decor retailers.

鑑於這種方式帶來的極大優勢,我們在為印度最大的傢俱和裝飾品零售商 Urban Ladder 建立一個新的移動端付款體驗時採用了這種構建流程。

In our already optimized bundle, we were able to squeeze savings of approximately 20% on the Gzip’d CSS and JavaScript resources sent down the wire to modern mobile users. Because more than 80% of our daily visitors were on these evergreen browsers, the effort put in was well worth the impact.

在我們優化過打包體積後,我們在現代化的移動端使用者上節省了將近 20% 的 Gzip 後的 CSS 和 JavaScript 資源消耗。因為平時我們的顧客 80% 以上都是“常青瀏覽器”,所以這點付出相對於它帶來的影響還是非常值得的。

FURTHER RESOURCES

擴充套件閱讀

相關文章