上图体现的是用户打开一个H5页面,经历的过程与代码内部所做的事情的对应关系。
用户:无感知(WebView进行初始化)->看到白屏(DNS,Connection,接收页面)->看到Loading界面(静态资源加载完毕后,拉取数据)->展现(数据请求成功)
再来看一下,一个Url是如何被浏览器加载并且呈现的,之前学习Java web知识的时候,接触到一个页面的加载流程为:
接下来分析一下各个阶段的耗时情况
众所周知,谈论Android APP的启动时间,我们喜欢说冷启和热启,那么谈论WebView的初始化时间,我们也从应用首次初始化和二次初始化,两个方面进行对比。
使用同一Android机器,对同一个App的活动页,进行20次测试,取平均值,得到的结果如下:
首次初始化时间 | 二次初始化时间 |
---|---|
192.79 ms | 142.53 ms |
从上面的测试数据,我们大概可以找到一个“页面加载总是很慢”的原因之一:
前端H5开发人员在开发页面时,测算的页面加载速度,是从“页面建立连接后”作为起始时间,“页面数据展示完成”作为结束时间。而这样的话,到手机移动端上,他们需要再加上200ms左右的时间。
那么如何解决这个问题呢?
这个做法很简单,只要在客户端启动后(或者某个契机点,只要在使用WebView之前),就初始化一个全局WebView,隐藏起来备用,当用户通过WebView访问H5页面时,使用这个全局的WebView来加载对应的网页即可。这种方案可以省去用户感知WebView的初始化这段时间。
缺点:内存消耗,WebView之前使用的痕迹需清除,使用不当容易造成内存泄露。
这个阶段,主要耗时在于:DNS解析,客户端与服务器端建立Connection,服务器处理完后返回值(首字节获取到,做为返回值的时间点)
以美团的某个活动页面的链接时间作为测试统计:
分位值 | DNS | Connection | 获取首字节 |
---|---|---|---|
50% | 1.3 ms | 71 ms | 172 ms |
90% | 60 ms | 360 ms | 541 ms |
这些时间都发生在网页加载之前,那么优化的方案无法从代码上入手,但是不表示完全无法优化,让我们看看几个过程中可以优化的点。
(1) DNS和Connection:由于系统会进行DNS缓存,即比如客户端App使用过域名api.meitu.com进行接口数据请求,那么该域名对应DNS解析出来的IP已经在系统级别上被缓存过了。那么得出结论,我们想优化这块时间,只需要尽可能将页面中的静态资源或者接口请求的域名保持与客户端App中一致。
(如上表格,分位值90%,说明至少有10%的用户是60ms的DNS解析,当我们控制了此操作后,能把大部分DNS解析控制到1.3ms的范围内即可)
(2) 服务器处理API:根据请求,通过业务API获取数据并返回。这段时间的优化,我们可以采用chunk编码,通过在header中设置 transfer-encoding:chunked 使得页面可以分块输出,让前端H5开发人员合理设计页面,让head部分都是确定的静态资源版本相关内容,而body部分是业务数据相关内容,那么我们可以在用户请求的时候,首先将Web API可以确定的部分先输出给浏览器,然后等API完全获取后,再将API数据传输给浏览器。
如果采用普通方式输出页面,则页面会在服务器请求完所有API并处理完成后开始传输。浏览器要在后端所有API都加载完成后才能开始解析。
如果采用chunk-encoding: chunked,并优先将页面的静态部分输出;然后处理API请求,并最终返回页面,可以让后端的API请求和前端的资源加载同时进行。
两者的总共后端时间并没有区别,但是可以提升首字节速度,从而让前端加载资源和后端加载API不互相阻塞。
页面在解析到足够多的节点,且所有CSS都加载完成后进行首屏渲染。
在此之前,页面都会保持白屏;在页面完全下载并解析完成之前,页面处于不完整展示状态。
测试页面:http://i.meituan.com/firework/meituanxianshifengqiang
在Mac上面,模拟4G情况
测试得到的时间耗费如下:
阶段 | 时间 | 大小 | 备注 |
---|---|---|---|
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的加载时间进行分析,可以得到如下时间:
指标 | 时间 | 计算方法 |
---|---|---|
HTML加载完成时间 | 218ms | performance.timing.responseEnd - performance.timing.fetchStart |
HTML解析完成时间 | 330ms | performance.timing.domInteractive - performance.timing.fetchStart |
可以看到HTML加载完成时间和HTML解析完成时间,中间间隔了112ms,那么这里面有什么猫腻,为什么一个DOM解析需要耗费这么久的时间,我们进一步看前端页面代码可以发现,在H5页面的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>
....
那么这段代码有什么异常吗,我们需要了解一些HTML解析的知识:
通常情况下,CSS不会阻塞HTML的解析,但如果CSS后面有JS,则会阻塞JS的执行直到CSS加载完成(即便JS是内联的脚本),从而间接阻塞HTML的解析。
CSS不会阻止页面继续向下继续。
内联的JS很快执行完成,然后继续解析文档。
然而,当这两部分同时出现的时候,问题就来了。
CSS加载阻塞了下面的一段内联JS的执行,而被阻塞的内联JS则阻塞了HTML的解析。
一个小小的内联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时时间主要为代码的执行时间
Zepto.js | Vue.js | React.js + ReactDOM.js |
---|---|---|
13ms / 40ms | 43ms / 127ms | 26ms / 353ms |
从这个数据来看,我们可以知道偏重的框架,例如React,仅仅JS的编译和执行时间就会达到30ms ~ 350ms,因此JS的优化也不容小觑。
居然我们知道HTML静态资源的请求需要耗费我们比较多的时间,那么可以将需求变更较小,使用较为频繁的H5页面的静态资源部分打包成静态资源Zip包(Zip包的版本信息带在Zip包的包名上),当客户端app启动后或者在该H5页面入口曝光的地方,进行预加载H5静态资源模板Zip包,并且Zip包解压到本地的固定缓存位置。
而WebView解析HTML页面的时候,当页面中的静态资源请求时,WebViewClient的shouldInterceptRequest中,我们拦截下来,指向对应的资源文件位置即可。
由于(1)中我们的方案,我们需要预下载Zip模板包,但是每个页面的资源文件,可能非常大而改动其实非常小,所以常用的方案是我们采用dsdiff的算法,服务器端将每个模板包的版本记录下来,当客户端请求预加载的时候,带上客户端之前zip包的版本信息与服务器端的最新版本进行匹配,服务器端将对应的差分包地址下发到客户端,一般修改也就10+kb的差分zip包,客户端再将老版本zip + diff差分包zip合并得到一个完整的最新zip包,解压即可。
由于HTML的解析与API数据请求是串行的操作,所以我们可以考虑通过服务器端下发部分URL对应需要的API数据接口,当某个URL地址接口曝光时,我们可以预先去请求API数据接口,进入WebView后,通过JsBridge将数据传递给HTML页面,由HTML完成数据的填充到页面中去。
加载网页的过程中,客户端,网络,服务器端,CPU,GPU等都会参与,各自都有相关的修改方案和工作,尽可能的并行处理或者预先处理,不相互阻塞,相互等待,才可以让网页加载得更快。
一些做页面加载优化的Github开源项目:
腾讯VasSonic : https://github.com/Tencent/VasSonic
CacheWebView : https://github.com/yale8848/CacheWebView
参考文章:
https://mp.weixin.qq.com/s/evzDnTsHrAr2b9jcevwBzA
https://tech.meituan.com/2017/06/09/webviewperf.html