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

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

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.css

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

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 代碼,以及引入墊片庫。java

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?webpack

從歷史的角度看這是能夠理解的——Web 其實老是在漸進式地加強的——但問題是:爲了支持正在快速衰減的過期瀏覽器,咱們真的須要把 Web 的速度下降而影響到咱們大多數用戶的嗎?git

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:es6

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

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:web

爲了評估出轉譯步驟會對 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

擴展閱讀

相關文章
相關標籤/搜索