背景
作为直接面向用户的前端开发人员,我们都知道页面首屏打开速度的重要性,直接关系到用户的留存。
提升 H5 页面打开速度的方法也有很多,有从网络优化入手的 CDN、gzip压缩、Keep-Alive等,有从加载优化入手的资源大小优化、懒加载、按需加载、代码拆分、Tree Shaking等,以及页面渲染时的一些优化。
在我们的项目中,上述优化基本都有使用,当优化达到一定程度,想要继续仅基于 H5 自身优化已难以再提升,就需要和客户端合作,提升 H5 页面的打开速度,比如离线缓存、预加载等。
这里介绍一个我们使用的接口预请求的方案,它可以相对低成本的提升 FMP (首次有效绘制) 的渲染速度,而且从我们的上线效果上来看提升非常明显。
什么是接口预请求
当我们打开在 APP 内的 H5 页面时,一般会经过原生切页面动画、创建新页面、加载 webview、加载 HTML、请求静态资源并解析、获取首屏数据、渲染内容、下载图片等步骤。
其实在加载的同时,可以利用 APP 在帮助 H5 获取首屏数据,这样在 H5 页面加载完成后,可以直接使用 APP 已请求的首屏数据进行渲染,这样就节省了获取首屏数据的时间。首屏数据接口越慢,该方案的价值就越大。
大体如下图所示:
为什么要用接口预请求而不是 SSR?
在做 H5 秒开优化前,我们也讨论过 SSR(服务端渲染) 的方案。 基于前后端分离的服务端渲染,一般由前端团队使用 nodeJS 在服务器进行页面渲染,前端服务器会组装一个携带了具体数据的HTML文本,并且返回给浏览器。 服务端渲染虽然可以减少白屏时间,但相较于接口预请求有如下问题:
- 需要有完善的基于 nodeJS 的服务器运维、监控、告警等支持。这块是最大问题,大多数公司的服务端是基于 Java 等,缺乏对 nodeJS 的运维经验,但是由前端团队来做这一块支持,对有C 端访问量的项目也不保险。
- 需要更多的服务器负载均衡,消耗更多的服务器自由。而接口预请求计算是在客户端,不增加服务端复杂度。
方案
方案概览
既然我们要利用 APP 来帮助 H5 获取首屏数据,而我们的 H5 页面也不止一个,并且每个页面的参数都不一样。 那第一个要解决的问题就是怎么告诉 APP 在打开那些页面时需要同步请求那些接口,这些接口的入参是什么样的?
当 APP 请求完成后,要解决的第二个问题是怎么把请求结果及时通知给 H5 页面,并且防止 H5 页面自身发起重复请求,增加服务端负担?
第三个要解决的是如果 H5 修改了代码,更新了参数,而告知 APP 入参的配置还未更新,怎么避免使用旧配置请求的结果?
我们先带着问题看下整个方案的流程图:
如何让APP 知道打开那些页面时需要预请求,请求什么接口,什么参数
H5 项目接口预请求配置
我们在每个 H5 项目先新增的另一个叫 app.config.json 的配置文件,配置了该页面需要请求的主接口(可以是多个),请求方法、header、body、url 参数等。
单个页面配置:
json
{
pageName: '页面名称',
key: 'H5页面的URL', //页面唯一路径,作为key
preRequest: { // 接口预请求配置
appVersion: '8.0.1', // 支持app最低版本号,为了处理APP识别字段新增等变更情况
support: 0, // 是否支持接口预请求,0 | 1
apis:[ // 预请求接口信息,数组
{
fragment: "#/detail", // 新增fragment 指定对应的路由(hash模式)页面
url: 'https://xxxx/api/address', // 请求url
method: 'get', // 请求类型,如get|post,不传默认get
header: {}, // 请求头,规则同请求体
body:{ // 请求体
shopId: "getUserInfo.shopId:String|''",
cityId:'getLocation.cityId:Int',
key5:'params.orderId',
key6:'params.xxx'
}
}
]
}
}
如上配置,页面 URL、接口地址等都好理解。
在打开一个 H5 时,接口的入参一般来自两个地方,最常见的是通过 URL 来获取,另一种常见情况是通过 JSBridge 在 APP 内获取,比如全局的用户信息等。
这里我们通过不同的字符串告知 APP 需要从哪里获取那些参数,比如 params.orderId 是告知 APP 需要从打开 H5 页面的URL 中获取 orderId 的参数,而 getUserInfo.shopId,则是告知 APP 需要通过 JSBridge 中的 getUserInfo 方法获取 shopId 参数。
这里APP并非真的去自己调用 JSBridge 的方法,而是告知 APP 该值需要和 JSBridge 中的方法达到同样的效果。
也可以传递固定值,比如接口请求中入参 type 是固定值 1,只需要和 APP 商议好规范,读取识别即可。 以上我们解决了 APP 请求那些接口以及参数的问题。
配置文件如何下发给 APP
我们再 H5 打包构建时对 app.config.json 进行处理,在增加些其他必备配置后,提交到 H5配置平台,这时可以选择在打开 APP 时调配置平台的接口,但每次都读取数据库中的配置,会给服务端带来极大的压力。
这里我们做了一个小小的优化,来很好的规避了这个问题。
我们再构建完成,发布上线后,在 H5配置平台 打开预请求的开关时,由配置平台读取所有配置的页面,生成一个配置的 JSON 文件,文件名类似 h5config.xxx.json,xxx 代表版本号,并把配置文件上传到 CDN。
用户打开 APP 时,通过接口下发最新配置文件的 地址和版本,APP 本地和缓存的文件对比,判断是否要下载最新的配置文件。
这样改造后,只需要在点击发布的那一刻,读库生成新配置文件,大量的 C 端用户访问时,只需要调一个简单的接口获取最新配置版本和地址即可。
最终的配置文件类似这样:
json
{
version: "0.0.1",
global:{
// 全局配置
},
pages: [
{
key: "https://xxx.com/pages/xxx.html",
pageName: "XXX活动页",
openImageCache: 0, // 是否开启图片缓存,0关闭,1开启
openPreRequest: 1, // 是否开启接口预请求,0关闭,1开启
config: {
// 上面的单个页面配置生成而来
}
}
]
}
APP 每次打开 H5 页面,都从配置中排查,是否有配置的页面 URL,如果有并且打开了接口预请求,则在打开 H5 页面的同时基于配置发起接口请求。
至此就解决了我们的第一个问题。
如何同步请求结果并避免 H5 重复请求
最初我们想到的是通过 JSBridge 去轮循获取 APP 中请求的数据,但这种方式显然不够优雅。 后来通过 APP 在获取到结果,并且 webview 触发"onLoad "事件后,把数据注入到 window 对象下。
如图所示,正常情况下,存在两种情况:
- H5 页面尚未加载好,数据就已返回
- H5 页面加载完成后,数据尚未返回
不管是哪种情况,我们都可以通过监听 注入的 window 对象,来获取数据,当第一次没有读取到时,会立即发起 H5 本身的请求,这样即使接口预请求异常也不影响 H5 本身。
对于 window 对象我们通过 Object.defineProperty 进行监听,APP 和 H5 自身的请求谁先返回就用谁。
这样就解决了第二个问题,既能提升又完全不影响 H5 自身业务。
如何获取到正确的数据
上文我们提到,因为 H5 可以随时发版,发版后,如果没有及时更新配置文件,或者更新了但 APP 有缓存,没有立即使用最新的配置文件。
此时如果 H5 本身的参数变化了,APP 还是按旧配置获取请求,得到的数据并非新版本的数据时如何处理了?
还记得前面提到注入的 window.request_cache_xxx 对象吗,其中 xxx 是配置中 url、method、header、body、params(params 是指参数挂载在 URL 上)这些字符串的 md5 值,每个H5 项目每次更新配置都有一个唯一的 md5 值。其中 H5 本地保留一份,另一份通过配置文件下发给APP,如果两边的 md5 值匹配不上,则 H5 不会使用 APP 注入的数据。
这里其实还有另一个风险。
如果 H5 开发者在代码中的参数和配置文件不同步,比如迭代时间长后,忘记需要更新配置文件了。那么也就出现悲剧。
作为技术方案设计者,我们不能依赖人员的谨慎性,而应该靠系统设计来规避这些问题。
能发生的事情终将会发生
在这里我们通过修改 H5 项目的 HTTP 公共库,使其在开发和测试环境都是通过读取 app.config.json 中配置的接口、参数等来发起请求,这样就做到开发调试和线上一致。
以上我们就解决了 APP 请求数据的正确性问题。
最终效果
通过以上方案,我们基本上没有借助其他部门的力量,单单依靠大前端组内的资源,就极大解决了 H5 秒开的问题。 通过我们的性能监控的数据,在开启 H5 秒开后包含接口预请求、DNS 预请求(通过 APP 对 H5 使用到的域名进行 DNS 缓存)策略后,所有 H5 项目,FMP 达到 1.2 秒的超过 80%。
PS:FMP 我们取的是从用户在 APP 点击 H5 链接到主内容渲染完成的时间
绝大部分页面有 200~600ms 的提升,主要使用的 CMS 活动页,安卓提升了 27%,iOS 提升了 43%,平均 FMP 在 900ms 左右。