WebView性能、体验分析与优化

在App开发中,内嵌WebView始终占有着一席之地。它能以较低的成本实现Android、iOS和Web的复用,也能够堂而皇之的突破苹果对热更新的封锁。css

 

然而便利性的同时,WebView的性能体验却备受质疑,致使不少客户端中须要动态更新等页面时不得不采用其余方案。html

 

以发展的眼光来看,功能的动态加载以及三端的融合将会是大趋势。那么如何克服WebView固有的问题呢?咱们将从性能、内存消耗、体验、安全几个维度,来系统的分析客户端默认WebView的问题,以及对应的优化方案。前端

 

性能web

 

对于WebView的性能,给人最直观的莫过于:打开速度比native慢。小程序

 

是的,当咱们打开一个WebView页面,页面每每会慢吞吞的loading好久,若干秒后才出现你所须要看到的页面。后端

 

这是为何呢?微信小程序

 

对于一个普通用户来说,打开一个WebView一般会经历如下几个阶段:api

  1. 交互无反馈浏览器

  2. 到达新的页面,页面白屏缓存

  3. 页面基本框架出现,可是没有数据;页面处于loading状态

  4. 出现所需的数据

若是从程序上观察,WebView启动过程大概分为如下几个阶段:

 

 

如何缩短这些过程的时间,就成了优化WebView性能的关键。

 

接下来咱们逐一分析各个阶段的耗时状况,以及须要注意的优化点。

 

WebView初始化

 

当App首次打开时,默认是并不初始化浏览器内核的;只有当建立WebView实例的时候,才会建立WebView的基础框架。

 

因此与浏览器不一样,App中打开WebView的第一步并非创建链接,而是启动浏览器内核。

 

咱们来分析一下这段耗时到底须要多久。

 

分析

 

针对WebView的初始化时间,咱们能够定义两个指标:

  • 首次初始化时间:客户端冷启动后,第一次打开WebView,从开始建立WebView到开始创建网络链接之间的时间。

  • 二次初始化时间:在打开过WebView后,退出WebView,再从新打开WebView,从开始建立WebView到开始创建网络链接之间的时间。

测试数据:

测试系统1: iOS模拟器,Titans 10.0.7

测试系统2: OPPO R829T Android 4.2.2

测试方式:测试10次取平均值

测试App:美团外卖

单位:ms

 

  首次初始化时间 二次初始化时间
iOS(UIWebView) 306.56 76.43
iOS(WKWebView) 763.26 457.25
Android 192.79 * 142.53


* Android外卖客户端启动后会在后台开启WebView进程,故并非彻底新建WebView时间。

 

这意味着什么呢?

 

做为前端工程师,统计了无数次的页面打开时间,都是以网络链接开始做为起点的。

 

很遗憾的通知您:WebView中用户体验到的打开时间须要再增长70~700ms。

 

因而咱们找到了“为何WebView老是很慢”的缘由之一:

  • 在浏览器中,咱们输入地址时(甚至在以前),浏览器就能够开始加载页面。

  • 而在客户端中,客户端须要先花费时间初始化WebView完成后,才开始加载。

 

而这段时间,因为WebView还不存在,全部后续的过程是彻底阻塞的。能够这样形容WebView初始化过程:

 

 

那么有哪些解决办法呢?

 

怎么优化

 

因为这段过程发生在native的代码中,单纯靠前端代码是没法优化的;大部分的方案都是前端和客户端协做完成,如下是几个业界采用过的方案。

 

全局WebView

 

方法:

  • 在客户端刚启动时,就初始化一个全局的WebView待用,并隐藏;

  • 当用户访问了WebView时,直接使用这个WebView加载对应网页,并展现。

 

这种方法能够比较有效的减小WebView在App中的首次打开时间。当用户访问页面时,不须要初始化WebView的时间。

 

固然这也带来了一些问题,包括:

  • 额外的内存消耗。

  • 页面间跳转须要清空上一个页面的痕迹,更容易内存泄露。

【参考东软专利 - 加载网页的方法及装置 CN106250434A

 

客户端代理数据请求

 

方法:

  • 在客户端初始化WebView的同时,直接由native开始网络请求数据;

  • 当页面初始化完成后,向native获取其代理请求的数据。

 

此方法虽然不能减少WebView初始化时间,但数据请求和WebView初始化能够并行进行,整体的页面加载时间就缩短了;缩短整体的页面加载时间:

 

【参考腾讯分享:70%以上业务由H5开发,手机QQ Hybrid 的架构如何优化演进?

 

还有其余各类优化的方式,再也不一一列举,总结起来都是围绕两点:

  1. 在使用前预先初始化好WebView,从而减少耗时。

  2. 在初始化的同时,经过Native来完成一些网络请求等过程,使得WebView初始化不是彻底的阻塞后续过程。

 

创建链接/服务器处理

 

在页面请求的数据返回以前,主要有如下过程耗费时间。

  • DNS

  • connection

  • 服务器处理

 

分析

 

如下为美团中活动页面的连接时间统计:

 

统计: 美团的活动页面

内容值: n%分位值(ms)

  DNS connection 获取首字节
50% 1.3 71 172
90% 60 360 541

 

优化

 

这些时间都是发生在网页加载以前,但这并不意味着没法优化,有如下几种方法。

 

DNS采用和客户端API相同的域名

 

DNS会在系统级别进行缓存,对于WebView的地址,若是使用的域名与native的API相同,则能够直接使用缓存的DNS而不用再发起请求图片。

 

以美团为例,美团的客户端请求域名主要位于api.meituan.com,然而内嵌的WebView主要位于 i.meituan.com。

 

当咱们初次打开App时:

  • 客户端首次打开都会请求api.meituan.com,其DNS将会被系统缓存。

  • 然而当打开WebView的时候,因为请求了不一样的域名,须要从新获取i.meituan.com的IP。

根据上面的统计,至少10%的用户打开WebView时耗费了60ms在DNS上面,若是WebView的域名与App的API域名统一,则可让WebView的DNS时间所有达到1.3ms的量级。

 

静态资源同理,最好与客户端的资源域名保持一致。

 

同步渲染采用chunk编码

 

同步渲染时若是后端请求时间过长,能够考虑采用chunk编码,将数据放在最后,并优先将静态内容flush。对于传统的后端渲染页面,每每都是使用的【浏览器】--> 【Web API】 --> 【业务 API】的加载模式,其中后端时间就指的是Web API的处理时间了。

 

在这里Web API通常有两个做用:

  1. 肯定静态资源的版本。

  2. 根据用户的请求,去业务API获取数据。

而通常肯定静态资源的版本每每是直接读取代码版本,基本无耗时;而主要的后端时间都花费在了业务API请求上面。

 

那么怎么优化利用这段时间呢?

 

在HTTP协议中,咱们能够在header中设置 transfer-encoding:chunked使得页面能够分块输出。若是合理设计页面,让head部分都是肯定的静态资源版本相关内容,而body部分是业务数据相关内容,那么咱们能够在用户请求的时候,首先将Web API能够肯定的部分先输出给浏览器,而后等API彻底获取后,再将API数据传输给浏览器。

 

下图能够直观的看出分chunk输出和一块儿输出的区别:

 

 

  • 若是采用普通方式输出页面,则页面会在服务器请求完全部API并处理完成后开始传输。浏览器要在后端全部API都加载完成后才能开始解析。

  • 若是采用chunk-encoding: chunked,并优先将页面的静态部分输出;而后处理API请求,并最终返回页面,可让后端的API请求和前端的资源加载同时进行。

  • 二者的总共后端时间并无区别,可是能够提高首字节速度,从而让前端加载资源和后端加载API不互相阻塞。

 

页面框架渲染

 

页面在解析到足够多的节点,且全部CSS都加载完成后进行首屏渲染。在此以前,页面保持白屏;在页面彻底下载并解析完成以前,页面处于不完整展现状态。

 

分析

 

咱们以一个美团的活动页面做为样例:

测试页面:http://i.meituan.com/firework/meituanxianshifengqiang

在Mac上面,模拟4G状况

页面样式:

 

 

测试获得的时间耗费以下:

 

表1

  阶段 时间 大小 备注
DOM下载 58ms 29.5 KB 4G网络  
DOM解析 12.5ms 198 KB 根据估算,在手机上慢2~5倍不等  
CSS请求+下载 58ms 11.7 KB 4G网络(包含连接时间,CDN)  
CSS解析 2.89ms 54.1 KB 根据估算,在手机上慢2~5倍不等  
渲染 23ms 1361节点 根据估算,在手机上慢2~5倍不等  
绘制 4.1ms   根据估算,在手机上慢2~5倍不等  
合成 0.23ms   GPU处理  

 

同时,对HTML的加载时间进行分析,能够获得以下时间点。

 

表2

  指标 时间 计算方法
HTML加载完成时间 218 performance.timing.responseEnd - performance.timing.fetchStart  
HTML解析完成时间 330 performance.timing.domInteractive - performance.timing.fetchStart  

 

这意味着什么呢?

 

对于表1

 

能够看到,随着在网络优良的状况下,Dom的解析所占耗时比例仍是不算低的,对于低端机器更甚。Layout时间也是首屏前耗时的大头,据猜想这与页面使用了rem做为单位有关(待进一步分析)。

 

对于表2,咱们能够发现一个问题

 

通常来讲HTML在开始接收到返回数据的时候就开始解析HTML并构建DOM树。若是没有JS(JavaScript)阻塞的话通常会相继完成。然而,在这里时间相差了90ms……也就是说,解析被阻塞了。

 

进一步分析能够发现,页面的header部分有这样的代码:

.....
<link href="//ms0.meituan.net/css/eve.9d9eee71.css" rel="stylesheet" onload="MT.pageData.eveTime=Date.now()"/>
<script>
window.fk = function (callback) {
require(['util/native/risk.js'], function (risk) {
    risk.getFk(callback);
});
}
</script>
</head>
....

 

一般状况下,上面代码的link部分和script部分若是单独出现,都不会阻塞页面的解析:

  • CSS不会阻止页面继续向下继续。

  • 内联的JS很快执行完成,而后继续解析文档。

 

然而,当这两部分同时出现的时候,问题就来了。

  • CSS加载阻塞了下面的一段内联JS的执行,而被阻塞的内联JS则阻塞了HTML的解析。

 

一般状况下,CSS不会阻塞HTML的解析,但若是CSS后面有JS,则会阻塞JS的执行直到CSS加载完成(即使JS是内联的脚本),从而间接阻塞HTML的解析。

 

优化

 

在页面框架加载这一部分,可以优化的点参照雅虎14条就够了;但注意不要犯错,一个小小的内联JS放错位置也会让性能降低不少。

  1. CSS的加载会在HTML解析到CSS的标签时开始,因此CSS的标签要尽可能靠前。

  2. 可是,CSS连接下面不能有任何的JS标签(包括很简单的内联JS),不然会阻塞HTML的解析。

  3. 若是必需要在头部增长内联脚本,必定要放在CSS标签以前。

 

 

JS加载

 

对于大型的网站来讲,在此咱们先提出几个问题:

  • 将所有JS代码打成一个包,形成首次执行代码过大怎么办?

  • 将JS以细粒度打包,形成请求过多怎么办?

  • 将JS按 "基础库" + "页面代码" 分别打包,要怎么界定什么是基础代码,什么是页面代码;不一样页面用的基础代码不一致怎么办?

  • 单一文件的少许代码改的是否会致使缓存失效?

  • 代码模块间有动态依赖,怎样合并请求。

 

关于这些问题的解决方案数量可能会比问题还多,而它们也各有优劣。

 

具体分析太过复杂,鉴于篇幅缘由在这里不作具体分析了。您能够期待咱们的后续计划:BPM(浏览器包管理)。

 

JS解析、编译、执行

 

在PC互联网时代,人们彷佛都快忘记了JS的解析和执行还须要消耗时间。确实,在几年前网速还在用kb衡量的时代里,JS的解析时间在整个页面的打开时间里只能算是九牛一毛。

 

然而,随着网速愈来愈快,而CPU的速度反而没有提高(从PC到手机),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>

 

将测试代码放入 【test code】 位置,而后在手机中执行;

  • 在t1~t2期间,JS代码仅仅声明了一个函数,主要时间会集中在解析和编译过程;

  • 在t2~t3时间段内,执行test时时间主要为代码的执行时间

 

在首次启动客户端后,打开WebView的测试页面,咱们能够获得以下的结果:

 

测试系统: iPhone6 iOS 10.2.1

测试系统: OPPO R829T Android 4.2.2

内容值: 编译时间(ms)/执行时间(ms)

 

系统 Zepto.js Vue.js React.js + ReactDOM.js
iOS 5.2 / 8 12.8 / 16.1 13.7 / 43.3
Android 13 / 40 43 / 127 26 / 353

 

 

当保持客户端进行不关闭状况下,关闭WebView并从新访问测试页面,再次测试获得以下结果:

 

系统 Zepto.js Vue.js React.js + ReactDom.js
iOS 0.9 / 1.9 5 / 7.4 3.5 / 23
Android 5 / 9 17 / 12 25 / 60

 

执行时间指的是框架代码加载的页面的初始化时间,没有任何业务的调用。

 

这意味着什么

 

通过测试能够得出如下结论:

  • 偏重的框架,例如React,仅仅初始化的时间就会达到50ms ~ 350ms,这在对性能敏感的业务中时比较不利的。

  • 在App的启动周期内,统一域名下的代码会被缓存编辑和初始化结果,重复调用性能较好。

 

因此,在移动浏览器上,JS的解析和执行时间并非不可忽略的。

 

在低端安卓机上,(框架的初始化+异步数据请求+业务代码执行)会远高于几KB网络请求时间;高性能的Web网站须要仔细斟酌前端渲染带来的性能问题。

 

优化

  • 高性能要求页面仍是须要后端渲染。

  • React仍是过重了,面向用户写系统须要谨慎考虑。

  • JS代码的编译和执行会有缓存,同App中网页尽可能统一框架。

 

WebView性能优化总结

 

一个加载网页的过程当中,native、网络、后端处理、CPU都会参与,各自都有必要的工做和依赖关系;让他们相互并行处理而不是相互阻塞才可让网页加载更快:

  • WebView初始化慢,能够在初始化同时先请求数据,让后端和网络不要闲着。

  • 后端处理慢,可让服务器分trunk输出,在后端计算的同时前端也加载网络静态资源。

  • 脚本执行慢,就让脚本在最后运行,不阻塞页面解析。

  • 同时,合理的预加载、预缓存可让加载速度的瓶颈更小。

  • WebView初始化慢,就随时初始化好一个WebView待用。

  • DNS和连接慢,想办法复用客户端使用的域名和连接。

  • 脚本执行慢,能够把框架代码拆分出来,在请求页面以前就执行好。

     

WebView内存消耗

 

分析

 

为了测试WebView会消耗多少内存,咱们设计了以下的测试方案:

  1. 客户端启动后,记录消耗的内存。

  2. 打开空页面,记录内存的上涨。

  3. 退出。

  4. 打开空页面,记录内存上涨。

  5. 退出。

  6. 打开加载了代码的页面,记录内存的额外增长。

获得以下测试结果:

 

测试系统: iOS模拟器,Titans 10.0.7

测试系统: OPPO R829T Android 4.2.2

测试方式:测试10次取平均值

 

  首次打开增长内存 二次打开增长内存 加载KNB+VUE+灵犀
iOS UIWebView 31.1M 5.52M 2M
iOS WKWebView 1.95M 1.6M 2M
Android 32.2M 6.62M 1.7M

 

 

WKWebView的内存消耗相比其余低了一个数量级,在此方面至关占优。

 

UIWebView和Android的WebView在首次初始化时都要消耗大量内存,以后每次新建WebView会额外增长一些。

 

UIWebView的内存占用不会在关闭WebView时主动回收,每次新开WebView都会消耗额外内存。

 

相比于性能,对于内存的优化能够作的仍是比较有限的。

  • WKWebView的内存占用优点比较大(代价是初始化比较慢)。

  • 页面内代码消耗的内存相比与WebView系统的内存消耗相比能够说是很低。

     

WebView体验

 

除了打开的速度,WebView一般体验也没有native的实现更好,咱们能够找到如下几个例子:

 

长按选择

 

在WebView中,长按文字会使得WebView默认开始选择文字;长按连接会弹出提示是否在新页面打开。

解决方法:能够经过给body增长CSS来禁止这些默认规则。

 

点击延迟

 

在WebView中,click一般会有大约300ms的延迟(同时包括连接的点击,表单的提交,控件的交互等任何用户点击行为)。

 

惟一的例外是设置的meta:viewpoint为禁止缩放的Chrome(然而并非Android默认的浏览器)。

 

解决方法:使用fastclick通常能够解决这个问题。

 

页面滑动期间不渲染/执行

 

在不少需求中会有一些吸顶的元素,例如导航条,购买按钮等;当页面滚动超出元素高度后,元素吸附在屏幕顶部。

 

这个功能在PC和native中都可以实现,然而在WebView中却成了难题:

 在页面滚动期间,Scroll Event不触发

 

不只如此,WebView在滚动期间还有各类限定:

  • setTimeout和setInterval不触发。

  • GIF动画不播放。

  • 不少回调会延迟到页面中止滚动以后。

  • background-position: fixed不支持。

  • 这些限制让WebView在滚动期间很难有较好的体验。

 

这些限制大部分是不可突破的,但至少对于吸顶功能仍是能够作一些支持:

 

解决方法:

  • 在iOS上,使用position: sticky能够作到元素吸顶。

  • 在Android上,监听touchmove事件能够在滑动期间作元素的position切换(惯性运动期间就无效了)。

 

键盘形态有限

 

WebView对键盘的控制能力很弱,没法直接调起或者隐藏键盘,并且键盘的确认文案是没法自定义的。

咱们以百度为例:

 

 

当你打开百度搜索时,点击【换行】就完成了输入并开始了搜索。

 

为何是【换行】而不是【搜索】呢?

 

固然不是bug……而是……臣妾作不到啊!

 

解决方法:

 

目前只能经过由与App经过桥协议的方式,由App代为唤起键盘(可是实际操做过于复杂)。

 

crash

 

一般WebView并不能直接接触到底层的API,所以比较稳定;但仍然有使用不当形成整个App崩溃的状况。

目前发现的案例包括:

  • 使用过大的图片(2M)

  • 不正常使用WebGL

 

WebView安全

 

WebView被运营商劫持、注入问题

 

因为WebView加载的页面代码是从服务器动态获取的,这些代码将会很容易被中间环节所窃取或者修改,其中最主要的问题出自地方运营商(浙江尤为明显)和一些WiFi。

 

咱们监测到的问题包括:

  • 无视通讯规则强制缓存页面。

  • header被篡改。

  • 页面被注入广告。

  • 页面被重定向。

  • 页面被重定向并从新iframe到新页面,框架嵌入广告。

  • HTTPS请求被拦截。

  • DNS劫持。

 

这些问题轻则影响用户体验,重则泄露数据,或影响公司信誉。

 

针对页面注入的行为,有一些解决方案:

 
使用CSP(Content Security Policy)

 

CSP能够有效的拦截页面中的非白名单资源,并且兼容性较好。在美团移动版的使用中,可以阻止大部分的页面内容注入。

 

但在使用中仍是存在如下问题:

  • 因为业务的须要,一般inline脚本仍是在白名单中,会致使彻底依赖内联的页面代码注入能够经过检测。

  • 若是注入的内容是纯HTML+CSS的内容,则CSP无能为力。

  • 没法解决页面被劫持的问题。

  • 会带来额外的一些维护成本。

 

整体来讲CSP是一个行之有效的防注入方案,可是若是对于安全要求更高的网站,这些还不够。

 

HTTPS

 

HTTPS能够防止页面被劫持或者注入,然而其反作用也是明显的,网络传输的性能和成功率都会降低,并且HTTPS的页面会要求页面内全部引用的资源也是HTTPS的,对于大型网站其迁移成本并不算低。

 

HTTPS的一个问题在于:一旦底层想要篡改或者劫持,会致使整个连接失效,页面没法展现。这会带来一个问题:原本页面只是会被注入广告,并且广告会被CSP拦截,而采用了HTTPS后,整个网页因为受到劫持彻底没法展现。

 

对于安全要求不高的静态页面,就须要权衡HTTPS带来的利与弊了。

 

App使用Socket代理请求

 

若是HTTP请求容易被拦截,那么让App将其转换为一个Socket请求,并代理WebView的访问也是一个办法。

 

一般不法运营商或者WiFi都只能拦截HTTP(S)请求,对于自定义的包内容则没法拦截,所以能够基本解决注入和劫持的问题。

 

Socket代理请求也存在问题。

  • 首先,使用客户端代理的页面HTML请求将丧失边下载边解析的能力;根据前面所述,浏览器在HTML收到部份内容后就马上开始解析,并加载解析出来的外链、图片等,执行内联的脚本……而目前WebView对外并无暴露这种流式的HTML接口,只能由客户端彻底下载好HTML后,注入到WebView中。所以其性能将会受到影响。

  • 其次,其技术问题也是较多的,例如对跳转的处理,对缓存的处理,对CDN的处理等等……稍不留神就会埋下若干大坑。

 

此外还有一些其余的办法,例如页面的MD5检测,页面静态页打包下载等等方式,具体如何选择还要根据具体的场景抉择。

 

客户端内打开第三方WebView

 

通常来讲,客户端内的WebView都是能够经过客户端的某个schema打开的,而要打开页面的URL不少都并不写在客户端内,而是能够由URL中的参数传递过去的。

 

那么,一旦此URL能够经过外界输入自定义,那么就有可能在客户端内部打开一个外部的网页。

 

例:做案过程

  • 某个App有个WebView,打开的schema为 appxx://web?url={weburl}。

  • App中有个扫码的功能,能够扫描某个二维码并打开对应的schema连接。

  • 某个坏人制做了一个二维码并张贴到街上,内容符合 : appxx://web?url={some_hack_weburl}。

  • 用户扫码打开了some_hack_weburl。

  • 若是some_hack_weburl是一个高仿的登陆页面,那么用户将会极可能将用户名密码提交到其余网站。

 

解决方法:在内嵌的WebView中应该限制容许打开的WebView的域名,并设置运行访问的白名单。或者当用户打开外部连接前给用户强烈而明显的提示。

 

发展

 

在一个客户端内,native目前主要功能是提供高效而基础的功能;内部的WebView则添加一些性能体验要求不高但动态化要求高的能力。

 

提升客户端的动态能力,或者提升WebView的性能,都是提高App功能覆盖的方式。

 

而目前的各类框架,ReactNative、Week包括微信小程序,都是这个趋势的尝试。

 

随着技术的发展,WebView的性能、体验和安全问题也将会逐渐的改善,在App中占有愈来愈多比重的同时,也将会为App开拓新的能力,为用户带来更优质的体验。

 

本文转载自http://tech.meituan.com/WebViewPerf.html;

相关文章
相关标签/搜索