原文: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
Let’s try to understand how different steps in a typical build pipeline can add weight to our front-end resources:es6
咱们先来看看,一个典型构建过程当中的各个步骤是如何把咱们的前端资源的体积加大的:github
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+ 拥有更简约的和表现力的方式来表达同等的逻辑,而这些须要转译的特性中某些则须要许多的代码来实现。
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+ 自带的一些特性——如 Promise
、Map
、Set
,以及数组和字符串的一些新方法——仍然须要加上垫片库。若是放入 babel-polyfill
这个库的话,在精简后的代码将增长将近 90KB 的大小。
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 URL
specification, 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.
对于这些特性,若是为他们添加垫片库的话,会对打包后的体积形成可观的影响。
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% |
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.
若是咱们能够利用现有的构建流程来实现只为有须要的浏览器分发兼容性层,那么咱们就能够为咱们的其余用户(指正在不断上升的用户群体)带来更轻快的体验,同时还兼顾了旧浏览器的兼容性。
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.
下面咱们来看下咱们能够怎样解决目前咱们所列出来的致使体积增长的问题。
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 的一些辅助插件。
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.
首先,咱们先来划分清楚浏览器的“现代”和“过期”的含义。为了方便维护和测试,把浏览器分为具体的两类会颇有帮助:不须要垫片和转译的划入“现代”一组;其他的划入“过期”一组。
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.
上面列出的只是一个示例,你能够基于你网站的须要来自定义。这段配置就是接下来咱们要建立的两组前端包的源头依据:一个针对现代浏览器,另外一个针对全部其余用户。
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";
…
复制代码
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 加入到项目中来减小这个过程发成错误的可能性。这个插件会在咱们使用到尚未加入垫片的浏览器特性时发出警告。
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 browserslist
configuration 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') ],
}
复制代码
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/modern
和 dist/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
命令,咱们应该能够获得两种版本了,他们在功能上是同等的。
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 不能正常解析的浏览器,咱们仍然能够经过用过期版本的包来替代,以此确保这种状况下咱们的网站仍能工做。
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:
上面咱们忙着讲解一次“端到端”的包分发过程;但只有咱们思考了这样作所带来的好处是否值得其给项目的维护所需的成本时才有理由进行应用。下面咱们来评估一下这种方法的正面和负面:
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.
虽然由这两包之间的不一样致使问题产生的状况应该是极少的,但对他们之间的区别进行监控仍是有助于识别并高效地消除问题的。
Unlike other techniques prevalent today, all of these optimizations occur at build time and are invisible to the client.
与如今流行的其余技术不同,全部的这些优化都是发生在构建时阶段,对客户端来讲是不可见的。
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.
现代浏览器的用户感觉到的是明显更快的体验,而过期浏览器的用户则继续接受到的是以前同样的包,不会产生任何反作用。
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.
考虑到使用垫片的尺寸,咱们一般会避免使用新浏览器特性。或者有时候,咱们会选择那些尺寸更小的但不彻底符合规范的垫片来节省体积。这种新的方式可让咱们使用符合规范的垫片的同时又不用担忧对全部用户都形成影响。
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% 以上都是“常青浏览器”,因此这点付出相对于它带来的影响仍是很是值得的。
@babel/preset-env
A smart Babel preset(一款智能的 Babel preset)