去年(2015)四月份,我在 QCon 北京大会上分享了阿里旅行 Hybrid 实战经验,作为航旅在 Hybrid 方向探索的一个收尾。当下集团内的重量级 App(手淘、钱包等)在 H5 容器建设上成长迅速,形成了宏大的技术体系,到去年双十一,H5 容器所承载的流量已经远远超过了有限的 Native Page。就航旅来说,H5 承载的流量是 App 的至少四倍。无疑,处在应用层的 Web 技术栈,以其独有的运行时环境(WebKit)、普适的技术标准(W3C & ES 5、6)以及极具优势的研发灵活性,成为面向 UI 和交互无线研发的不二解决方案。而 H5 离线技术体系的逐渐成熟,让 H5 和 Native 的融合达到前所未有的深度。
资源离线的思路简单、场景复杂,最复杂的就是 H5 活动页面的离线化。今天跟大家分享的就是航旅去年在 H5 活动页推包体系建设的一些实践。
就加载性能来说,资源离线和发版频度是一对矛盾。活动页结构简单,不具备复杂的业务逻辑,但变更极其频繁,对时效性要求很高,这种更新频繁程度在双十一期间表现最甚。我们在去年 4 月份立项的独角兽项目,集中精力建设活动页推包体系,试图克服这一对矛盾。
资源离线是加载性能优化的“二向箔”
Mobile Web 在弱网提速的唯一的办法就是坚决杜绝不必要的(运行时)网络请求,即除了 Json 格式的动态数据和其携带的商品配图之外,不应再有其他网络请求(埋点请求除外)。所以,HTML 和业务数据之间必须解耦。在此基础上,航旅的信鸽平台力争完成这四项任务:
- 定时程序:紧随页面结构(HTML)变更进行推包
- 增量包:增量包(保性能)和全量包(保安全)必须同时提供
- 服务器推:基于长连接的消息推送 & 客户端静默更新
- 自动化:推包过程必须自动化,解放人工,搭好页面点击“发布”即完成推包
航旅的页面也会通过 Zcache 在手淘中做离线,但由于缺少对线上页面变更的监控,所以双十一会场页面仍然无法在 Zcache 中缓存 HTML,只能缓存 JS、CSS 和 Img。我们知道,HTML 是否缓存对弱网加载速度影响很大,下图是航旅双十一主会场页面在两个端里 2G 网络下的加载瀑布:
可以看到,手淘 App 里的渲染时机被 HTML 网络请求推后了,而且有一个更新后的 CSS 文件请求,不适时宜的阻塞了渲染。
定时程序动态推包
所以,针对去啊 H5 容器,我们写了两个程序来弥补 Zcache 在缓存动态页面方面的不足,
- o2o 在线资源抓取程序:基于 phantomjs 解析资源
- grunt-inc-offline 增量包计算器:基于 git-diff 的增量包运算
当然离线包生成器也是必不可少的,我们用信鸽平台将它们这样整合起来:
- o2o 定时程序监听线上页面变更,将其所携带的资源(HTML、CSS、JS 和部分图片)抓取下来
- 增量包计算器会计算好与之前若干版本之间的增量文件,配合包生成器将增量包逐一构建打包,同时生成好每个增量包的 Diff Json
- 调用 Clam 命令通过 Gitlab 将资源包部署至 CDN,以备手机端更新。
- Gitlab 仓库 的更新会触发一个 Hook 脚本,调用 tSync 服务器的接口,来通知资源变更
- tSync 服务器沙箱完成消息封装,包括了第二步生成了的 Diff Json 文本
- tSync 长连接将消息指令下发给手机终端
- 手机终端拼好资源文件链接,从 CDN 将增量包更新下来,随后执行 Diff Json中的指令,完成包的更新。
其中,获取增量包时,手机会将本地离线仓库版本带上,和远端 tSync 消息中的更新包版本一起拼成资源包的 URL(一个真实的增量包 URL),格式形如:
http://g.alicdn.com/trip/h5-op2op/{$线上最新版本}/{$本地包版本}.zip
另外,Diff Json也很干净,包含了“新增”、“删除”、“更改”,这样可以让客户端来删除旧文件,减少新包覆盖后的冗余。平台的易用性上,信鸽平台的操作界面里很贴心的加上了“快速下线”功能,即一旦发现离线包更新到达率不够,可以立即做离线包下线,端的虚拟域会自动切换到线上页面。
可以看到,整个系统关键在两个平台的衔接,即信鸽平台和 tSync Server。两者的分工很明确:
- 信鸽平台面向在线的 URL 完成资源抓取、构建和部署。
- tSync Server 完成消息推送和动态更新。
只要思路捋顺,整个推包的逻辑设计并不难,难的是这些模块的实现是否可靠健壮,其中较为核心的模块就是 o2o 定时抓取程序,o2o 定时程序是@弘树 开发的独立的命令行脚本,将资源离线的过程中,需要根据文件内容来决定文件路径的哈希值,即只要文件内容不变,离线后的引用路径就不变,这样就比较容易由 git-diff 程序来计算增量文件,毕竟文件是否属于“新增”应当看内容是否有变化,而不应该根据文件名(或者版本号)的不同来判断。
由于 o2o 是基于 phantomjs 来抓资源,所以,以 o2o 为原型我们衍生出了 o2o-capture 项目,将线上页面完全静态化到本地,用来做 TMS 系统挂掉的容灾备份方案,也是棒棒的。
有了完整的外围设施,手机端就可以聚焦在文件 IO 的性能优化上了,之前也介绍过,航旅 H5 容器用多层保障来加速 Local File 的文件读写,一方面避免不必要的 IO、另一方面将资源池管理和运行时的缓存管理隔离开,确保各自程序任务聚焦、高效,下图是手机端的两个重要进程:
- 资源预加载进程:在实际访问页面之前,将资源预加载到缓存池并更新 Cache Map
- 创建 WebView 进程:只聚焦本地资源读写,别的什么也不干
所以手机端 touch 到网络的环节收敛到了两处,第一,Package Update Controller,第二,WebView 本身必要的网络请求:
最终,在定时程序的帮助下,我们可以放心的将 HTML 也离线到端,而不必担心更新不及时的问题,配合高性能的 H5 容器,做到秒出就是自然而然的事情了。我们来看 2G 下去啊 App 和 手淘加载航旅会场页的速度对比,显然,去啊 App 的离线更干净彻底,不管首次加载还是二次加载,速度都是很可观的:
从全网的性能数据看亦是如此,下图是航旅双十一预售阶段的数据,10月28日主会场页在去啊、钱包、手淘里各种网络情况下的 DomReady 时间统计。在 2G 网络下,支付宝和手淘基本都卡在 HTML 请求阻塞上。
三端在三个网络下的 DomReady 时间对比(单位秒),结果是显而易见的:
应当说明的是,这是在手淘和钱包没有条件缓存活动页 HTML 资源的情况下拉的数据,依照上面的思路,我们也写了一个外围工具 zcache pusher,来半自动化推送动态更新的页面,理论上手淘也是可以做到 2G 离线加速的。
此外,Zcache 较早前就有计划和新版 TMS 尝试打通,让离线操作自动化起来,期待能尽快看到进展,和我们一样,Zcache 也会面临这个问题:“发版频度和离线包更新到达率”的问题。那么,影响包的到达率的因素都是什么呢?
发版频度 vs 离线包更新到达率
信鸽的整个体系对性能的考验不是来自架构设计,而来自硬件瓶颈,尤其是当定时程序检测到线上页面频繁部署(比如双十一期间,航旅会场页面每天更新频率峰值多达四十多次每天),频繁推包会带来两个问题:
- 手机终端是否能及时更新到最新版的离线包
- 如果要保证更新足够及时,频繁静默更新又会大量占用手机的流量
显然,这两点是相互矛盾的。我们既希望用户尽可能及时的更新到最新版的离线包,又不希望这种频繁推送过多占用手机流量。这种情况下,增量包只能确保用户及时更新的情况下,尽可能少的占用流量,而无法完全杜绝。也就是说,不管是手淘、钱包还是航旅 App,目前离线包的推送策略仍然过于“全量”,稍显粗暴,用户“无条件”接受所有更新。即目前所有端都做不到对用户行为的精准判断,做不到用户需要(订阅)A,我就给他推包 A。所以,信鸽平台和 tSync Server 都存在很大升级空间。
So,在这次双十一执行过程中,在航旅 App 里,这一对矛盾究竟如何表现?下图是10月28日会场页面最新一次推包后的客户端更新比例,左图是推包 1 小时后,右图是推包 2 小时后。
天哪,看看今天执行了多少次推包!
可以看到,相比于过去传统的线上页面部署,离线包的部署时效性显然慢一些,不够 100%
所见即所得。这也是为了换取加载速度不得不做的牺牲。但基本上每次推包 3 个小时候可以完成 90% 以上的更新。受用户所在网络和打开时机等因素影响,散落在手机端里的离线包旧版本的碎片化依然非常严重。不管用了多少优化手段,频繁修改页面、频繁推包都会对页面体验带来负面影响。
所以,对于时效性很强的页面,比如凌晨零点的发布需要切会场的场景,需要页面即时部署即时生效。若要走离线有两个方案可以选择:
- 提前(至少四个小时)推离线包,需要在页面中写好定时切换的逻辑
- 提前推消息指令,先将离线页面从端删除,在切换时同时执行线上部署和离线推包,去啊客户端支持虚拟域, 在线离线页面 URL 在容器中保持一致,在保证线上页面可用的情况下,逐步扩大离线包在端的覆盖率。
但不论哪个方案,我们都不可能像过那样部署线上资源那样轻松了。如果需要更高的时效性 + 更快的加载速度,则必须适度减少琐碎需求变更,降低推包次数。从这个角度看,决定性能的因素这里已经主要不在技术上、而在工程上了。
今年手淘(天猫)双十一主会场页面也遇到同样的状况,本来 Weapp 可以非常优雅的将 H5 Page 转义成 Native Page,理论上是可大大提速的,但还是为了满足页面动态性更新和个性化配置,不得不引入一些额外的网络开销,这些网络开销不合时宜的阻塞了布局的渲染,在弱网里的影响更大。我们来看去啊 App 里首次进航旅主会场(H5)和手淘中首次进天猫主会场在移动 3G 下的加载速度:
可以看到,H5 页面是逐级加载的,Native Page 是等待请求完成后瞬时渲染的。所以,不管是 H5 还是 Native,只要是应对这种频繁修改部署变更的活动页面,都会遇到加载上的瓶颈。信鸽平台是解决这对矛盾的一个缓冲,但根本上还是要从控制页面灵活性角度来求解。
但是,这种离散性未必都是坏事,它能适度缓解安装包体积膨胀的压力。
安装包体积极限
大家相信吗,手淘和猫客的安装包都过百兆了。这已经到了一款电商 App 包体积的极限。在上一篇文章中也提到,目前客户端技术架构在面对新功能的井喷时显得力不从心。H5 是一个方案,将资源和内容置于远端,但又会极大稀释客户端的体验。离线包技术就很合事宜的弥合这对矛盾。
但这显然不够,和钱包 App 的早期一样,航旅 App 在构建安装包时就会“预装”一部分重要 H5 资源,但面临高速迭代的产品需求,这个缓冲区是远远不够用的。目前,钱包和手淘显然已经将这个缓冲挤占完全挤占:
以去啊 App v6.0 为例,7 M
的 H5 离线包承载了将近 40%
的功能性页面 和 100%
的活动页面。所以只要你的 H5 页面质量够高,H5 离线包的体积消耗是非常划算的。这也是在航旅 BU 正在发生的事情,即便是在交互极其华丽的“去啊 App 6.0 行程”项目中,PM 还是不断从前端团队抽调同学参与一些关键页面的研发,一方面大家已经不担心 H5 体验的瓶颈,另一方面,“前端同学搭界面真是快!!!”
所以,高性能 H5 容器配合高效的离线包推送体系,再加上高质量的 H5 代码(通过 Clam 工具来保证),做出全网络“无缝秒出”的体验是完全没有问题的(体验视频):
大家还可以感受下航旅这次双十一的无线狂欢城和跑步游戏在3G
网络中的表现,
经过上面这些折腾,最终就达成了我们希望的结果:
- 快速的页面研发
- 灵活的部署、持续交付
- 无缝秒出
- 划算的功能体积比
- 时效性强的活动页也能做到高效离线化!
这就是航旅无线技术团队正在做的事情。好像很爽的样子,下面我们来说但是吧。
另一种混合
由于 WebView 对 App 进程来说是一个沙盒,所以 H5 页面的内存分配和 CPU 分片都是 WebView 独立完成,前端代码因为普遍缺少细致的内存管理,所以内存泄露时有发生,以至于H5 容器一定程度会影响 Crash 率。比如手淘 Android 就会限制打开的 WebView 堆栈的个数来减少内存压力。
ReactNative 是一个解法,就像我跟@小马 开玩笑时讲的,“前端同学用 React 搭了一个 App,就好像 Java 开发同学用 Bootstrap 搭了一个后台界面一样兴奋”。和 Bootstrap 一样,ReactNative 是业余 Native 开发同学的脚手架,是无法做出面向 C 端的产品的,只能做一做“阿里内外”这种量级的应用。
还有一个方向就是重新设计动态的 Native 页面布局,集团内代表性的就是鸟巢、 Dynative和今年双十一手淘在尝试的基于 Web Component 在三个端同构渲染的 Weapp,尽管对于复杂列表还是偶有性能问题,但至少整个 UI 的渲染已经脱离了 WebView,内存更加可控。我们也必须承认,在运行时性能上,不管是 H5 翻译 Native 还是原生 Native,都要强过 H5 容器的,尤其是在超长复杂列表的渲染上。相对于鸟巢和 Dynative 的全 UI 渲染,航旅也在规划 H5 和 Native 的交叉渲染技术的尝试,总体思想是借助 JSCore 和一个删减版的 Webkit 内核来渲染翻译好的 HTML 片段,页面交由 Native 来拼装:
这个方向我们也是刚刚起步,希望能和兄弟 BU 们一同共建。
小结
航旅在 Hybrid 方向上做的事情,并非要证明航旅 H5 比其他端快,毕竟应用场景不同(比如手淘就有选择的忽略 3G 和 2G 网络)。但从航旅和集团各 BU 的混合开发实践来看,其实大家都在朝着一个方向努力,Native 期望获得 H5 快速开发和部署能力、H5 期望获得更快的速度和更高的硬件调用权限,两个思路:
- H5 容器技术(得益于 Clam 工具的保障,手淘、钱包、去啊 各自的容器,已经做到
90%
相互兼容):- 优势:独立的 WebView,对前端开发友好,天然获得多端兼容的特性
- 劣势:外围体系化建设难度大
- 需要克服:一,硬件调用能力,通过桥协议解决;二,秒出保证,通过“离线包体系” + “精心编程的 H5 页面”解决
- H5 代码 Native 化(鸟巢、Dynative、ReactNative、Weapp,互不兼容):
- 优势:H5 代码编译成二进制代码直接运行,天然的秒出体验
- 劣势:对前端开发极其不友好
- 需要克服:一,阉割版的布局能力,通过增加对 CSS3 标签的支持来解决;二,无法做到多端兼容,通过限制 H5 语法来解决
可见,两个思路都各有取舍、各有克服,都很难完美,从实践效果来看,H5 容器更加适合哪些对多端部署有要求的 BU,航旅就是一个典型。而 H5 的 Native 化方案更加适合独立客户端的一些私有场景,比如支付宝和天猫。所以要根据自身需求来选择技术路线。
我作为一名传统的前端工程师,从 PC 时代转战到无线,从去年航旅开始无线研发模式的探索以来,我也很幸运的尝鲜各种 Native 技术,从开始的好奇,到现在一大堆体系和工具的落地,一系列探索让我自己脑洞大开,也打破了之前固有的编程理念,这两年前端技术被颠覆的如此剧烈,让人有点不敢相信自己的眼睛。我想一方面,PC Web 开发的迅速衰落为无线前端技术快速崛起带来契机,另一方面,无线技术快速崛起,带来的不仅仅是技术体系的混合(H5 和 Native),更多的呼唤人的混合。我们很欣慰的看到一大堆前端同学在研究 ReactNative,另外一大堆 Native 同学在研究 W3C 和 HTML5。打破技术边界、拥抱无线技术的 All In,充满好奇,用怀疑一切的眼睛去看待自己固有的技术理念,投身并享受新一轮的无线技术变革,我想,或许正是因为此,使得阿里无线端技术体系伴随业务的增长,不断走向百花齐放、走向多元的吧。
To Be Continue...
一些 Q & A:
Q:现在 Wifi 是最多的用户网络,为什么要纠结于弱网用户?
A:首先,我们希望在最严苛的环境中考练技术,再者,广大(地级)市、县乡这些 Wifi 覆盖率低的地区,我们都亲身体验过那里 4G 和 3G 是什么网速。
Q:这种强混合的研发模式,对前端技术框架有什么影响?
A:影响是颠覆式的。前端的模块化编程过于依赖 Loader 和上层的模块规范,而 Mobile Web 里是不是还强依赖 Loader 要打一个问号,一方面,Loader 本身太重,另一方面,Loader 是在运行时去组织资源依赖加载,显然会抢占宝贵的运行时资源,资源的组织和加载应当下沉交给更底层的 H5 容器或者工具去解决。
Q:H5 容器即是浏览器,怎么去平衡 Web 的加载性能和运行时性能?
A:这篇文章主要说的就是加载性能的解决方案。运行时性能我们也有一揽子的最佳实践,另开一篇介绍。
Q:基于 H5 容器毕竟不像浏览器,怎么快速打离线包调试?
A:这方面手淘、钱包 都有最佳实践。航旅通过工具可以实时构建 zip 离线包,直接拷贝到手机即可。目前是最土,也是最有效的办法。
Q:航旅怎么做到 7 M 的离线包完成 App 里40%
的功能页面?
A:我们化了一整年来做体积瘦身的优化,另开篇介绍吧。
Q:上面提到的离线数据从哪里来的?
A:航旅自己提供了一个非常全面的打点方案 tracker,目前部署上抽离的还不是很够,但基本上我们想要的关键数据可以比较精确的取到了。大部分数据可以通过鱼眼来查看。
该文章来自阿里巴巴技术协会(ATA)精选集