QQ直播Hippy首屏耗时性能优化总结
1. 背景
QQ直播 是 PCG 社交线下在直播领域的一个重要业务,主要包含秀场、交友、游戏等直播内容,满足了用户浏览搜索直播内容及观看视频直播的诉求。 本文的主体是 QQ直播大厅 ,核心功能是展示各直播品类下的直播内容,采用了腾讯对外开源的类 RN 动态框架 Hippy 作为技术栈,关于 Hippy 框架的信息,请移步开源项目 Tencent/Hippy 了解更多。本文将针对功能迭代过程中遇到的一些性能问题进行全面剖析,并详细阐述过程中采取的优化手段。
本文适合的阅读对象:对跨端开发感兴趣的所有朋友们,万字长文,多图预警,看不完也一定要收藏/点赞,有需要用到的时候再回来继续阅读交流。
1.1. 性能问题
优化前页面加载录屏如下,优化后效果视频直达第 4 节查看。
1.2. 性能指标
一个 Hippy 页面加载阶段,经历了哪些耗时阶段呢?
如何衡量一次性能优化是否有效?
1.2.1. performance API
为了帮助开发者更好地衡量和改进Hippy页面性能,手Q hippy 开发团队提供了performance API,以获取Hippy加载当前页面的性能打点。
performance API 提供了哪些性能打点?在手Q 平台调用 QQDebugModule#getPerformanceData,可以获得终端性能打点时间点,下面是各时间点含义:
我们可以通过以上的时间点,计算出Hippy页面终端性能指标,如下:
到这里,我们会发现 performance API并没有前端相关的性能打点,这部分需要业务按自身需要进行打点上报。
1.2.2. 前端性能打点
通过以上前端时间点,可以就可以计算出前端性能指标:
再配合终端性能打点,可以计算出首屏耗时、终端耗时、前端耗时等关键耗时,如下:
1.3. 性能现状
有了性能指标的量化标准,我们就可以通过统计现网真实用户数据来重新定义性能问题。
2. 性能分析
【首屏耗时瀑布图】
3. 性能优化
页面性能损耗无处不在,贯穿整个页面生命周期,任何一次代码提交都有可能引入性能问题,所以性能优化是一项持续性的工作。
性能优化应该遵循 “ 前期抓大放小、后期长尾优化 ” 原则,前期将精力聚焦在预计带来80%收益的那部分工作,后期再花时间解决另外棘手的部分。 本文主要分 终端、前端、后台 三个方向来着手首屏耗时优化。【阶段耗时优化方向脑图】
3.1. 终端优化方向
-
主进程打开大厅 -
预处理优化 -
调整预渲染预加载号码包
3.1.1. 主进程打开大厅
现状:tool进程打开 hippy 速度慢,影响初始化速度。直播业务整体包括首页和直播间是放在 tool 进程启动,如果放在主进程启动,打开速度会更快,但会导致手 Q 内存暴增,因为直播间有两个播放器+首页 hippy 预览。
预渲染、预加载方案,都无法优化掉初始化耗时,非常依赖主进程启动,那有没有可能在主进程启动?
优化方案: 方案一: 解决内存暴增问题,推动直播间业务放在主进程启动; 方案二: 将首页和直播间拆开,首页放到主进程启动,直播间还是放在 tool 进程;升级运行环境被否掉了,那还有没有其他更直接有效的优化方案?
换个思路,将页面加载提前一点。
3.1.2. 预处理优化
目前,手 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,切换其中任意一个入口都会触发预渲染,但不是所有用户都会真正进入大厅。所以,预渲染对后台服务增加负担。
简而言之,预渲染存在以下三点副作用:-
吃终端内存,不是百分百业务、用户都能获益 -
增加后台服务压力,开启后请求量增加数倍 -
对 tool 进程业务不友好
这些道理大家都懂,但还是没办法动摇“坚决不卷”的决心啊。
好吧,只能拿数据说话了:
辛辛苦苦接入的预渲染,结果预渲染打开大厅用户占比仅有 3.0%,有点儿绷不住了。
这不应该啊,是哪个环节出现了问题?
3.1.3. 调整预渲染号码包
现状:预渲染策略命中率低。上图显示,真实访问的用户中,非预渲染预加载的用户占比在 96.05%,从上一节结论知道,预渲染耗时远优于非预渲染,非预渲染预加载占比过高会导致大部分用户感知的加载会慢,这跟老板的预期不符,需要再卷一卷。
问题分析要回答上一节“哪个环节出现问题”,那先要知道,预渲染有哪些环节?
预渲染主要有以下几个环节:
① 业务侧数据同学按出包规则,每天计算 T+1 的号码包,落到 hdfs,供后台同步数据
② hippy 后台从 hdfs 同步号码包
③ 用户打开抽屉页或切换到动态 tab,触发预渲染流程
④ 终端按优先级处理预渲染配置
了解了预渲染流程,我们带着以下问题,逐个环节排查:
-
预渲染号码包策略是否最优? -
号码包出库是否及时? -
号码包更新是否及时? -
预渲染优先级判断是否准确? -
预渲染是否按预期执行?
3.1.3.1. 预渲染号码包策略是否最优?
暂且假设预渲染号码策略不是最优的,下面从出包规则、数据对比两方面来佐证我的猜想是否正确。
出包规则(全部或):-
最近 7 天点击过抽屉页直播入口 -
最近 7 天点击过动态页直播入口 -
内部用户白名单
规则有了,一眼看去好像也不能说明什么。
真实用户和预渲染号码包取交集,看下到底有多少重合用户:
可能心细的同学会发现,96.05% 这个数据并不是完全可靠,因为预渲染页面后还要等待用户真正点击了入口才能算真实访问的用户,这里存在一定的折损。
这么说也有道理,好在手 Q hippy 上报的数据已经非常全面,
我们可以拿客户端打开 hippy 的号码包跟预渲染号码包取交集,结果如下:
从上图可以看到,A∩B/B、A∩C/C 占比几乎一致,占比达到 50%,说明用户是否点击进入的折损,对预渲染号码包的交集影响不大。
真实访问用户在预渲染号码包的用户占比达到 50%,说明数据同学提供的号码包覆盖程度还是不错的。而真实访问用户中预渲染用户占比仅 3.08%,存在比较大的差距,造成这个差距的原因,还需要进一步确认。
3.1.3.2. 号码包出库是否及时?
数据同学反馈正常情况下每天 9 点前完成数据出库,一般凌晨 4 点左右完成,除非上游数据延迟。也就是说,号码包出库时效性在 4~27h,跟预期相比晚了 4 个小时,考虑到直播凌晨属于用户活跃低峰期,出包还算及时。
3.1.3.3. 号码包更新是否及时?
排查到这里,任然没有找问题的根本原因,都僵住了。这里有两个猜想:
-
hippy 后台用来判断是否预渲染的号码包和数据同学提供的号码包不是同一个 -
终端预渲染成功率低
顺着 hippy 数据上报这条路继续探索,出于好奇,拿到兄弟业务 A预渲染用户覆盖率数据对比直播大厅数据,不看不知道,相差接近 10 倍,好家伙~
预渲染用户覆盖率:-
直播:预渲染用户覆盖率,Android 为 8.82%,iOS 为 10.73%
预渲染用户覆盖率计算规则:
-
预渲染用户:后台下发的可以预渲染的用户,即预渲染号码包中当日登录的用户 -
打开 hippy 的用户:成功进入 hippy 页面的用户 -
有效预渲染用户:可以预渲染的用户并成功进入 hippy 页面的用户
-
直播:打开 hippy 用户中预渲染进入的占比,Android 为 1.35%,iOS 为 6.25%
计算规则:打开 hippy 用户中预渲染进入的占比 = 打开 hippy 用户中预渲染进入用户/打开 hippy 用户
相比【预渲染用户覆盖率】,这里算的打开 hippy 用户中预渲染进入用户,不包含预渲染实例被移除变为非预渲染进入的那部分用户
【问题】
相比业务 A,直播的预渲染用户覆盖率很低,其中打开 hippy 用户中预渲染进入的占比更低,和最初现状分析的3.08%的占比几乎一致。
看到这个差异简直又惊又喜,惊的是差距没想到这么巨大,喜的是问题看起来有了些眉目。
【分析问题】
直播&业务 A 号码包差异:
通过对比发现,直播预渲染号码包要和业务 A 对齐,需要做以下三件事情:
-
拉长出包覆盖的时长,从最近 7 天调整为最近 15 天或 30 天 -
调整号码包量级,提高到 a 万,对客户端有内存压力 -
业务后台提供接口用于动态更新号码包,对业务后台有请求压力
1、 2 比较容易实现,而 3 需要花费较多人力去实现,能带来多少收益呢?
考虑到7 日以后留存对整个号码包占比影响不大,出包规则两个业务都是取的活跃用户,活跃时长的应该不是影响的占比的大头。
用 a 万的号码包再取一次交集,A∩B/B占比大概在 60%,就算增长的 10% 都是有效的预渲染用户,加上原有的 10%,也才 20%,相比 50%,还有 30% 的差距。
那只剩下更新频率的变量了,继续沿着留存率对占比的影响思路,假设直播是每天更新一次号码包而且能及时更新到,跟业务 A实时动态更新号码包相比,虽然一个离线 T+1数据,一个是实时数据,预渲染用户覆盖率相差 10 倍,也比较离谱了,直播的留存率真的这么低吗?
因为没有两边找到留存数据,但从另外一个数据佐证,大概率更新号码包出了问题。
如果使用 hippy 上报的数据,计算当日打开 hippy 用户中 7 天内打开过 hippy 的用户占比,发现:
-
业务 A,iOS 占比为 66.2% - 直播,iOS 占比为 55.36%
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%都能受益,那还得继续着手非预渲染加载场景的优化。
3.2. 前端优化方向
-
预请求业务数据 -
优化业务包体积 -
按优先级加载业务模块 -
渲染优化 -
提高代码执行效率 -
提高缓存读写效率
3.2.1. 预请求业务数据
【QQ直播大厅加载数据时序图】
【现状】
-
非预渲染预加载场景,不算缓存,请求首屏数据接口耗时超过 500ms,如果将首屏数据提前请求,预计节省耗时 24%~35%。
-
请求 tab 配置串行阻塞请求首屏数据 -
banner 和直播间、关注列表是分开缓存,串行获取两次缓存导致获取缓存耗时增加
【优化策略】
-
预请求 tab 配置 -
预请求首屏数据 -
合并 banner 及直播间、关注列表缓存
3.2.1.1. 预请求 tab 配置
将 abtest 结果也放到 tab 配置中?
3.2.1.2. 预请求首屏数据
【预请求首屏数据】终端方案:
-
hippy终端给一个拉取业务数据的时机,供业务终端执行拉取业务后台首屏数据,前端通过 JSI 接口主动获取首屏数据。 -
如果访问前端页面时,终端还没获取到首屏数据,前端等待终端 resolve 首屏数据,业务终端收到后台数据后 resolve 通知业务前端。 -
如果终端预请求异常,前端兜底按原方式请求首屏数据。
终端方案牵扯协同方较多,本次优化中,采用了短期方案在前端预请求首屏数据的方式,快速验证优化效果。
3.2.1.3. 合并 banner 及直播间、关注列表缓存
读取列表缓存和banner 缓存是串行的,其中读取 banner 缓存使用了 100ms,这 100ms 是没必要的消耗。
将读取列表缓存和banner 缓存从串行改为并行,省去 100ms 耗时。
3.2.2. 优化业务包体积
包体积大小直接影响下载业务包耗时,减小包体大小能有效降低下载业务包耗时。
Android:
iOS:
【现状】
图片资源体积占用超过 jsbundle,在 hippy 页面中尽量避免使用大图片;
【优化策略】
-
图片优化 -
jsbundle 优化 -
vendor 包增加支持 react-dom
3.2.2.1 图片优化
-
压缩图片 -
上传大图片到 cdn,通过 cdn 地址访问
-
避免使用 base64 图片,增加体积且无法使用缓存
3.2.2.2 jsbundle 优化
-
剔除个人中心模块代码 -
抽离两个关注页面的共用代码,降低重复率
整体优化掉的体积不多,但是苍蝇腿也是肉,优化成本不大就顺便优化掉了。
3.2.2.3 vendor 包优化
vendor 包不包含 react-dom,导致业务包体积大 100kb 左右
【包体分析示意图】
a. vendor 无 react-dom
b. vendor 有 react-dom
但由于涉及到hippy 基础包增量问题,且使用 hippy-react 的业务并不多,多方考量后决定暂时不加入基础包,之后恰当的时机再推动手 Q hippy纳入基础包。
优化业务包体的整体效果:
3.2.3. 按优先级加载业务模块
【现状】
-
关注列表代码在首屏,增加【解析业务代码量】,同时增加【首屏更新节点数量】,导致【加载业务包】耗时增加
-
进入首页首就立刻确认是否可以预览,实际上要等返回首屏数据了才需要预览视频,阻塞加载首屏数据 -
加载非首屏数据可能会阻塞首屏渲染,虽然已经串行请求各 tab 数据,但是首屏数据返回后立刻请求下一个 tab 数据,和首屏数据渲染形成竞争关系
【优化策略】
-
懒加载关注列表 -
延迟加载卡片预览,等首屏渲染后,再进行卡片预览 -
延迟加载非首屏 tab 数据 -
懒加载非首屏 tab
【节点数量优化】初始化节点有 429 个,如果不在视窗且没有请求回来数据的,可以不生成节点,耗时间隔 150ms 左右
不渲染后,节点数量减少为 117 个,耗时间隔 46ms 左右
【优化后加载流程】
3.2.4. 渲染优化
本次优化前已处理过渲染优化问题,主要是解决长列表卡顿问题,不在本次优化的重点内容,优化手段类似 h5 页面,这里不再展开。
【优化策略】
-
避免过渡渲染,节点嵌套层级不超过 3 层 -
避免重复渲染 -
避免离屏渲染
3.2.5. 提高代码执行效率
【优化策略】
-
检查代码异常耗时 -
仅更新一次的数据不放在 redux 中
3.2.5.1 检查代码异常耗时
-
getBaseUrl 获取页面地址,在 tkd 中是有的,可以同步获取,预计节省耗时 244ms
-
【耗时逻辑】接口返回后,处理数据逻辑耗时相差 250ms 左右
-
【耗时逻辑】获取 tab 配置后到启动加载首屏列表数据相差 100ms 左右
-
【耗时逻辑】开始请求到真正发出请求中间耗时约 400ms 左右,非常大的 gap
-
【代码优化】HTTP_REQUEST 请求方式不需要从 cookie 获取登录态,所以可以优先从 globalinfo 获取更快,直接从 globalinfo 里面获取登录态信息,延时很小,有利于缩短请求前的消耗。
原逻辑:
优化后:
-
路由选择的逻辑,可以默认展示 live 组件,但得了解下是否会对产品交互体验有影响
3.2.5.2. 仅更新一次的数据不放在 redux 中
比如 tab 配置,访问一次仅更新一次
3.2.6. 提高缓存读写效率
【现状】
-
缓存读写效率低,数据量大的时候,甚至比直接请求接口更慢。 -
QQWebDataModule读写效率比较低,缓存数据比较分散,独立读写,重复执行相同读写逻辑,浪费资源。目标提高缓存数据读写效率,降低二次访问耗时。
【优化策略】
-
减小缓存数据体积 -
使用 AsyncStorage 替换 QQWebDataModule -
异步写同步读
3.2.6.1. 减小缓存数据体积
【问题】
-
【包体优化】首屏直播间列表缓存,仅存 6 个也有 30KB 多 -
【缓存性能】有时候,推荐列表缓存读取耗时比请求接口更慢,缓存:402ms,接口:389ms
【解决方法】
接口减包,详见3.3.2
3.2.6.2. 使用 AsyncStorage 替换 QQWebDataModule
【缓存方式】使用 web-data,读取缓存 193ms;使用 asyncstorage读取缓存79ms,快一倍多
web-data:
asyncstorage:
【现状】
现在首屏缓存数据大概 30KB,使用的终端接口QQWebDataModule,为了数据安全对数据有做加解密操作,会有一定耗时。
【解决方案】
最初沟通可以尝试使用AsyncStorage接口读取缓存,没有加解密操作,速度确实会快,相比QQWebDataModule快一倍。
但是考虑到:
-
AsyncStorage 所有业务共用且仅有 10M 存储空间,有限制 -
AsyncStorage 的数据没有加解密,可能存在安全风险,需业务评估数据安全等级再使用
基于这两点,方案一基本认定不能用于首屏数据缓存,跟 mark 沟通了一下,计划是联合 iOS 一起提供一套通用供业务预请求业务数据的方案,来代替缓存方案,详见 3.2.1.2。
3.2.6.3. 异步写同步读
【问题】
读取列表和banner 缓存是串行的,有 100ms 延时,详见3.2.1.3介绍
【解决方案】
由于一次读取缓存A+B相比串行两次读取缓存 A、B 耗时更少,采取【异步写同步读】方式,将串行获取 tab 配置和数据缓存,改为同时获取tab 和数据缓存,降低耗时(对二次访问生效)
3.3. 后台优化方向
-
预请求业务数据 -
接口减包 -
开启 gzip 压缩 -
支持 http3
3.3.1. 预请求业务数据
和前端、终端协同优化,方案详见 3.2.1
3.3.2. 接口减包
【现状】
推荐列表接口包含了流包、推荐上下文、进房串等大内容,每页 20 条的回包约 80KB,影响接口回包耗时,以及终端通信负载,通过减小接口回包体积降低端到端接口耗时、缓存读写耗时。
【优化策略】
-
分端分版本下发进房串、流包 -
减少首屏每页请求条数,目前为 20 条,减少为 10 条
3.3.2.1. 分端分版本下发进房串、流包
【现状】
-
进房串和直播预览会用到流包 -
进房时会根据客户端缓存的清晰度档位,【选择对应档位流地址】,替换进房串中的流地址,再用替换后进房串跳转直播间 -
预览直播会根据【前端灰度策略】,选择预览直播流地址 -
jump_url 同时包含了 rtmp 和 flv_url 参数,实际上一次访问不会同时使用两种格式的流地址 -
推荐相关的字段rcmd_context 在进房串 jump_url 也存在,没必要单独返回一个rcmd_context。
【减包方案】
方案一: 将进房串【选择对应档位流地址】、预览直播【前端灰度策略】这两步放到后台来做,返回给前端【最终进房串】和【预览直播流地址】。具体做法:
-
前端请求直播间列表时,传入【终端类型】、【版本号】、【终端记忆清晰度档位】 -
后台根据进房串【选择对应档位流地址】、预览直播【前端灰度策略】,获取【最终进房串】和【预览直播流地址】返回给前端
【预期改动点】
-
针对手 Q 新版本(8.9.5)只保留 flv 类型的流地址,剔除 rtmp 类型流地址,具体操作如下:a. 保留play_info.flv_url,剔除play_info.multi_rtmp_url、play_info.rtmp_url b. room_info.jump_url剔除 rtmp 参数 -
终端记忆清晰度档位保留原有逻辑,基于产品逻辑考虑,暂不改动 -
推荐相关,剔除 rcmd_context 字段,前端从 jump_url中获取,避免重复内容
减包后前端选择流程:
3.3.2.2. 减少首屏每页请求条数
减少首屏每页请求条数,目前为 20 条,减少为 10 条。
接口减包整体效果:
3.3.3. 开启 gzip 压缩
【现状】
大厅接口服务端没有开启 gzip 压缩,需要在 ias 开启 gzip 压缩。
【优化策略】
-
跟终端确认是否支持 gzip,跟 webview 加载 h5 页面不同,终端需要网络框架根据回包的头进行解压缩。 -
服务端,对整个大厅服务的路由开启gzip 压缩,影响范围太大,单独新增一个路由开启 gzip,按尾号灰度验证。
开启 gzip 压缩整体效果:实验数据(请求抓包回包大小):
实验数据(请求抓包耗时):
大盘数据(端到端耗时):
3.3.4. 支持 http3
Android 仅支持 http1.1,iOS 支持到 http2,双端暂时都不支持 http3,初步沟通对 http3 的预研效果都表示认可,且在手 Q 中也有业务尝试过http3,相信在不久的将来手 Q hippy 业务也能从 http3 中获益。
4. 优化效果
优化前后加载效果对比:
优化前后数据对比:
5. 总结
本文重点介绍了 QQ 直播大厅首屏耗时性能优化实践中遇到的一些问题及优化手段,覆盖终端、前端、后台的优化手段,关键优化手段总结为以下几点:
-
预渲染 -
预请求业务数据 -
接口减包 -
业务包体优化
本次优化中提到的支持 http3、预请求首屏数据终端方案还在方案落地阶段,后续会持续优化,此外,未来优化方向可以考虑 HippySSR 方案,相比预渲染来说,没有吃内存问题,真正做到 100% 用户获益。