本文作者:蚂蚁金服高级无线开发专家bang
阿里妹导读:越来越多的APP内业务使用H5的方式实现,怎样让H5页面启动更快是很多人在探索的技术点,本文梳理了启动过程中的各个点,分别从前端和客户端角度去探讨有哪些优化方案,供大家参考。
随着移动设备性能不断增强,web页面的性能体验逐渐变得可以接受,又因为web开发模式的诸多好处(跨平台,动态更新,减体积,无限扩展),APP客户端里出现越来越多内嵌web页面(为了配上当前流行的说法,以下把所有网页都称为H5页面,虽然可能跟H5没关系),很多APP把一些功能模块改成用H5实现。
虽然说H5页面性能变好了,但如果没针对性地做一些优化,体验还是很糟糕的,主要两部分体验:
1.页面启动白屏时间:打开一个H5页面需要做一系列处理,会有一段白屏时间,体验糟糕。
2.响应流畅度:由于webkit的渲染机制,单线程,历史包袱等原因,页面刷新/交互的性能体验不如原生。
本文先不讨论第二点,只讨论第一点,怎样减少白屏时间。对APP里的一些使用H5实现的功能模块,怎样加快它们的启动速度,让它们启动的体验接近原生。
过程
为什么打开一个H5页面会有一长段白屏时间?因为它做了很多事情,大概是:
初始化webview-请求页面-下载数据-解析HTML-请求js/css资源-dom渲染-解析JS执行-JS请求数据-解析渲染-下载渲染图片
一些简单的页面可能没有JS请求数据这一步,但大部分功能模块应该是有的,根据当前用户信息,JS向后台请求相关数据再渲染,是常规开发方式。
一般页面在dom渲染后能显示雏形,在这之前用户看到的都是白屏,等到下载渲染图片后整个页面才完整显示,首屏秒开优化就是要减少这个过程的耗时。
前端优化
上述打开一个页面的过程有很多优化点,包括前端和客户端,常规的前端和后端的性能优化在PC时代已经有最佳实践,主要的是:
1.降低请求量:合并资源,减少HTTP请求数,minify/gzip压缩,webP,lazyLoad。
2.加快请求速度:预解析DNS,减少域名数,并行加载,CDN分发。
3.缓存:HTTP协议缓存请求,离线缓存manifest,离线数据缓存localStorage。
4.渲染:JS/CSS优化,加载顺序,服务端渲染,pipeline。
其中对首屏启动速度影响最大的就是网络请求,所以优化的重点就是缓存,这里着重说一下前端对请求的缓存策略。我们再细分一下,分成HTML的缓存,JS/CSS/image资源的缓存,以及json数据的缓存。
HTML和JS/CSS/image资源都属于静态文件,HTTP本身提供了缓存协议,浏览器实现了这些协议,可以做到静态文件的缓存。
总的来说,就是两种缓存:
1.询问是否有更新:根据If-Modified-Since/ETag等协议向后端请求询问是否有更新,没有更新返回,浏览器使用本地缓存。
2.直接使用本地缓存:根据协议里的Cache-Control/Expires字段去确定多长时间内可以不去发请求询问更新,直接使用本地缓存。
前端能做的最大限度的缓存策略是:HTML文件每次都向服务器询问是否有更新,JS/CSS/Image资源文件则不请求更新,直接使用本地缓存。那JS/CSS资源文件如何更新?常见做法是在在构建过程中给每个资源文件一个版本号或hash值,若资源文件有更新,版本号和hash值变化,这个资源请求的URL就变化了,同时对应的HTML页面更新,变成请求新的资源URL,资源也就更新了。
json数据的缓存可以用localStorage缓存请求下来的数据,可以在首次显示时先用本地数据,再请求更新,这都由前端JS控制。
这些缓存策略可以实现JS/CSS等资源文件以及用户数据的缓存的全缓存,可以做到每次都直接使用本地缓存数据,不用等待网络请求。但HTML文件的缓存做不到,对于HTML文件,如果把Expires/max-age时间设长了,长时间只使用本地缓存,那更新就不及时,如果设短了,每次打开页面都要发网络请求询问是否有更新,再确定是否使用本地资源,一般前端在这里的策略是每次都请求,这在弱网情况下用户感受到的白屏时间仍然会很长。所以HTML文件的“缓存”和跟“更新”间存在矛盾。
客户端优化
接着轮到客户端出场了,桌面时代受限于浏览器,H5页面无法做更多的优化,现在H5页面是内嵌在客户端APP上,客户端有更多的权限,于是客户端上可以超出浏览器的范围,做更多的优化。
HTML缓存
先接着缓存说,在客户端有更自由的缓存策略,客户端可以拦截H5页面的所有请求,由自己管理缓存,针对上述HTML文件的“缓存”和“更新”之间的矛盾,我们可以用这样的策略解决:
1.在客户端拦截请求,首次请求HTML文件后缓存数据,第二次不发请求,直接使用缓存数据。
2.什么时候去请求更新?这个更新请求可以客户端自由控制策略,可以在使用本地缓存打开本地页面后再在后台发起请求询问更新缓存,下次打开时生效;也可以在APP启动时或某个时机在后台去发起请求预更新,提升用户访问最新代码的几率。
这样看起来已经比较完美了,HTML文件在用客户端的策略缓存,其余资源和数据沿用上述前端的缓存方式,这样一个H5页面第二次访问从HTML到JS/CSS/Image资源,再到数据,都可以直接从本地读取,无需等待网络请求,同时又能保持尽可能的实时更新,解决了缓存问题,大大提升H5页面首屏启动速度。
问题
上述方案似乎已完整解决缓存问题,但实际上还有很多问题:
1.没有预加载:第一次打开的体验很差,所有数据都要从网络请求。
2.缓存不可控:缓存的存取由系统webview控制,无法控制它的缓存逻辑,带来的问题包括:
清理逻辑不可控,缓存空间有限,可能缓存几张大图片后,重要的HTML/JS/CSS缓存就被清除了。
磁盘IO无法控制,无法从磁盘预加载数据到内存。
更新体验差:后台HTML/JS/CSS更新时全量下载,数据量大,弱网下载耗时长。
无法防劫持:若HTML页面被运营商或其他第三方劫持,将长时间缓存劫持的页面。
这些问题在客户端上都是可以被解决的,只不过有点麻烦,简单描述下:
1.可以配置一个预加载列表,在APP启动或某些时机时提前去请求,这个预加载列表需要包含所需H5模块的页面和资源,还需要考虑到一个H5模块有多个页面的情况,这个列表可能会很大,也需要工具生成和管理这个预加载列表。
2.客户端可以接管所有请求的缓存,不走webview默认缓存逻辑,自行实现缓存机制,可以分缓存优先级以及缓存预加载。
3.可以针对每个HTML和资源文件做增量更新,只是实现和管理起来比较麻烦。
4.在客户端使用