阅读 296

[译] 如何根据浏览器的现代、过时进行包的分发

原文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

扩展阅读

关注下面的标签,发现更多相似文章
评论