一、简介
一提到App内的WebView加载网页,大家的第一印象就是:慢、耗流量、体验比原生差。但WebView加载网页也有其天生的优势:动态,跨平台,开发周期短。
那能如何解决WebView加载网页慢和体验差的问题呢?可以思考下面两个问题:
- 从打开浏览器到网页完全展示都发生了什么?
- 如何给WebView加载网页提速?
二、整体思维导图
三、衡量标准
快慢是一个相对量,如何衡量WebView的快慢呢?
3.1 用户体验的时间尺度
从用户角度来看,如下图是2018年份百度移动端的统计数据:
根据上面的统计折线图,得出如下表格:
可以发现:
- 小于1s,用户更容易接受,关闭率更低。
- 用户对移动端的容忍比PC端更低,要求更高。
3.2 加载时长标准
加载时长 = 加载结束 - 加载开始
加载开始很好界定,当用户点击feed流里面的item开始,就开始计时。
加载结束呢?WebView有个WebViewClient#onPageFinished回调方法,这个方法是在页面完全加载结束时候回调的,但是页面DOM渲染完页面就已经有内容,对于用户来说算是页面已经展示出来了。统计加载时长以DOM渲染完更好些。
3.3 统计标准
通过收集真正的用户使用数据,才能更好的根据用户的情况进行优化。那如何才能反应用户的真实情况呢?
通常有两种方式:
- 平均数,容易被较长的加载时间给拉高,不容易反应真实情况。
- 中位数,能很好的反应大多数用户的情况,但是中位数的要求较低,可以将其提高到80分位,或者90分位。
在们项目进行数据统计时候可以先采用80分位,检测下优化效果,后续再提高要求,使用更高分位如95分位等。
根据现网数据可知,当95分位的用户页面加载时长为1s以内时,80分位的用户页面加载时长为0.35s以内时,APP内网页的体验最佳。
具体最佳时间可以根据真实的上报数据的统计结果进行调整。
四、问题分析
前端页展示一般分两种:
- 前后端分离,前端加载资源后,通过js请求展示的数据并在前端渲染展示。
- 页面直出,页面数据由服务器填充完成后,直接下发到前端,由前端直接展示。
现在较多的采用前后端分离的方式,下面都以这种方式为例讲解。
4.1 WebView渲染过程
WebView渲染大致需要如下几步:
- 解析 HTML 文件
- 加载 JavaScript 和 CSS 文件
- 解析并执行 JavaScript
- 构建 DOM 结构
- 加载图片等资源
- 页面加载完毕
4.2 WebView耗时统计方法
统计可从两方面入手,一是网页层统计,二是App层统计。
4.2.1 网页层统计:WebView中网页耗时统计方法
WebView加载url到完全展示出各个部分耗时情况,可以根据w3c标准中网页performance参数获取具体耗时统计参数信息,详细的页面加载过程见下图:
根据performance统计情况可以得出如下数据:
- 重定向耗时:redirectEnd - redirectStart
- DNS查询耗时 :domainLookupEnd - domainLookupStart
- TCP链接耗时 :connectEnd - connectStart
- HTTP请求耗时 :responseEnd - responseStart
- 解析dom树耗时 : domComplete - domInteractive
- 白屏时间 :responseStart - navigationStart
- DOMready时间 :domContentLoadedEventEnd - navigationStart
- onload时间:loadEventEnd - navigationStart,也即是onload回调函数执行的时间。
4.2.2 App层统计:App层统计WebView耗时
Android可以通过WebViewClient#onPageFinished回调统计页面整个加载时长,开始时间以WebView创建开始算,严格一点可以从feed流中点击item开始算。这个统计只能算整个加载时长,加载到用户可见的时长以DOM渲染完页面为准,后者比前置时长更短一些。前置供参考,以后者为准。
根据上图可以获取的统计数据:
- WebView创建耗时:navigationStart - createWebView(以初始化开始时间为准,下同)
- 交互开始到页面可见耗时:onClickItem - createWebView
- 页面加载到可见耗时:domContentLoadedEventEnd - createWebView
- 页面完全加载耗时:onPageFinished - createWebView
4.3 资讯统计数据
测试资讯连接1:测试文章1
测试资讯连接2:测试文章2
五、优化方案
从统计数据看,WebView首次加载耗时较多2s左右,二次加载耗时也有0.5s左右。
5.1 离线化
同我们现有的离线包一样,将页面用的公共资源html,css,js等模板化,将模板打成压缩包形成离线包内置或动态下发到App端,在App中访问访问到具体的页面时候优先加载本地的模板资源。
通过WebViewClient#shouldInterceptRequest方法拦截WebView的资源加载,匹配到本地模板中的资源就直接加载本地资源,没有匹配本地模板资源再去加载线上资源。genWebResourceResponse用于实现具体的匹配策略。
override fun shouldInterceptRequest(view: WebView?, request: WebResourceRequest?): WebResourceResponse? {
return genWebResourceResponse(request, view)
}
模板注意事项:
- 精简模板,移除不必要的js、css,进行异步拉取
- 模板内联js、css,减少io
- js尽量放到最后,避免阻碍DOM解析
5.2 数据与模板加载
5.2.1 并行执行:数据请求与模板加并行
虽然进行了本地化网页模板化,但整体的页面加载依然是串行执行的。为了进一步提高页面的加载速度,可以让数据请求由app端代理。使数据加载与模板加载并行执行,待数据加载完成时通过JsBridge回填到网页中。效果如下图:
5.2.2 数据预加载
既然数据请求已经由app代理了,当然也可以通过一定的策略预加载数据,当页面打开时候直接使用缓存数据。这样整个网页加载过程完全离线化不受网络影响。
5.3 WebView预创建
由上面统计数据可知,WebView创建与二次创建耗时相差甚远,如下图总结:
原因是Webview所有的逻辑处理都是通过WebViewProvider来实现的,它需要加载Webview内核,这是一个重量级的操作,内核是以apk的形式存在。而内核加载后在同一页面是共享的,因此后续的初始化时间就很少了。
可以通过预创建WebView来加速这一过程,预创建会消耗一定量的内存,如何平衡预创建和内存消耗问题还需实践把握衡量,具体方式:
WebView池(或统一全局WebView):在app启动时候后台创建WebView池,当app需要展示网页的时候直接拿已创建的WebView,需要在页面销毁时候清除页面数据。池结构如下:
预创建WebView注意事项:
- WebView初始化需要传context,需要注意内存泄漏。
- WebView创建需要较大内存,需要注意内存耗费。
- WebView复用需要清除数据,需要注意状态维护。
5.4 模板预热
经过前面几步处理后,网页加载过程可以实现全部本地化后,但每次打开网页的时候还需要重复加载模板数据。DOM解析耗时,如下图:
为了避免重复加载模板,则需要在WebView池的基础上,让池中的WebView预先加载本地模板。当需要展示网页时候直接拿到已经加载过本地模板的WebView,并通过JsBridge注入数据。池中结构如下:
网页加载的整个过程如下:
5.5 图片加载
WebView在加载大量图片时候表现不佳,重复进入时还会重复加载图片,体验不好且浪费浏览。
5.5.1 App代理图片加载
该方式需要借助图片加载库如Glide,在WebViewClient#shouldInterceptRequest方法拦截WebView的资源加载,判断要加载的资源url是否为图片,是就走Glide加载并生成加载图片的WebResourceResponse,通过Glide来达到缓存图片目的,避免多次打开页面重复加载线上图片资源,genWebResourceResponse用于实现具体的匹配策略。这种方式有点是不需要前端配合,客户端完全自己处理即可。
在api>=21时,可以通过WebResourceRequest获取请求中的accept字段获取返回值类型,用于区分url类型。
override fun shouldInterceptRequest(
view: WebView?,
request: WebResourceRequest?
): WebResourceResponse? {
val url = request.url.toString()
if (checkImageRequest(request)) {
val imageFile = Glide.with(view.context)
.asFile()
.load(url)
.submit()
.get()
return WebResourceResponse(
"image/png,*/*",
"UTF-8",
FileInputStream(imageFile)
)
}
return super.shouldInterceptRequest(view, url)
}
在api<21时,只能通过url来判断来判断类型。
override fun shouldInterceptRequest(view: WebView?, url: String?): WebResourceResponse? { // 处理资源匹配
return genWebResourceResponse(url, view)
}
注:示例代码仅展示用,细节需要自己处理
5.5.2 hybrid
使用网页和原生控件的混合开发模式,网页中文字部分让WebView渲染,网页中的图片视频等使用原生控件展示。优点即可以避免重复加载图,又能提升图片浏览体验;缺点实现成本高,需要前后端协调处理。今日头条8.0.3版本同样采用了这种方式加载展示图片。
具体思路:
- 图片展示容器与WebView上下叠放,大小一致
- WebView中预留图片占位div
- 获取网页中图片的url、大小以及位置信息
- 通过js或其他方式通知App
- App加载图片并根据WebView中占位div位置设置原生图片位置
- 原生控件与WebView同步滚动
六、总结
Android中WebView还存在较大的优化空间,可以进一步提升资讯、活动页等h5页面的浏览体验。本文涉及的优化方式仅是方向性的,为后续Android App的WebView优化提供方向性指引,实际操作会涉及到多端配合,细节较多,需要不断迭代优化。
参考文章:
Does Page Load Time Really Affect Bounce Rate?