使用prelink优化跨域接口性能

背景

当页面中存在跨域接口调用时,该跨域域名下的第一个请求往往会相对比较慢。

自从 HTTP1.1 默认开启 keep-alive 后,如果页面上的 xhr 为同域下的请求,那么可以直接复用获取 html 时就建立好的连接。

但跨域请求没有前置的请求帮其建立连接,当页面上发起跨域请求时就需要额外的时间进行 dns 查询,ssl 以及建立 tcp 连接的时间,会给用户带来不太好的体验。

prelink:从百度小程序到离线包

百度小程序

最开始接触这个概念是在百度小程序的文档中,因为百度小程序相当于是离线的代码包,其页面是本地渲染的,并没有经过网络请求,而且页面的内容也是通过百度自身的服务器分发,当页面需要调用我们自己的数据接口时,需要额外的建立连接的时间,百度基于这个原因提出了 prelink 的概念,就是在框架加载时,业务还没有发起请求时,利用这段时间提前建立连接,这样后续请求发起是就可以利用之前建立好的连接直接进行数据传输,据百度描述可以节省 200ms 左右的时间。

离线包

在设计离线包的技术方案时,同样也遇到了异步接口加载的问题,也设计了多套解决方案。

最开始设计了客户端拦截请求,后来发现这么设计太不灵活,无法扩展,也出现了一些问题,于是很快就放弃了。

接下来的几套方案也和客户端有关,例如通过 jsbridge 进行数据调用,最后也因为数据传输的效率问题放弃了。

直接从客户端解决接口性能问题没有走通,后来就想到了能否参照百度小程序的 prelink 方案也做一个类似的方案,最开始和客户端讨论是打算让客户端帮忙建立连接,但通过沟通发现客户端实现的难度较大,需要对内核有较深入的了解和定制,暂时没有很好的办法。

没办法,只能从 web 的角度出发看有没有解决办法。

另辟蹊径

既然客户端做不到,那能不能从 web 端出发解决这个问题呢?答案应该也是可以的。

从线上性能数据来看,我们的离线包基本都是 Vue 的 spa 框架,在使用离线包的情况下,即使 html 和静态资源加载很快,但 SPA 框架的特性之一就是入口文件启动时间较长,只有当入口文件执行完毕,再加载异步 chunk 时,页面的代码才真正开始执行。

举个例子,下面这张图是通过 performance.getEntries 收集上来的某 SPA 项目的资源加载时间:

image2021-12-16_17-3-47.png

从图里圈出来的部分可以看到,入口文件大概在 330ms 的时候加载完毕,而异步 chunk 是在 462ms 左右才开始加载,这中间有 160ms 的时间差,这部分时间是入口文件解析执行的时间。

这还是在性能较强的 pc 端 chrome 浏览器的测试结果,在性能一般的设备上可能会更长。

那这几百毫秒的时间用来建立预连接岂不是正好,利用 js 异步 io 的特性,可以在程序启动前先发起一个 http 请求建立起链接,请求的过程是异步的,并不会阻塞 js 的执行。

HTTP2 的链接建立好后,只要是同样域名下的请求,后续真正的数据请求就可以直接复用,解决了首次加载慢的问题,而且也不需要客户端修改浏览器内核,反倒是充分利用了浏览器内核特性。

代码测试和数据验证

以上方案只是理论上可行,具体是否有效还需要编写测试代码测试和数据验证。

测试

具体操作方式可以用内联 script 标签的形式,只针对离线包的情况下,在构建的 html 模板中加入一段代码(因为离线包没有 csp)

代码只需要用 xhr 请求一下主域名即可,不需要关心返回值(目的是建立连接)。

数据验证

需要对比第一次请求是否比优化前有所提升,但传统的接口性能数据没办法直接区分出“第一次请求”这一特点,因为传统的接口数据是通过重写原型链的形式计算的时间。

这里可以采用另一种思路,还是利用 performance API 来获取,因为 xhr 也是一种“资源”,也会计入到 performance.getEntries 中,

执行 performance.getEntriesByType(‘resource’) 后如图所示:

image2021-12-16_18-31-7.png

通过过滤器过滤出目标域名下第一条 xhr 数据:

image2021-12-16_18-33-39.png

可以发现里面有类似 performance.timing 返回的数据接口,可以获得这次请求的 dns ,tcp ,ssl 等核心指标,通过对比前后数据就可以判断这个优化方案是否真的有预期的效果。

实际测试验证

统计共计 24H 某应用数据如下:

收集线上离线包环境下特定日志 24964 条,其中安卓 16273 条,ios 8691 条。

对照同一时间请求日志共 48.8 万条,其中安卓 32.2 万条,ios16.6 万条。

自定义日志中分为三个字段上报:

字段名 duration duration1 duration2
字段解释 请求总响应时间 dns + tcp 时间 发起请求的相对时间

通过分析日志发下 ios 的 duration1 字段均为 0,判断有可能是 ios 浏览器不支持获取这个字段数据,另外经过测试发现 ios 部分情况下连 duration 也无法获取到,且发现通过 getEntries 方法只能获取到 xhr 数据,也没有其他资源数据。

image4.png

image5.png

平均值分析

由于样本量较少,为了使平均值不受极端情况数据影响,增加限定范围 duration < 5000 且 duration2 < 5000 进行筛选!

先分析整体情况下的第一次请求和所有请求的平均响应时间:

分类 第一次请求 所有接口
平均响应时间 267 239

分平台统计平均值:

平台 第一次请求 所有接口
android 237 237
ios 323 243

从第一次请求的平均值来看,和整体的平均值并没有太大的差异,差异最大的 ios 也只有不到 100ms 的差异。

但从实际体验上来看第一次进入页面的响应时间是明显比第二次进来慢的。其中可能的原因是上报第一个请求是指当前页面的第一个请求,但不一定是下载打开 app 后的第一个请求,我们知道像 dns 查询是存在浏览器缓存的,即使我关掉了页面缓存可能依旧存在,几乎很难模拟到完全第一次进入的情况。

但好在我们依旧可以从数据入手,duration1 记录了 dns + tcp 的时间,通过筛选 duration1 不等于 0 的数据可以尽可能吧范围缩小。由于只有安卓有这个数据,样本量再次减少,可能后续需要多项目一起上线后才能有相对准确的数据。

添加筛选条件 duration1 > 0 首次请求剩余样本量 6180 条,占比 37.9%(6180/16273)

由于样本较少,为了尽量减少误差,我们再次增加条件限制 duration1 < 1000 得到平均值数据:

指标 duration duration1
平均值 354 219

从上表分析可知当 dns + tcp 存在时,其占请求的总时间占比达到了 61.8%,且其在安卓整体第一次请求的占比也达到了 37.9%,整体看下来是有很高的优化价值的。

通过抽样可以进一步验证我们的猜想,当请求耗时比较长的时候,绝大多数情况下 dns + tcp 的占比会比较大:

image6.png

请求发起时间分析

整体情况下的第一次请求发起时间平均值为:548 ms

这意味着从用户打开页面到开始发起数据请求需要 500ms 多的时间,从体验来看并不算太好。

分平台统计:

平台 android ios
发起时间 673 314

但这段时间也正好给我们的 prelink 提供了时间,利用 http 长链接以及不阻塞的特点,可以在业务请求发起前提前建立连接。

通过抽样也发现大部分情况下 duration1 的时间会小于 duration2 的时间:

image7.png

代码

通过在 html 模板中添加 script 标签实现,代码如下:

1
2
3
4
5
6
7
8
<script>
if (window.XMLHttpRequest) {
var __xhr__ = new XMLHttpRequest();
__xhr__.open("POST", "https://" + window.location.host + "/api/prelink");
__xhr__.send();
__xhr__ = null;
}
</script>

抓包分析

通过 charles 进行实验室抓包分析:

image8.png

DNS + TCP + SSL 在 prelink 接口已经完成,后续接口已经没有相应的时间消耗。

上线后数据验证

上线试运行了几天进行数据分析,筛选条件依旧是 duration < 5000,duration1 < 1000,duration2 < 5000,结果如下:

对照组数据来源:2022.1.9 和 2022.1.10 日两天数据,版本号为 ***(对照组),共收集首次接口请求数据:25463 条,其中安卓 16877 条

试验组数据来源:2022.1.12 日全天数据,版本号为 ***(试验组),共收集首次接口请求数据:19540 条,其中安卓 12533 条

平均值分析

整体接口请求时间分析:

组别 第一次请求 所有接口
平均响应时间(对照组) 257 233
平均响应时间(试验组) 150 216

试验组分平台统计平均值:

平台 第一次请求 所有接口
android 129 211
ios 187 226

对比添加 prelink 后 duration1 的变化(只考虑安卓):

组别 第一次请求
平均值(对照组) 73
平均值(试验组) 0.056

同时对比增加 prelink 后对业务是否造成阻塞:

组别/平台 android ios 整体
发起时间(对照组) 670 348 556
发起时间(试验组) 674 371 566

结论

第一次请求平均减少 100ms 左右,优化幅度达到 **41.6%**, 建立连接平均时间几乎为 0。

同时增加 prelink 后并未对业务 js 执行造成阻塞,整体业务请求发起时间几乎不变。

prelink 带来的对第一次跨域请求优化幅度较大,确实有百度小程序描述的 200 ~ 300ms 的提升。


使用prelink优化跨域接口性能
https://www.wobushi.top/2022/使用prelink优化跨域接口性能/
作者
Pride Su
发布于
2022年1月13日
许可协议