前端遇上Go: 静态资源增量更新的新实践
作者:网友投稿 时间:2018-07-12 09:17
为什么要做增量更新
美团金融的业务在过去的一段时间里发展非常快速。在业务增长的同时,我们也注意到,很多用户的支付环境,其实是在弱网环境中的。
大家知道,前端能够服务用户的前提是 JavaScript 和 CSS 等静态资源能够正确加载。如果网络环境恶劣,那么我们的静态资源尺寸越大,用户下载失败的概率就越高。
根据我们的数据统计,我们的业务中有2%的用户流失与资源加载有关。因此每次更新的代价越小、加载成功率越高,用户流失率也就会越低,从而就能够变相提高订单的转化率。
作为一个发版频繁的业务,要降低发版的影响,可以做两方面优化:
更高效地使用缓存,减少静态资源的重复下载。
使用增量更新,降低单次发版时下发的内容尺寸。
针对第一点,我们有自己的模块加载器来做,这里先按下不表,我们来重点聊聊增量更新的问题。
增量更新是怎么一个过程
看图说话。

增量更新的客户端流程图
我们的增量更新通过在浏览器端部署一个 SDK 来发起,这个 SDK 我们称之为 Thunder.js 。
Thunder.js 在页面加载时,会从页面中读取最新静态资源的版本号。同时, Thunder.js 也会从浏览器的缓存(通常是 localStorage)中读取我们已经缓存的版本号。这两个版本号进行匹配,如果发现一致,那么我们可以直接使用缓存当中的版本;反之,我们会向增量更新服务发起一个增量补丁的请求。
增量服务收到请求后,会调取新旧两个版本的文件进行对比,将差异作为补丁返回。Thunder.js 拿到请求后,即可将补丁打在老文件上,这样就得到了新文件。
总之一句话:老文件 + 补丁 = 新文件。
增量补丁的生成,主要依赖于 Myers 的 diff 算法。生成增量补丁的过程,就是寻找两个字符串最短编辑路径的过程。算法本身比较复杂,大家可以在网上找一些比较详细的算法描述,比如这篇 《The Myers diff algorithm》,这里就不详细介绍了。
补丁本身是一个微型的 DSL(Domain Specific Language)。这个 DSL 一共有三种微指令,分别对应保留、插入、删除三种字符串操作,每种指令都有自己的操作数。
例如,我们要生成从字符串“abcdefg”到“acdz”的增量补丁,那么一个补丁的全文就类似如下:
=1\t-1\t=2\t-3\t+z
这个补丁当中,制表符\t是指令的分隔符,=表示保留,-表示删除,+表示插入。整个补丁解析出来就是:
保留1个字符
删除1个字符
保留2个字符
删除3个字符
插入1个字符:z
具体的 JavaScript 代码就不在这里粘贴了,流程比较简单,相信大家都可以自己写出来,只需要注意转义和字符串下标的维护即可。
增量更新其实不是前端的新鲜技术,在客户端领域,增量更新早已经应用多年。看过我们《美团金融扫码付静态资源加载优化实践》的朋友,应该知道我们其实之前已有实践,在当时仅仅靠增量更新,日均节省流量达30多GB。而现在这个数字已经随着业务量变得更高了。
那么我们是不是就已经做到万事无忧了呢?
我们之前的增量更新实践遇到了什么问题
我们最主要的问题是增量计算的速度不够快。
之前的优化实践中,我们绝大部分的优化其实都是为了优化增量计算的速度。文本增量计算的速度确实慢,慢到什么程度呢?以前端比较常见的JS资源尺寸——200KB——来进行增量计算,进行一次增量计算的时间依据文本不同的数量,从数十毫秒到十几秒甚至几十秒都有可能。
对于小流量业务来说,计算一次增量补丁然后缓存起来,即使第一次计算耗时一些也不会有太大影响。但用户侧的业务流量都较大,每月的增量计算次数超过 10 万次,并发计算峰值超过 100 QPS 。
那么不够快的影响是什么呢?

我们之前的设计大致思想是用一个服务来承接流量,再用另一个服务来进行增量计算。这两个服务均由 Node.js 来实现。对于前者, Node.js 的事件循环模型本就适合进行 I/O 密集型业务;然而对于后者,则实际为 Node.js 的软肋。 Node.js 的事件循环模型,要求 Node.js 的使用必须时刻保证 Node.js 的循环能够运转,如果出现非常耗时的函数,那么事件循环就会陷入进去,无法及时处理其他的任务。常见的手法是在机器上多开几个 Node.js 进程。然而一台普通的服务器也就8个逻辑CPU而已,对于增量计算来说,当我们遇到大计算量的任务时,8个并发可能就会让 Node.js 服务很难继续响应了。如果进一步增加进程数量,则会带来额外的进程切换成本,这并不是我们的最优选择。
更高性能的可能方案
“让 JavaScript 跑的更快”这个问题,很多前辈已经有所研究。在我们思考这个问题时,考虑过三种方案。
Node.js Addon




