一、背景
在 App 的开发过程中,随着业务的发展,越来越多的公司会选择内嵌 H5,但是便利性的同时,H5 的性能却有着很大的问题,备受争议。作为移动开发者应该从哪些方面去优化 H5 是一个很值得研究的话题。
达达骑士 APP 目前内嵌的网页大概有 260+,高频页面 PV 达到日均数十万 ,功能复杂的页面加载耗时也达到了2s+,对骑士的直观感受和配送效率有着严重的影响。
如图所示: 综合上面所说的问题,提升网页加载速度,减少网页加载时间是现阶段需要考虑的事情,小组内计划通过技术改造完成网页加载速度的提升,提升用户体验以优化配送效率。
在考虑如何优化 H5 网页加载速度之前,我们简单了解下 H5 是如何加载和渲染的。
注意:本次测试数据以众包个人中心页为例,测试机型红米 note 9
从上图可以看出 H5 的加载大致经历以下几个阶段:
交互无反馈阶段
该阶段主要是控件初始化和启动浏览器内核
到达新页面页面白屏
该阶段主要是建立链接,下载资源文件并加载、执行、渲染
初显阶段,页面基本框架显示
显示固定内容
加载阶段
依赖于网络速度、接口规模、后端响应和渲染次数
移动页面白屏时间 = 无反馈阶段+白屏阶段(资源加载、首次渲染、JS执行)
由此可以看出我们优化的重点可以放在 H5 的预加载和缩短白屏时间上,对此我们可以很容易想到以下方案。
当模板和正文数据分离之后,由于 H5 每次使用的都是同一个模板文件,因此我们并不需要在用户进入页面的时候才去加载模板,可以直接在预加载 H5 的同时就让其预热加载模板,这样每次使用时仅需要将正文数据传给 H5,H5 收到数据后直接进行页面渲染即可。
利用CDN服务器的缓存能力,保证用户访问资源的速度和体验。
首次加载 webview 比较耗时,我们可以在 Application 中预先初始化好 webview,用的时候直接取来节省 webview 初始化的时间。
如果每次网页加载都去服务端请求下载资源耗时是比较高的,有些更新频率低的资源我们可以在第一次加载的时候存一份到本地,下次其他页面加载的时候直接取本地的,节省用户流量和提升加载速度,缺点是会消耗部分手机的存储空间。
第1点适用于页面结构单一如新闻客户端详情页,第2点已经在使用,考虑到达达骑士的业务和服务端、前端现有的整体架构,目前可以从第3和第4点来做重点优化。
对于第4点本地缓存复用还有以下两种可选方案
App 端 H5 离线包下载解压,后续直接加载本地资源
第一次渲染下载资源到本地后续直接复用,后续相同请求拦截直接拿本地资源
两个方案优缺点对比
出于投入产出比考虑,最终使用方案二,让一些高频使用的页面平均加载时长快速缩短,从原来的平均 2s+ 缩短到 1s+.
当在 App 中第一次打开一个网页,系统首先做的并不是直接建立连接,而是启动浏览器内核。所以第一次 webview 的初始化要花费大量时间,统计下来不同机型耗时大概在 200ms~400ms 左右。因此预创建和复用可以减少部分等待时间。
大致逻辑是先创建 webview 并缓存起来,等到需要的时候直接取出来。这边自己做了一个简单的缓存策略大家可以参考下。首先预先创建几个 webview,业务根据实际需要自行设置,注意创建过多webview会导致内存消耗过大等问题,这边的方案是根据业务情况预先创建N个webview放入堆栈中,需要的时候去取,不足的时候动态创建新的 webview 加入到堆栈中。释放出大于N的 webview 的时候做及时释放回收处理,如图所示。
我们可以通过 shouldInterceptRequest 回调拦截所有网页的请求资源,下载缓存到本地,在通过 WebResourceResponse 作为网页内容返回。
本地拦截过程如下图所示
iOS这边实现并没有安卓那么便捷。(1)通过NSURLProtocol拦截的方式不仅会出现post请求body丢失的情况,而且无法控制拦截范围。(2)通过WKURLSchemeHandler去拦截自定义scheme听起来是个很好的方案,但是需要前端小伙伴配合改造。
所以只能采用hook handlesURLScheme方式实现对网络请求的拦截,通过重写WKWebview的handlesURLScheme方法使WKURLSchemeHandler有了拦截https和http请求的能力。
1.版本兼容问题
苹果是从iOS11开始提供WKURLSchemeHandler功能的,并且在iOS11的部分小版本中还存在body丢失的问题,所以我们直接从iOS12开始使用这个功能。
工具库向外部WKWebView提供WKWebViewConfiguration,仅在iOS12及以上系统版本并且是在我们指定页面范围内才添加拦截功能。
public func customConfig(url: String) -> WKWebViewConfiguration {
let config = WKWebViewConfiguration()
if #available(iOS 12.0, *), containsUrl(url: url) {
config.setURLSchemeHandler(schemeHandler, forURLScheme: "https")
config.setURLSchemeHandler(schemeHandler, forURLScheme: "http")
}
return config
}
加载本地JS、CSS文件时,需要添加跨域支持否则JS和CSS会无法加载本地资源从而白屏。需要在响应头中添加Access-Control-Allow-Origin":"*"来解决跨域问题。
func loadFromLocal (urlSchemeTask: WKURLSchemeTask, cacheData: Data, urlString: String) {
if let reqURL = urlSchemeTask.request.url {
if let urlResponse = HTTPURLResponse.init(url: reqURL, statusCode: 200, httpVersion: nil, headerFields: ["Access-Control-Allow-Origin":"*"]) {
urlSchemeTask.didReceive(urlResponse)
urlSchemeTask.didReceive(cacheData)
urlSchemeTask.didFinish()
}
}
}
当第一次进入页面没有缓存时,客户端发送WKURLSchemeTask的请求后,如果没等到网络请求的回调就退出当前页面,就会出现闪退。原因是退出页面的时候WKURLSchemeTask会被终止,相关资源也会被回收,继续调用didReceive方法便会发生闪退。所以判空处理尤为重要。苹果官方文档也有说明:
/**@abstract Add received data to the task.An exception will be thrown if you try to send the task any data before sending it a response.An exception will be thrown if you try to send the task any data after the task has already been completed.An exception will be thrown if your app has been told to stop loading this task via the registered WKURLSchemeHandler object.*/ func didReceive(_ data: Data)
我们采取了页面链接、资源类型、资源服务域名可动态配置的方案来保证线上运行稳定。
工具库对外暴露的属性:
///暂存从配置服务拉取的可缓存资源的页面链接(外部通过配置拉取)
public var canCachePages: [String] = []
///可支持的资源后缀
public var canCacheTypes: [String] = [".png",".js",".css",".jpg"]
///可支持的域名
public var canCacheDomainName: [String] = ["xx.xxxxx.cn"]
我们采取逐步放开的方式灰度页面,通过监控观察页面的运行情况。一旦灰度过程中出现问题,立马关闭配置。
通过缓存优化我们在上线新版本以后看到明显的耗时降低,从缓存前平均2.2s下降到平均1.7s整体打开页面有较为明显的速度提升。效果如下图:
上图通过同一时间A/B环境可以明显对比出来采用缓存的新版本11.15之后的版本页面打开的平均耗时比优化之前的低了很多。
通过同一个页面批量后不同时间维度也可以看到明显的下降趋势,我们是2月26号上线的,从26号之后随着灰度加大整体的平时时长下降的还是比较明显。
效果如下:
目前通过前几批灰度,安卓和 iOS 已有 40+ 页面开启了上述优化,后续我们会继续扩大灰度,目标是所有高频页面都能用上上述功能。在大量推广的过程中,我们从数据上也明显看出如果推广的页面是低频页面效果其实是不明显。所以这个方案可以很好的解决高频页面,对低频页面其实是不适用的。
低频页面:五、未来规划和总结
随着业务的不断发展,用户对App的要求越来越高,我们需要从多方面多维度的挖掘可优化的点,H5 优化方案有很多种,我们本次使用方案是投入极少人力去获得较大收益。后续我们会继续探索更多可行方案。
1.1 对于缓存方案组件单独抽离,后续方便快速的复用到其他APP上。
1.2 低频页优化如果此类页面变动很少,几个月发一次版,后续规划下载离线 zip 包方案,直接加载本地资源进行优化。