智能捆绑:如何仅向旧版浏览器提供旧版代码

已发表: 2022-03-10
快速总结 ↬尽管在网络上有效地捆绑资源近来得到了广泛的关注,但我们向用户提供前端资源的方式几乎没有改变。 网站附带的 JavaScript 和样式资源的平均权重正在上升——尽管用于优化网站的构建工具从未像现在这样好。 随着常青浏览器的市场份额快速增长以及浏览器同步推出对新功能的支持,我们是时候重新考虑现代网络的资产交付了吗?

今天的网站从常青浏览器那里获得了很大一部分流量——其中大部分都很好地支持了 ES6+、新的 JavaScript 标准、新的 Web 平台 API 和 CSS 属性。 但是,在不久的将来仍需要支持旧版浏览器——它们的使用份额足够大,不容忽视,具体取决于您的用户群。

快速浏览一下 caniuse.com 的使用表就会发现,常青浏览器占据了浏览器市场的最大份额——超过 75%。 尽管如此,规范是为 CSS 加上前缀,将我们所有的 JavaScript 转换为 ES5,并包含 polyfill 以支持我们关心的每个用户。

尽管从历史背景来看这是可以理解的——网络一直是关于渐进增强的——但问题仍然存在:我们是否会为大多数用户减慢网络速度以支持数量不断减少的旧版浏览器?

转译为 ES5、Web 平台 polyfills、ES6+ polyfills、CSS 前缀
Web 应用程序的不同兼容性层。 (查看大图)

支持旧版浏览器的成本

让我们尝试了解典型构建管道中的不同步骤如何增加我们前端资源的权重:

转译为 ES5

为了估计转译可以为 JavaScript 包增加多少权重,我选取了一些最初用 ES6+ 编写的流行 JavaScript 库,并比较了它们转译前后的包大小:

图书馆尺寸
(缩小的 ES6)
尺寸
(缩小的 ES5)
区别
TodoMVC 8.4 KB 11 KB 24.5%
可拖动53.5 KB 77.9 KB 31.3%
卢克森75.4 KB 100.3 KB 24.8%
视频.js 237.2 KB 335.8 KB 29.4%
PixiJS 370.8 KB 452 KB 18%

平均而言,未转译的包比已转译到 ES5 的包小 25%。 这并不奇怪,因为 ES6+ 提供了一种更紧凑和更具表现力的方式来表示等效逻辑,并且将其中一些功能转换为 ES5 可能需要大量代码。

ES6+ 填充物

虽然 Babel 在将语法转换应用到我们的 ES6+ 代码方面做得很好,但 ES6+ 中引入的内置特性——例如PromiseMapSet ,以及新的数组和字符串方法——仍然需要填充。 按原样放入babel-polyfill可以为您的压缩包增加近 90 KB。

跳跃后更多! 继续往下看↓

Web 平台 Polyfills

由于大量新的浏览器 API 的可用性,现代 Web 应用程序开发得到了简化。 常用的有fetch ,用于请求资源, IntersectionObserver ,用于有效地观察元素的可见性,以及URL规范,这使得在 Web 上读取和操作 URL 更加容易。

为这些功能中的每一个添加符合规范的 polyfill 会对包大小产生显着影响。

CSS 前缀

最后,让我们看看 CSS 前缀的影响。 虽然前缀不会像其他构建转换那样为捆绑包增加太多的自重——尤其是因为它们在 Gzip 时压缩得很好——但这里仍有一些节省空间。

图书馆尺寸
(缩小,最后 5 个浏览器版本的前缀)
尺寸
(缩小,最后一个浏览器版本的前缀)
区别
引导程序159 KB 132 KB 17%
布尔玛184 KB 164 KB 10.9%
基础139 KB 118 KB 15.1%
语义用户界面622 KB 569 KB 8.5%

发布高效代码的实用指南

这可能很明显我要去哪里。 如果我们利用现有的构建管道将这些兼容性层仅提供给需要它的浏览器,我们就可以为其他用户(占多数的用户)提供更轻松的体验,同时保持对旧浏览器的兼容性。

现代包比旧包小,因为它放弃了一些兼容性层。
分叉我们的捆绑包。 (查看大图)

这个想法并不是全新的。 Polyfill.io 等服务试图在运行时动态填充浏览器环境。 但是像这样的方法有一些缺点:

  • polyfill 的选择仅限于服务列出的那些——除非您自己托管和维护服务。
  • 因为 polyfill 发生在运行时并且是一个阻塞操作,所以旧浏览器上的用户的页面加载时间可能会显着增加。
  • 为每个用户提供一个定制的 polyfill 文件会给系统带来熵,当出现问题时,这使得故障排除变得更加困难。

此外,这并不能解决应用程序代码转换所增加的权重问题,有时可能比 polyfill 本身更大。

让我们看看我们如何解决到目前为止我们已经确定的所有臃肿来源。

我们需要的工具

  • 网页包
    这将是我们的构建工具,尽管该过程将与其他构建工具类似,例如 Parcel 和 Rollup。
  • 浏览器列表
    有了这个,我们将管理和定义我们想要支持的浏览器。
  • 我们将使用一些Browserslist 支持插件

1. 定义现代和传统浏览器

首先,我们要明确“现代”和“传统”浏览器的含义。 为了便于维护和测试,它有助于将浏览器分为两个独立的组:将几乎不需要 polyfill 或 transpilation 的浏览器添加到我们的现代列表中,并将其余浏览器放在我们的旧列表中。

火狐 >= 53;边缘 >= 15;铬 >= 58; iOS >= 10.1
支持 ES6+、新 CSS 属性和浏览器 API(如 Promises 和 Fetch)的浏览器。 (查看大图)

项目根目录下的 Browserslist 配置可以存储此信息。 “环境”小节可用于记录两个浏览器组,如下所示:

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

此处提供的列表只是一个示例,可以根据您网站的要求和可用时间进行定制和更新。 此配置将作为我们接下来将创建的两组前端包的真实来源:一组用于现代浏览器,一组用于所有其他用户。

2. ES6+ 转译和Polyfilling

为了以环境感知的方式编译我们的 JavaScript,我们将使用babel-preset-env

让我们在项目的根目录初始化一个.babelrc文件:

 { "presets": [ ["env", { "useBuiltIns": "entry"}] ] }

启用useBuiltIns标志允许 Babel 选择性地填充作为 ES6+ 的一部分引入的内置功能。 因为它过滤了 polyfill 以仅包含环境所需的那些,所以我们降低了使用babel-polyfill整体运输的成本。

为了使这个标志起作用,我们还需要在入口点导入babel-polyfill

 // In import "babel-polyfill";

这样做会用细粒度的导入替换大的babel-polyfill导入,由我们定位的浏览器环境过滤。

 // Transformed output 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 平台功能

要将 web 平台功能的 polyfills 发送给我们的用户,我们需要为这两种环境创建两个入口点:

 require('whatwg-fetch'); require('es6-promise').polyfill(); // … other polyfills

和这个:

 // polyfills for modern browsers (if any) require('intersection-observer');

这是我们流程中唯一需要一定程度手动维护的步骤。 我们可以通过在项目中添加 eslint-plugin-compat 来减少这个过程出错的可能性。 当我们使用尚未填充的浏览器功能时,此插件会警告我们。

4.CSS前缀

最后,让我们看看如何为不需要它的浏览器减少 CSS 前缀。 因为autoprefixer是生态系统中支持从browserslist配置文件读取的首批工具之一,所以我们在这里没有太多工作要做。

在项目的根目录创建一个简单的 PostCSS 配置文件就足够了:

 module.exports = { plugins: [ require('autoprefixer') ], }

把它们放在一起

现在我们已经定义了所有必需的插件配置,我们可以组合一个 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', }), }, };

最后,我们将在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" }

而已。 运行yarn build现在应该给我们两个构建,它们在功能上是等效的。

为用户提供正确的捆绑包

创建单独的构建只能帮助我们实现目标的前半部分。 我们仍然需要识别并向用户提供正确的捆绑包。

还记得我们之前定义的 Browserslist 配置吗? 如果我们可以使用相同的配置来确定用户属于哪个类别,那不是很好吗?

输入 browserslist-useragent。 顾名思义, browserslist-useragent可以读取我们的browserslist配置,然后将用户代理与相关环境相匹配。 以下示例使用 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); });

在这里,设置allowHigherVersions标志可确保如果发布了较新版本的浏览器(尚未包含在 Can I Use 的数据库中的浏览器),它们仍将报告为现代浏览器的真实版本。

browserslist-useragent的功能之一是确保在匹配用户代理时考虑平台怪癖。 例如,iOS 上的所有浏览器(包括 Chrome)都使用 WebKit 作为底层引擎,并将匹配到各自的 Safari 特定 Browserslist 查询。

仅仅依靠生产中用户代理解析的正确性可能并不谨慎。 通过回退到未在现代列表中定义或具有未知或不可解析的用户代理字符串的浏览器的旧捆绑包,我们确保我们的网站仍然有效。

结论:值得吗?

我们已经设法为我们的客户提供了一个端到端的流程,用于向我们的客户运送无膨胀的捆绑包。 但是有理由怀疑这给项目增加的维护开销是否值得它的好处。 让我们评估一下这种方法的优缺点:

1. 维护和测试

只需要维护一个为该管道中的所有工具提供支持的 Browserslist 配置。 将来可以随时更新现代和旧版浏览器的定义,而无需重构支持配置或代码。 我认为这使得维护开销几乎可以忽略不计。

然而,依赖 Babel 生成两个不同的代码包存在一个小的理论风险,每个代码包都需要在各自的环境中正常工作。

虽然由于捆绑包中的差异而导致的错误可能很少见,但监控这些变体的错误应该有助于识别和有效缓解任何问题。

2. 构建时间与运行时间

与当今流行的其他技术不同,所有这些优化都发生在构建时并且对客户端是不可见的。

3. 逐步提高速度

现代浏览器上的用户体验变得明显更快,而旧版浏览器上的用户继续获得与以前相同的捆绑服务,没有任何负面后果。

4. 轻松使用现代浏览器功能

由于使用它们所需的 polyfill 的大小,我们经常避免使用新的浏览器功能。 有时,我们甚至会选择更小的不符合规范的 polyfill 来节省大小。 这种新方法允许我们使用符合规范的 polyfill,而不必担心会影响所有用户。

生产中的差异化捆绑服务

鉴于显着优势,我们在为印度最大的家具和装饰零售商之一 Urban Ladder 的客户创建新的移动结账体验时采用了此构建管道。

在我们已经优化的捆绑包中,我们能够节省大约 20% 的 Gzip'd CSS 和 JavaScript 资源,这些资源通过网络发送给现代移动用户。 因为我们 80% 以上的日常访问者都使用这些常青浏览器,所以付出的努力是值得的。

更多资源

  • “仅在需要时加载 Polyfills”,Philip Walton
  • @babel/preset-env
    一个智能的 Babel 预设
  • 浏览器列表“工具”
    为 Browserslist 构建的插件生态系统
  • 我可以用吗
    当前浏览器市场份额表