WebView性能优化
加载webview页面会经历如下阶段:
- 交互无反馈
- 到达新的页面,页面白屏
- 页面基本框架出现,但是没有数据;页面处于loading状态
- 出现所需的数据,show
Webview的初始化
当App首次打开时,默认是并不初始化浏览器内核的;只有当创建WebView实例的时候,才会创建WebView的基础框架。
所以与浏览器不同,App中打开WebView的第一步并不是建立连接,而是启动浏览器内核。
分析:那么webview的初始化时间需要多少呢?
分2种情况:
- 首次初始化时间:客户端冷启动后,第一次打开WebView,从开始创建WebView到开始建立网络连接之间的时间
- 二次初始化时间:在打开过WebView后,退出WebView,再重新打开WebView,从开始创建WebView到开始建立网络连接之间的时间
问题
WebView中用户体验到的打开时间需要再增加70~700ms
于是我们找到了“为什么WebView总是很慢”的原因之一:
- 在浏览器中,我们输入地址时(甚至在之前),浏览器就可以开始加载页面。
- 而在客户端中,客户端需要先花费时间初始化WebView完成后,才开始加载。
解决方案
2种解决思路:
- 使用前预先初始化好WebView,相当于预加载
- 在初始化的同时,通过Native来完成一些网络请求等过程,使得WebView初始化不是完全的阻塞后续过程。
全局WebView
客户端维护一个全局的WebView,在一开始的时候就完成WebView的初始化操作,后面直接使用。
存在的问题:
- 额外的内存消耗,即时不用,webview也在占用着内存
- 容易内存泄漏
客户端代理数据请求
在客户端初始化Webview的同时,直接由native开始网络请求数据;当页面ready后,js向native获取其代理请求的数据。
这个方法不能缩小WebView初始化的时间,但是将数据请求和WebView初始化操作并发进行,缩短了整个的页面加载时间。
建立连接/服务器处理
在页面请求的数据返回之前,主要有以下过程耗费时间:
- DNS
- connection
- 服务器处理
解决方案
DNS采用和客户端API相同的域名
DNS会在系统级别进行缓存,所以如果webviewd的域名和native的域名保持一致,则可以利用native的DNS的缓存。
同步渲染采用chunk编码
如果服务端处理的时间比较长,可以采用chunk方式。
在HTTP协议中,我们可以在header中设置transfer-encoding:chunked使得页面可以分块输出,首先将Web API可以确定的部分先输出给浏览器,然后等API完全获取后,再将API数据传输给浏览器。
传统的输出方式和chunk的输出方式:
- 如果采用普通方式输出页面,则页面会在服务器请求完所有API并处理完成后开始传输。浏览器要在后端所有API都加载完成后才能开始解析。
- 如果采用chunk-encoding: chunked,并优先将页面的静态部分输出;然后处理API请求,并最终返回页面,可以让后端的API请求和前端的资源加载同时进行。
- 两者的总共后端时间并没有区别,但是可以提升首字节速度,从而让前端加载资源和后端加载API不互相阻塞。
页面渲染
页面在解析到足够多的节点,且所有CSS都加载完成后进行首屏渲染。在此之前,页面保持白屏;在页面完全下载并解析完成之前,页面处于不完整展示状态。
解决方案
在页面框架加载这一部分,能够优化的点参照雅虎14条就够了;但注意不要犯错,一个小小的内联JS放错位置也会让性能下降很多。
- CSS的加载会在HTML解析到CSS的标签时开始,所以CSS的标签要尽量靠前。
- CSS链接下面不能有任何的JS标签(包括很简单的内联JS),否则会阻塞HTML的解析;
- 如果必须要在头部增加内联脚本,一定要放在CSS标签之前。
JS加载
对于大型的网站来说,在此我们先提出几个问题:
- 将全部JS代码打成一个包,造成首次执行代码过大怎么办?
- 将JS以细粒度打包,造成请求过多怎么办?
- 将JS按 "基础库" + "页面代码" 分别打包,要怎么界定什么是基础代码,什么是页面代码;不同页面用的基础代码不一致怎么办?
- 单一文件的少量代码改的是否会导致缓存失效?
- 代码模块间有动态依赖,怎样合并请求。
JS解析、编译、执行
<script>
window.t1 = performance.now()
</script>
<script>
window.test = function () {
// test code
}
</script>
<script>
window.t2 = performance.now()
test();
window.t3 = performance.now();
alert("编译耗时:" + (t2 - t1));
alert("执行耗时:" + (t3 - t2));
</script>
- 在t1~t2期间,JS代码仅仅声明了一个函数,主要时间会集中在解析和编译过程;
- 在t2~t3时间段内,执行test时时间主要为代码的执行时间
首次启动客户端,打开WebView的测试页面:
客户端进行不关闭情况下,关闭WebView并重新访问测试页面:
在低端安卓机上,(框架的初始化+异步数据请求+业务代码执行)会远高于几KB网络请求时间;高性能的Web网站需要仔细斟酌前端渲染带来的性能问题。
解决方案
- 高性能要求页面还是需要后端渲染。
- React还是太重了,面向用户写系统需要谨慎考虑。
总结
- WebView初始化慢,可以在初始化同时先请求数据,让后端和网络不要闲着。
- 后端处理慢,可以让服务器分trunk输出,在后端计算的同时前端也加载网络静态资源。
- 脚本执行慢,就让脚本在最后运行,不阻塞页面解析。
- 合理的预加载、预缓存可以让加载速度的瓶颈更小。
- WebView初始化慢,就随时初始化好一个WebView待用。
- DNS和链接慢,想办法复用客户端使用的域名和链接。
- 脚本执行慢,可以把框架代码拆分出来,在请求页面之前就执行好。
WebView内存优化
- Android的WebView在首次初始化时都要消耗大量内存,之后每次新建WebView会额外增加一些。
- 页面内代码消耗的内存相比与WebView系统的内存消耗相比可以说是很低
WebView体验优化
长按选择
在WebView中,长按文字会使得WebView默认开始选择文字;长按链接会弹出提示是否在新页面打开。
解决方法:可以通过给body增加CSS来禁止这些默认规则。
点击延迟
在WebView中,click通常会有大约300ms的延迟(同时包括链接的点击,表单的提交,控件的交互等任何用户点击行为)。
解决方法:使用fastclick(JS的一个开源库)一般可以解决这个问题。
键盘形态有限
WebView对键盘的控制能力很弱,无法直接调起或者隐藏键盘,而且键盘的确认文案是无法自定义的。
解决方法:目前只能通过由与App通过桥协议的方式,由App代为唤起键盘。
关于页面滚动
WebView在滚动期间还有各种限定:
- setTimeout和setInterval不触发。
- GIF动画不播放。
- 很多回调会延迟到页面停止滚动之后。
- background-position: fixed不支持。
WebView安全优化
打开第三方WebView
一旦URL可以通过外界输入自定义,那么就有可能在客户端内部打开一个外部的网页。而这个外部网页可能是不合法的。
解决方法:在内嵌的WebView中应该限制允许打开的WebView的域名,并设置运行访问的白名单。或者当用户打开外部链接前给用户强烈而明显的提示。
HTTPS
HTTPS可以防止页面被劫持或者注入,然而其副作用也是明显的,网络传输的性能和成功率都会下降,而且HTTPS的页面会要求页面内所有引用的资源也是HTTPS的,对于大型网站其迁移成本并不算低。
App使用Socket代理请求
如果HTTP请求容易被拦截,那么让App将其转换为一个Socket请求,并代理WebView的访问也是一个办法。
通常不法运营商或者WiFi都只能拦截HTTP(S)请求,对于自定义的包内容则无法拦截,因此可以基本解决注入和劫持的问题。
补充知识点:
一般来说HTML在开始接收到返回数据的时候就开始解析HTML并构建DOM树。通过走app代理加载数据将丧失边下载边解析的能力。只能由客户端完全下载好HTML后,注入到WebView中。因此其性能将会受到影响。