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中。因此其性能将会受到影响。

results matching ""

    No results matching ""