腾讯祭出大招VasSonic,让你的H5页面首屏秒开

VasSonic成长历程


前言

2017.8.8 14时,SNG增值产品部Vas团队研发的轻量级高性能Hybrid框架VasSonic经过了公司最终审核,做为腾讯开源组件分享给你们。从当初立项优化页面加载速度,到不断摸索、优化,再到整理代码、文档,最终在Github上开源,而且在24小时内获取star数超过1600。咱们很是高兴看到咱们的成果收到这么多的关注,趁此机会,正好回顾一下VasSonic的成长历程,也但愿可以让你们更了解VasSonic。javascript

项目背景

Web相信你们再熟悉不过了,它具备快速迭代发布的自然优点,但也存在中一些让人诟病的问题,好比加载速度慢,体验差等。在此以前,手Q上不少页面首屏打开速度居高不下,甚至有些耗时达到3s以上,这意味着用户打开页面必须通过3秒以后才能进行交互操做,体验至关差,不少用户忍受不了这个漫长的时间直接流失掉了。css

为了提高用户体验和业务用户留存率,咱们不少业务一开始经过Web开发,等页面模型验证符合预期后,再将H5页面转化成原生界面。咱们很快意识到这不是一种健康的可持续的开发模式,一方面存在重复人力浪费,另一方面原生商城除了速度快一点,要运营活动改版都很难。html

因此后来团队改了切入方向,安排人力专心研究如何加快页面打开速度,通过了一系列的摸爬滚打和优化探索,最终咱们研发出了VasSonic框架,让H5页面首屏达到秒开,给用户一个更好的H5体验。下面就和你们分享VasSonic框架的发展历程。前端

业务形态

任何一个技术框架都是结合具体的业务形态来进行发展优化的,技术是为了更好地服务业务,业务也会驱动技术的发展。在此首先介绍一下业务形态,咱们是来自手Q增值产品部门的VAS团队,负责手机QQ上不少深受年轻人喜欢的个性化增值服务,好比气泡、挂件、主题等等。手Q上大部分的业务仍是基于H5开发的,你们对手Q的业务形态可能有简单的了解。好比下图的游戏分发中心、会员特权中心、个性化装扮商城等。这部分商城的特色比较明显,页面的不少数据都是动态的,是由咱们的产品经理在后台配置的。
业务java

这些都是很常见页面,咱们一般将html/js/css等静态资源放到CDN上,而后页面加载后,再经过CGI去拉取最新的数据,进行拼接展现, 这样子能够利用到CDN的多地部署和就近接入等优点,同时提升了服务器的并发能力。这种传统模式的加载流程以下所示:
加载流程git

  1. 用户点击后,通过终端一系列初始化流程,好比进程启动、Runtime初始化、建立WebView等等。
  2. 完成初始化后,WebView开始去CDN上面请求Html加载页面。
  3. 页面发起CGI请求对应的数据或者经过localStorage获取数据,数据回来后再对DOM进行操做更新

能够看出上述流程存在着几个问题:github

  1. 从外网统计数据来看,用户的终端耗时在1s以上,这意味着在这1s多的时间里,网络彻底是空闲在等待的,很是浪费;
  2. 页面的资源和数据彻底依赖于网络,特别是用户在弱网络场景下,页面会出现很长时间的白屏,体验很是差;
  3. 由于页面的数据依赖于动态拉取,加载完页面后,每每是看到一些模块先转菊花,再展现,体验也是很差的。同时这里涉及到较多数据更新,常常要更新DOM,性能上也有很多开销。

因此针对以上几个问题,咱们也对应作了不少优化和探索。web

问题

VasSonic的前世

优化终端

针对终端耗时1s以上的状况,咱们对手Q WebView框架进行了重构:算法

  1. 启动流程完全拆分,设计为一个状态机按序按需执行
  2. View相关拆分模块化设计,尽量懒加载,IO异步化
  3. X5内核在手Q中的独立进程中提早预加载
  4. 建立WebView对象复用池

关于第四点,咱们想分享一些Android平台上的细节,因为Android系统的生态缘由,致使用户的系统版本和系统Webkit内核处于极其分裂状态,因此咱们公司在手Q和微信统一使用X5内核。相对系统WebView来讲,首次启动X5内核时,建立WebView比较耗时,所以咱们尽可能想复用WebView,可是WebView倒是与Activity Context绑定。销毁复用的时候,须要释放Activity的Context,不然会内存泄露。针对这种状况,有没有一种一箭双鵰的办法呢?缓存

计算机有一句经典的名言:计算机领域任何一个问题均可以经过引入中间层来解决。因而咱们经过包装的方式,实现了一个Context的壳,真正的实现体包装在里面,逻辑调用真正调用到对应的实现体的函数。 通过实验发现,Android系统自己提供了这么一个MutableContextWrapper,做为Context的一个中间层。

咱们会将Activity context包在MutableContextWrapper里面,destory的时候,会将WebView的Context设置为Application的Context,从而释放Activity Context。
相似以下:

//precreate WebView
MutableContextWrapper contextWrapper = new MutableContextWrapper(BaseApplicationImpl.sApplication);
mPool[0] = new WebView(contextWrapper);

//reset WebView 
ct =(MutableContextWrapper)webview.getContext();
ct.setBaseContext(getApplication());

//reuse WebView
((MutableContextWrapper)webview.getContext()).setBaseContext(activityContext);

静态直出

“直出”这个概念对前端同窗来讲,并不陌生。为了优化首屏体验,大部分主流的页面都会在服务器端拉取首屏数据后经过NodeJs进行渲染,而后生成一个包含了首屏数据的Html文件,这样子展现首屏的时候,就能够解决内容转菊花的问题了。
固然这种页面“直出”的方式也会带来一个问题,服务器须要拉取首屏数据,意味着服务端处理耗时增长。
不过由于如今Html都会发布到CDN上,WebView直接从CDN上面获取,这块耗时没有对用户形成影响。
手Q里面有一套自动化的构建系统Vnues,当产品经理修改数据发布后,能够一键启动构建任务,Vnues系统就会自动同步最新的代码和数据,而后生成新的含首屏Html,并发布到CDN上面去。

直出

离线预推

页面发布到CDN上面去后,那么WebView须要发起网络请求去拉取。当用户在弱网络或者网速比较差的环境下,这个加载时间会很长。因而咱们经过离线预推的方式,把页面的资源提早拉取到本地,当用户加载资源的时候,至关于从本地加载,即便没有网络,也能展现首屏页面。这个也就是你们熟悉的离线包。
手Q使用7Z生成离线包, 同时离线包服务器将新的离线包跟业务对应的历史离线包进行BsDiff作二进制差分,生成增量包,进一步下降下载离线包时的带宽成本,下载所消耗的流量从一个完整的离线包(253KB)下降为一个增量包(3KB)。
带宽优化

通过一系列优化后,在Android平台上,点击到页面首屏展现的耗时从平均3s多下降为1.8s,优化40% 以上

数据对比

VasSonic的诞生

虽然经过静态直出和离线预推等方式优化后,速度已经达到1.8s,但还存在很大的优化空间,当咱们准备持续深刻优化时,咱们的业务形态发生了新的变化。

以前咱们页面内容的数据主要是由产品经理要配置的,用户看到的内容基本都是同样的。而如今页面为了更好地为用户推荐喜欢的内容,咱们后台引入机器学习和随机算法来作智能个性化推荐。好比左边新用户推荐的是新货精选,而右边活跃用户展现的是潮品推荐。另外还有部分的内容是随机算法推荐的。这意味着不一样用户看到的内容是不一样的,同一个用户不一样时间看到的内容也有可能不一样。

新业务

因此为了知足业务的需求,咱们只能实时拉取用户数据并在服务端渲染后返回给客户端,也就是动态直出的方案。

可是动态直出方案存在几个比较明显的问题:

  1. 服务端实时拉取数据渲染致使白屏时间长,由于服务器要先实时拉取我的数据,而后进行渲染直出,这个耗时不可控;
  2. 首屏没法使用离线预推等缓存策略,由于每一个用户看到的内容不同,咱们没法经过静态直出的方式那样把Html所有发布到CDN;

虽然动态直出方案下,页面首屏没法经过离线预推等方式进行加载优化,但前面优化积累的经验给咱们提供了思路:要优化白屏问题,核心仍是得从提高资源加载速度方向入手。因此咱们重点在资源加载方面进行了深度优化。

并行加载

首先在加载流程方面,咱们发现这里WebView访问依然是串行的, WebView要等终端初始化完成以后,才发起请求。虽然终端耗时优化了很多,可是从外网的统计数据来看,终端初始化仍是存在几百毫秒的耗时,而这段时间内网络是在空等的。

串行

所以性能上不够极致,咱们优化代码,这两个操做并行处理,流程改成:

并行

并行处理后速度有所改善,但咱们发如今某些场景下,终端初始化比较快,但数据没有完成返回,这意味着内核在空等,而内核是支持边加载边渲染的,咱们在并行的同时,可否也利用内核的这个特性呢?

因而咱们加入了一个中间层来桥接内核和数据,内部称为流式拦截:

桥接流

  1. 启动子线程请求页面主资源,子线程中不断讲网络数据读取到内存中,也就是网络流(NetStream)和内存流(MemStream)之间的转换;
  2. 当WebView初始化完成的时候,提供一个中间层BridgeStream来链接WebView和数据流;
  3. 当WebView读取数据的时候,中间层BridgeStream会先把内存的数据读取返回后,再继续读取网络的数据。

经过这种桥接流的方式,整个内核无需等待,继续作到边加载边解析。这种并行的方式让首屏的速度优化15%以上,进一步提高了页面加载速度。

动态缓存

经过并行加载,咱们极大地提高了WebView请求的速度,可是在弱网络场景下白屏时间仍是很是长,用户体验很是糟糕。因而咱们在思考,是否可以将用户的已经加载的页面内容缓存下来,等用户下此点击页面的时候,咱们先加载展现页面缓存,第一时间让用户看到内容,而后同时去请求新的页面数据,等新的页面数据拉取下来以后,咱们再从新加载一遍便可。

动态缓存

保存页面内容这个工做很简单,由于如今咱们资源读取都是经过中间层BridgeStream来管理的,只须要将整个读取的内容缓存下来便可。
因而咱们就按动态缓存这种方案去实现了,但很快就发现了问题。用户打开页面以后,先是看到历史页面,等用户准备去操做的时候,忽然页面白闪一下,从新加载了一遍,这种体验很是差,特别在一些低端机器上,这个白闪的过程太明显,很是影响体验,这是用户和产品经理都不能接受的。因而咱们在思考,可否只作局部的刷新,仅刷新变化的元素呢?

经过分析,咱们发现同一个用户的页面,大部分数据都是不变的,常常变化的只有少许数据,因而咱们提出了模板(template)和数据块(data)的概念:页面中常常变化的数据咱们称为数据块,除了数据块以外的数据称为模板。

页面分离

咱们将整个页面html经过VasSonic标签进行划分,包裹在标签中的内容为data,标签外的内容为模版。

页面规范

首先咱们对Html内容进行了扩展,经过代码注释的方式,增长了“sonicdiff-xxx”来标注一个数据块的开始与结束。
而模板就是将数据块抠掉以后的Html,而后经过{albums}来表示这个是一个数据块占位。
数据就是JSON格式,直接Key-Value。
固然,为了完美地兼容Html,咱们对协议头部进行了扩展,好比增长accept-diff来标注是否支持增量更新、template-tag来标注模板的md5是多少等。OK,有了上面这个规则或者公式后,咱们就能够实现增量更新了。

请求规范约定

VasSonic为了支持区分客户端是否支持增量更新等能力,对头部字段进行了扩展

字段 说明 请求头(Y/N) 响应头(Y/N)
accept-diff 表示终端是否支持VasSonic模式,true为支持,不然不支持 Y N
If-none-match 本地缓存的etag,给服务端判断是否命中304 Y N
etag 页面内容的惟一标识(哈希值) N Y
template-tag 模版惟一标识(哈希值),客户端使用本地校验 或 服务端使用判断是模板有变动 Y Y
template-change 标记模版是否变动,客户端使用 N Y
cache-offline 客户端端使用,根据不一样类型进行不一样行为 N Y

cache-offline字段说明

字段 说明
true 缓存到磁盘并展现返回内容
false 展现返回内容,无需缓存到磁盘
store 缓存到磁盘,若是已经加载缓存,则下次加载,不然展现返回内容
http 容灾字段,若是http表示终端六个小时以内不会采用sonic请求该URL

模式介绍

VasSonic根据本地是否有缓存以及本地缓存数据跟服务器数据的差别状况分为如下四种模式。

模式 说明 条件
首次加载 本地没有缓存,即第一次加载页面 etag为空值或template_tag为空值
彻底缓存 本地有缓存,且缓存内容跟服务器内容彻底同样 etag一致
数据更新 本地有缓存,本地模版内容跟服务器模版内容同样,但数据块有变化 etag不一致 且 template_tag一致
模版更新 本地有缓存,缓存的模版内容跟服务器的模版内容不同 etag不一致 且 template_tag不一致

首次加载

咱们会在请求头部带上支持accept-diff为true和sdk版本号等标识着首次加载的信息。当请求返回后,VasSonic会在延迟几秒后(避免激烈IO竞争)将页面抽离成模板和数据并保存到本地。此时终端缓存目录下,该页面将对应三个缓存文件xxx.html、xxx.template、xxx.data,其中xxx是该页面的惟一标识(即sonicSessionId)。

对于页面非首次加载场景,VasSonic优先加载本地缓存, 同时咱们会在请求头部带上当前缓存和模板的md5,后台进行模板md5对比以后,分为如下几种状况:

非首次加载之彻底缓存

本地有缓存,且缓存内容跟服务器内容彻底同样.

非首次加载之增量数据

增量数据

若是模板发现没有变化,那么会在响应头部返回template-change=false,同时响应包体返回的数据再也不是完整的html,而是一段JSON数据,及所有的数据块。咱们如今须要跟本地数据进行差分,找出真正的增量数据,如上图中,后台返回了N个数据,实际上仅有一个数据是有变化的,那么咱们仅须要将这个变化的数据提交到页面便可。通常场景下,这个差别的数据比所有数据要小不少。若是页面拆分数据得更细,那么页面的变更就更小,这个取决于前端同窗对数据块的细化程度。

得到变化数据块(diff_data)后,客户端只须要通知页面页面设置的回调接口(getDiffDataCallback)进行界面元素更新便可。这里javascript的通讯方式也能够自由定义(可使用webview标准的javascript通讯方式,也可使用伪协议的方式),只要页面跟终端协商一致就能够。
提交增量

对于数据更新这种场景,终端还会将新的数据和模板拼接成为新的页面,保持缓存最新。当终端初始化比较慢的时候,WebView去加载缓存的时候,这个页面可能已是最新的了,连数据刷新都不须要。

非首次加载之模板更新

与数据更新模式不同,因为业务需求,页面的模板会发生更改。当终端在获取到新的模板和数据后,本地在子线程中进行合并,生成一个新的缓存,而后回调通知终端,刷新WebView来加载新的缓存。

咱们来看一下最终的流程图,跟动态缓存对比,有很多细节优化:

总体流程

咱们从第2步开始,SonicSession首先会去读取缓存。会抛个消息通知WebView读取缓存,若是Webview已经准备好,则直接加载缓存,若是没有,则缓存先放在内存里面。同时SonicSession也会带上模板等信息到后台拉取新的内容,后台通过Sonic-Diff以后,会返回新的数据。SonicSession拿到新的数据后,首先会跟本地数据进行Diff,若是发现WebView已经加载缓存,则直接提交增量数据给页面。不然继续拼接最新的页面,替换掉内存里面的缓存,同时保存到本地。这个时候WebView若是Ready,则直接进行第5步load最新的内容便可。

效果统计

效果统计

这个是咱们外网的统计数据。在数据更新模式下,首屏的耗时在1s左右,相比普通的动态直出,优化了50%以上。模板更新这个会比首次高,是由于加载了两次页面,不过从模式占比上来看,咱们大部分页面都是数据更新。针对模板更新这种耗时比较高的状况,前面优化积累的经验给咱们提供了思路,核心仍是从提早获取资源方向入手,所以咱们优先考虑如何预加载模板更新。

预加载

实际上整个SonicSession在没有WebView的状况下,也是能够独立完成全部逻辑的,当用户点击页面的时候,咱们在将WebView和SonicSession绑定起来便可。因而咱们支持了两种预加载的模式,一种是经过后台push的方式,来提早获取数据。还有一种就是JSAPI,页面能够调用JSAPI来预加载用户可能操做的下一个页面。经过这两种方式,咱们能够把须要的增量更新数据提早拉取回来
预加载

效果对比

Pic 1: 没有使用VasSonic Pic 2: 使用VasSonic
default mode VasSonic mode

展望将来

开源只是故事的开始,咱们仍会持续对 VasSonic 作改进,包括更易用的接口、更好的性能、更高的可靠性,同时快速响应解决开源后的issue和PR。这些改进最终也会原封不动地在手Q内使用,这一切都是为了更快的WebView加载速度。

Talk is cheap,read the fucking code. If you are interested in VasSonic, don't forget to STAR VasSonic.Thank you for reading ~

相关文章
相关标签/搜索