QQ直播 是 PCG 社交线下在直播领域的一个重要业务,主要包含秀场、交友、游戏等直播内容,满足了用户浏览搜索直播内容及观看视频直播的诉求。
本文的主体是 QQ直播大厅,核心功能是展示各直播品类下的直播内容,采用了腾讯对外开源的类 RN 动态框架 Hippy 作为技术栈,关于 Hippy 框架的信息,请移步开源项目 Tencent/Hippy 了解更多。
本文将针对功能迭代过程中遇到的一些性能问题进行全面剖析,并详细阐述过程中采取的优化手段。
本文适合的阅读对象:对跨端开发感兴趣的所有朋友们,万字长文,多图预警,看不完也一定要收藏/点赞,有需要用到的时候再回来继续阅读交流。
优化前页面加载录屏如下,优化后效果视频直达第 4 节查看。
目标:降低大厅首屏耗时,从而降低用户感知的页面加载耗时,提升用户体验。
一个 Hippy 页面加载阶段,经历了哪些耗时阶段呢?
如何衡量一次性能优化是否有效?
为了帮助开发者更好地衡量和改进Hippy页面性能,手Q hippy 开发团队提供了performance API,以获取Hippy加载当前页面的性能打点。
performance API 提供了哪些性能打点?
在手Q 平台调用 QQDebugModule#getPerformanceData,可以获得终端性能打点时间点,下面是各时间点含义:
我们可以通过以上的时间点,计算出Hippy页面终端性能指标,如下:
到这里,我们会发现 performance API并没有前端相关的性能打点,这部分需要业务按自身需要进行打点上报。
通过以上前端时间点,可以就可以计算出前端性能指标:
再配合终端性能打点,可以计算出首屏耗时、终端耗时、前端耗时等关键耗时,如下:
有了性能指标的量化标准,我们就可以通过统计现网真实用户数据来重新定义性能问题。
【首屏耗时瀑布图】
可能增加首屏耗时的问题
页面性能损耗无处不在,贯穿整个页面生命周期,任何一次代码提交都有可能引入性能问题,所以性能优化是一项持续性的工作。
性能优化应该遵循 “前期抓大放小、后期长尾优化” 原则,前期将精力聚焦在预计带来80%收益的那部分工作,后期再花时间解决另外棘手的部分。
本文主要分 终端、前端、后台 三个方向来着手首屏耗时优化。
【阶段耗时优化方向脑图】
现状:
tool进程打开 hippy 速度慢,影响初始化速度。直播业务整体包括首页和直播间是放在 tool 进程启动,如果放在主进程启动,打开速度会更快,但会导致手 Q 内存暴增,因为直播间有两个播放器+首页 hippy 预览。
预渲染、预加载方案,都无法优化掉初始化耗时,非常依赖主进程启动,那有没有可能在主进程启动?
优化方案:
方案一:解决内存暴增问题,推动直播间业务放在主进程启动;
方案二:将首页和直播间拆开,首页放到主进程启动,直播间还是放在 tool 进程;
初步估计方案二更可行,但如果只是将首页放在主进程中,和直播间跨进程通信,对终端维护成本较高,不仅影响现有系统架构,可能引入影响其他的性能问题。最终,没有采用主进程打开 hippy 的方案。
升级运行环境被否掉了,那还有没有其他更直接有效的优化方案?
换个思路,将页面加载提前一点。
目前,手 Q hippy 已提供预下载、预加载、预渲染、预截屏四种预处理优化方式。
先看看四种方式是做了什么:
预下载:在页面没有打开前,提前下载js-bundle。当用户点击业务入口后,不需要下载js-bundle。
预加载:在页面没有打开前,提前准备和解析js-bundle。当用户点击业务入口后,直接将内存的js-bundle运行。
预渲染:在页面没有打开前,提前运行页面js-bundle,并且生成原生视图到内存。当用户点击业务入口后,直接将内存的视图展示给用户。
预截屏:在页面没有打开前,提前运行页面js-bundle,生成hippy的快照,并将快照信息保存到硬盘。当用户点击业务入口后,终端同时加载这些快照信息并展示,在业务页面展示成功后,通知终端移除这些快照信息。
手 Q hippy 大佬非常给力,一下子提供了四种预处理方式,该如何选择呢?
下面分别列举了四种预处理方式的优劣势:
综合对比:
以上排序,仅凭个人认知实践给出,不代表官方观点。
再结合直播业务特性,列表页进直播间转化是非常重要的指标,所以不能接受预截屏【无法响应点击事件】的问题,而且列表内容变化比较频繁,比较容易触发快照和真实页面内容不一致的情况,所以首先放弃预截屏。
【终端预处理优化示意图】
从上图看出,预下载、预加载,仅对终端耗时阶段做了优化,而预渲染优化了整个 hippy 页面耗时阶段,相当于提前将页面加载了一遍。遵循 80/20 法则,先着手把预渲染上了,如果效果好,再将剩余的次活跃用户安排上预加载、预下载能力,以达到 ROI 平衡。
所以,
QQ 直播大厅第一个版本就上线了预渲染,预渲染带来的收益巨大,预渲染相比非预渲染首屏耗时提升了 90%,同时用户感知实际加载效果也非常亮眼:
看到这里,是不是觉得这样的效果已经非常棒了,还需要优化啥子啊,不要再卷了。
其实,上面我们提到预渲染这种方式非常吃内存,搞不好直接把手 Q 搞崩了,原因参考接入手Q预渲染指南。当然手 Q hippy 大佬们不会让这样的事情发生,制定了非常严格的预渲染优(zhun)先(ru)级(men)策(kan)略,需要在预渲染号码包内的用户才能触发预渲染,因此目前手 Q hippy 业务中仅有 10% 接入预渲染,对这个策略感兴趣的同学可以进一步了解。
QQ 直播大厅通过各种 buff 加持,有幸成为在这 10% 的一员,可谓是“含着金汤匙出生”!
虽然达到了预渲染准入门槛,但是由于自己视频基因的特性,带来巨大的内存消耗,整个直播业务被强制关进(Android) tool 进程的小黑屋,这直接影响预渲染复用率,因为 tool 进程可能会被回收导致预渲染实例被销毁。
另外,预渲染的触发时机非常频繁,原因在接入指南有提到,QQ直播大厅主要有两个入口:抽屉页、动态tab,切换其中任意一个入口都会触发预渲染,但不是所有用户都会真正进入大厅。所以,预渲染对后台服务增加负担。
简而言之,预渲染存在以下三点副作用:
这些道理大家都懂,但还是没办法动摇“坚决不卷”的决心啊。
好吧,只能拿数据说话了:
辛辛苦苦接入的预渲染,结果预渲染打开大厅用户占比仅有 3.0%,有点儿绷不住了。
这不应该啊,是哪个环节出现了问题?
现状:
预渲染策略命中率低。上图显示,真实访问的用户中,非预渲染预加载的用户占比在 96.05%,从上一节结论知道,预渲染耗时远优于非预渲染,非预渲染预加载占比过高会导致大部分用户感知的加载会慢,这跟老板的预期不符,需要再卷一卷。
问题分析
要回答上一节“哪个环节出现问题”,那先要知道,预渲染有哪些环节?
预渲染主要有以下几个环节:
① 业务侧数据同学按出包规则,每天计算 T+1 的号码包,落到 hdfs,供后台同步数据
② hippy 后台从 hdfs 同步号码包
③ 用户打开抽屉页或切换到动态 tab,触发预渲染流程
④ 终端按优先级处理预渲染配置
了解了预渲染流程,我们带着以下问题,逐个环节排查:
暂且假设预渲染号码策略不是最优的,下面从出包规则、数据对比两方面来佐证我的猜想是否正确。
出包规则(全部或):
更新频率:每天更新一次
量级: x 万(后文如无特殊说明,活跃数据等用户数据均已做脱敏处理)
规则有了,一眼看去好像也不能说明什么。
真实用户和预渲染号码包取交集,看下到底有多少重合用户:
数据结果说明,真实访问的用户中有一半以上是预渲染号码包用户。
可能心细的同学会发现,96.05% 这个数据并不是完全可靠,因为预渲染页面后还要等待用户真正点击了入口才能算真实访问的用户,这里存在一定的折损。
这么说也有道理,好在手 Q hippy 上报的数据已经非常全面,
我们可以拿客户端打开 hippy 的号码包跟预渲染号码包取交集,结果如下:
从上图可以看到,A∩B/B、A∩C/C 占比几乎一致,占比达到 50%,说明用户是否点击进入的折损,对预渲染号码包的交集影响不大。
真实访问用户在预渲染号码包的用户占比达到 50%,说明数据同学提供的号码包覆盖程度还是不错的。
而真实访问用户中预渲染用户占比仅 3.08%,存在比较大的差距,造成这个差距的原因,还需要进一步确认。
数据同学反馈正常情况下每天 9 点前完成数据出库,一般凌晨 4 点左右完成,除非上游数据延迟。也就是说,号码包出库时效性在 4~27h,跟预期相比晚了 4 个小时,考虑到直播凌晨属于用户活跃低峰期,出包还算及时。
排查到这里,任然没有找问题的根本原因,都僵住了。这里有两个猜想:
顺着 hippy 数据上报这条路继续探索,出于好奇,拿到兄弟业务 A预渲染用户覆盖率数据对比直播大厅数据,不看不知道,相差接近 10 倍,好家伙~
预渲染用户覆盖率:
预渲染用户覆盖率计算规则:
打开 hippy 用户中预渲染进入的占比:
计算规则:打开 hippy 用户中预渲染进入的占比 = 打开 hippy 用户中预渲染进入用户/打开 hippy 用户
相比【预渲染用户覆盖率】,这里算的打开 hippy 用户中预渲染进入用户,不包含预渲染实例被移除变为非预渲染进入的那部分用户
【问题】
相比业务 A,直播的预渲染用户覆盖率很低,其中打开 hippy 用户中预渲染进入的占比更低,和最初现状分析的3.08%的占比几乎一致。
看到这个差异简直又惊又喜,惊的是差距没想到这么巨大,喜的是问题看起来有了些眉目。
【分析问题】
直播&业务 A 号码包差异:
通过对比发现,直播预渲染号码包要和业务 A 对齐,需要做以下三件事情:
1、 2 比较容易实现,而 3 需要花费较多人力去实现,能带来多少收益呢?
考虑到7 日以后留存对整个号码包占比影响不大,出包规则两个业务都是取的活跃用户,活跃时长的应该不是影响的占比的大头。
用 a 万的号码包再取一次交集,A∩B/B占比大概在 60%,就算增长的 10% 都是有效的预渲染用户,加上原有的 10%,也才 20%,相比 50%,还有 30% 的差距。
那只剩下更新频率的变量了,继续沿着留存率对占比的影响思路,假设直播是每天更新一次号码包而且能及时更新到,跟业务 A实时动态更新号码包相比,虽然一个离线 T+1数据,一个是实时数据,预渲染用户覆盖率相差 10 倍,也比较离谱了,直播的留存率真的这么低吗?
因为没有两边找到留存数据,但从另外一个数据佐证,大概率更新号码包出了问题。
如果使用 hippy 上报的数据,计算当日打开 hippy 用户中 7 天内打开过 hippy 的用户占比,发现:
55.36% 这个比例,跟之前取交集的 50%,比较接近了。说明如果那这份 7 日内的活跃用户数据作为预渲染号码包,那么也有 50%以上的用户会预渲染进入 hippy。
这就侧面验证了,hippy 后台用来判断是否预渲染的号码包和数据同学提供的号码包不是同一个的假设。
因此,跟hippy后台同学商量了个对账策略,手动更新直播的预渲染号码包,确保当日用的号码包是T+1 那个包。
预渲染用户覆盖率:
调整前:Android 为 8.82%,iOS 为 10.73%
调整后:Android 为 24.85% ,iOS 为 41.37%
结论:
Android 提升约 3 倍,iOS 提升约 4 倍,其中 Android 提升不及 iOS,可能是号码包采样命中 iOS 用户占比更多。
其次,相比业务 A覆盖率还是相差不少,说明除了更新频率因素外,各业务的活跃、留存的差异也可能对预渲染用户覆盖率产生影响,如何取得最优的预渲染号码包,这里就不继续展开了,既然性能优化是个持久的过程,那就留给下一次优化吧。
既然预渲染不能100%都能受益,那还得继续着手非预渲染加载场景的优化。
【QQ直播大厅加载数据时序图】
【现状】
【优化策略】
将 abtest 结果也放到 tab 配置中?
【预请求首屏数据】终端方案:
终端方案牵扯协同方较多,本次优化中,采用了短期方案在前端预请求首屏数据的方式,快速验证优化效果。
读取列表缓存和banner 缓存是串行的,其中读取 banner 缓存使用了 100ms,这 100ms 是没必要的消耗。
将读取列表缓存和banner 缓存从串行改为并行,省去 100ms 耗时。
包体积大小直接影响下载业务包耗时,减小包体大小能有效降低下载业务包耗时。
Android:
iOS:
【现状】
图片资源体积占用超过 jsbundle,在 hippy 页面中尽量避免使用大图片;
【优化策略】
整体优化掉的体积不多,但是苍蝇腿也是肉,优化成本不大就顺便优化掉了。
vendor 包不包含 react-dom,导致业务包体积大 100kb 左右
【包体分析示意图】
a. vendor 无 react-dom
b. vendor 有 react-dom
但由于涉及到hippy 基础包增量问题,且使用 hippy-react 的业务并不多,多方考量后决定暂时不加入基础包,之后恰当的时机再推动手 Q hippy纳入基础包。
优化业务包体的整体效果:
【现状】
【优化策略】
【节点数量优化】初始化节点有 429 个,如果不在视窗且没有请求回来数据的,可以不生成节点,耗时间隔 150ms 左右
不渲染后,节点数量减少为 117 个,耗时间隔 46ms 左右
【优化后加载流程】
本次优化前已处理过渲染优化问题,主要是解决长列表卡顿问题,不在本次优化的重点内容,优化手段类似 h5 页面,这里不再展开。
【优化策略】
【优化策略】
原逻辑:
优化后:
比如 tab 配置,访问一次仅更新一次
【现状】
【优化策略】
【问题】
【解决方法】
接口减包,详见3.3.2
【缓存方式】使用 web-data,读取缓存 193ms;使用 asyncstorage读取缓存79ms,快一倍多
web-data:
asyncstorage:
【现状】
现在首屏缓存数据大概 30KB,使用的终端接口QQWebDataModule,为了数据安全对数据有做加解密操作,会有一定耗时。
【解决方案】
最初沟通可以尝试使用AsyncStorage接口读取缓存,没有加解密操作,速度确实会快,相比QQWebDataModule快一倍。
但是考虑到:
基于这两点,方案一基本认定不能用于首屏数据缓存,跟 mark 沟通了一下,计划是联合 iOS 一起提供一套通用供业务预请求业务数据的方案,来代替缓存方案,详见 3.2.1.2。
【问题】
读取列表和banner 缓存是串行的,有 100ms 延时,详见3.2.1.3介绍
【解决方案】
由于一次读取缓存A+B相比串行两次读取缓存 A、B 耗时更少,采取【异步写同步读】方式,将串行获取 tab 配置和数据缓存,改为同时获取tab 和数据缓存,降低耗时(对二次访问生效)
和前端、终端协同优化,方案详见 3.2.1
【现状】
推荐列表接口包含了流包、推荐上下文、进房串等大内容,每页 20 条的回包约 80KB,影响接口回包耗时,以及终端通信负载,通过减小接口回包体积降低端到端接口耗时、缓存读写耗时。
【优化策略】
【现状】
【减包方案】
方案一:将进房串【选择对应档位流地址】、预览直播【前端灰度策略】这两步放到后台来做,返回给前端【最终进房串】和【预览直播流地址】。
具体做法:
方案二:针对【现状 4】,jump_url 分端分版本保留 rtmp 或 flv 流地址
方案三:针对【现状 5】,去掉单独的rcmd_context字段,从 jump_url 获取
【预期改动点】
减包后前端选择流程:
减少首屏每页请求条数,目前为 20 条,减少为 10 条。
接口减包整体效果:
【现状】
大厅接口服务端没有开启 gzip 压缩,需要在 ias 开启 gzip 压缩。
【优化策略】
开启 gzip 压缩整体效果:实验数据(请求抓包回包大小):
实验数据(请求抓包耗时):
大盘数据(端到端耗时):
Android 仅支持 http1.1,iOS 支持到 http2,双端暂时都不支持 http3,初步沟通对 http3 的预研效果都表示认可,且在手 Q 中也有业务尝试过http3,相信在不久的将来手 Q hippy 业务也能从 http3 中获益。
优化前后加载效果对比:
优化前后数据对比:
本文重点介绍了 QQ 直播大厅首屏耗时性能优化实践中遇到的一些问题及优化手段,覆盖终端、前端、后台的优化手段,关键优化手段总结为以下几点:
本次优化中提到的支持 http3、预请求首屏数据终端方案还在方案落地阶段,后续会持续优化,此外,未来优化方向可以考虑 HippySSR 方案,相比预渲染来说,没有吃内存问题,真正做到 100% 用户获益。