B站的离线包技术方案与大部分互联网公司实现的方案在底层逻辑上是一致的,都实现了基础的资源下发和拦截匹配机制。但在此基础上我们也有一些创新,例如页面快照技术、AB实验能力,同时也做了很多优化,包括用扫码调试降低调试成本、版本快速收敛、预约定时错峰发布等。目前我们的离线包技术已经接入183个项目,覆盖12个业务线,在公司内被广泛使用。
当我们用工具分析一个典型H5活动页(2024纪录片开放周)时,可以发现一个页面的速度瓶颈主要在这些方面:
HTML请求
通常按照HTML是否动态生成,将其分为SSR(服务器端渲染)和CSR(客户端渲染)页面。
请求页面主接口
应用虚拟DOM树生成
加载首屏资源
当页面实际的DOM构建完成后,浏览器才会知道需要加载哪些资源,并开始下载。这个过程大概需要数百毫秒。这一步通常会使用CDN加速、文件名带哈希+强缓存、优化图片和其他资源体积等方式,优化加载速度。
离线包的技术基础就在于Webview为调用层提供的请求劫持能力,匹配资源路径后可直接返回本地文件内容。
解决了请求劫持问题,接下来就可以实现透明的资源加速能力了。我们需要先完成资源的准备:
资源准备的过程比较简单,与构建工具无关,只要收集页面所需的资源文件,并且配置好URL到本地路径的映射关系就可以了。
最后打包的目录,大概包含以下内容
一个典型的代码产出目录如图所示:
config.json示例:
离线包的下发依赖Fawkes(客户端统一平台)的ModManager,使用与客户端统一的下发渠道,有利于统一调度,控制下载期间的CPU、带宽占用和存储空间占用。
ModManager是用于下发客户端资源的一个统一渠道,绝大部分需要动态下发的资源都会通过ModManager统一管理和分发,并具备了诸如增量包生成、资源错峰下发、热推送下发、版本和网络环境限制等诸多能力。复用这套基础能力省去了我们额外建立一套下发链路的成本,也能更容易地管控CDN流量以及客户端存储空间占用。
离线包平台有三种发布模式,分为常规发布、错峰发布和预约发布,这些发布模式的实现方案有一定的差异:
如果用户本地没有这个包的旧版本,那么会下载全量包;如果已经存在较近的旧版本,则会下载增量包。但即使有增量更新能力,在用户使用的高峰时段,常规发布仍会带来较大的带宽压力,所以高峰期发布需要额外审批。
如图所示,在常规发布后,资源包的下载会快速制造一个流量高峰,而后随着覆盖率的提升,缓慢下降,直到大部分用户完成更新。因此,我们除了常规发布外,还需要更智能的发布策略来降低成本。
错峰发布模式下,会设置离线包延后1-3天生效,在此期间,会借用流量低谷下载离线资源包。从而有效利用CDN的闲时流量,节约成本。
在离线包的实践中有这样一个问题:直播业务的大部分页面都接入了离线包,但某些页面常常需要在晚上高峰期上线发布,但在高峰期上线离线包又会带来很高昂的带宽费用。如何既保证可以及时发布,又可以兼顾离线包呢?我们设计了预约发布功能。
如图所示,当Webview加载一个URL时,会经历以下环节:
离线包由于是预先给客户端下发资源,所以不可避免地会遇到更新时效的问题。在更新时效上,我们之前在使用ModManager时会遇到这些问题:
考虑到前端离线包的主要目的是加速,不同于客户端包下载到资源才可以使用对应功能的限制,我们可以灵活利用线上兜底策略,来解决这几个问题。
版本接口的大概形式是这样的:
版本接口为了提供较高的时效性,每次切后台返回或者冷启动都会请求刷新,会有较高的QPS,我们在接口上做了充足的缓存策略,所有数据均是异步定时更新并完成计算,在请求到达时始终从内存缓存中返回数据,服务资源占用率比较可控
上图表示了一个设置了1小时旧版本过期的项目在占有率变化上的趋势。版本控制功能主要解决旧版本过期问题,可以使得旧版本在过期后立刻失效,而新版本不受影响。
离线包由于需要等待客户端将包下载完成才可以体验,因此在开发调试阶段会带来很大的痛苦。为此,我们仿照小程序的开发流程,设计了扫码调试功能,在开发、预览等场景上,均可以使用扫码调试功能快速预览页面在离线包下的效果。
扫码后,在预览调试页面,会调用JSAPI要求客户端下载指定的zip资源包,放置到特定的调试目录。后续在App存续期间再次打开对应的H5页面时,会优先使用调试目录内的资源,而其他机制保持一致,因此在大部分场景下可以完美还原线上离线包的使用体验。
另外在打开的调试页面上,还会注入离线包调试工具,帮助业务发现有哪些未命中离线的资源以及查看PerformanceTimingAPI数据,帮助优化离线包的文件组合,提升加速效果。
在很多需要优化首屏速度的场景,使用SSR都是一个可行的方式,在服务器端提前渲染好整个页面的HTML结构,这样浏览器拉取到HTML后就可以立刻开始渲染,用户就能更快地看到内容。
SSR能够加速的核心条件是提前渲染完的HTML,如果我们可以缓存上一次的页面渲染结果,是否也可以用于加速下一次用户进入时的首屏速度呢?显然是可以的,只不过需要先解决一些问题,例如
基于上述问题,离线包HTML快照能力做了一些针对性的方案。不过我们可以先看一下这个功能的运行流程:
如图所示,当用户请求一个URL时,会先判断是否命中离线包,如果命中,则检查是否存在HTML快照,如果存在快照,那么使用快照HTML,否则使用离线包内附带的HTML。当页面完成加载后,会在页面上调用一个JSB,用来存储当前页面的快照。
关于快照,还设置了以下限制:
除此之外,对于设置的HTML内容,还需要注意以下问题:
这样基本可以解决上述提到的问题。HTML快照的技术基础是在基于一个假设,即页面DOM结构会与数据一一对应,所以相同的数据输入,无论多少次渲染都应该输出相同的DOM结构。那么只要提前缓存DOM结构,就可以在下次访问时先展示旧页面,再无缝切换到新页面。
由于离线包会拦截入口HTML文件请求,在页面做了大改版需要页面级AB实验的场合,就无法在服务端来进行分流决策。为此,我们使用网址Rewrite能力使得同样一个URL在不同用户侧对应两份不同的离线包。
其大概流程如下:
上述方案可以实现页面的AB能力,但由于AB分流决策与页面打开的时机不同,所以无法上报进组数据,只能上报用户实际进入页面的数据,所以会影响部分数据的回收。
新旧版本的配置差异如上图所示。使用两个不同的URL,就可以分别对应两个不同的离线包,从而在不影响离线包本体设计的基础上实现AB能力。
离线包提供的几个核心能力,可以有效提升页面的首屏速度,但仍然因为技术实现和架构的原因,存在一些限制:
存在失效场景,需保留线上访问能力
在某些场景下,离线能力会暂时失效,从而会请求线上页面。接入离线包的业务需要自行确保线上URL可访问且功能正常,只能把离线包作为加速手段,而非最终部署目标。
失效的场景包括:App版本过低、尚未更新到可用版本的用户(新下载App用户或长期低活跃用户)、紧急下线或预约发布期间由业务触发的暂时禁用
增加上线复杂度
接入离线包后,需要在上线前完成离线包功能验证,且需要在线上页面上线完成后,再操作离线包发版。会增加一定的上线复杂度。
虽然离线包的资源预载和接口预载能力已经比较成熟,通常不会引入额外的问题,但仍然需要注意自测。
上线链路复杂的问题,通过在公司前端统一发布平台集成离线包发布流程来解决。
带来额外带宽成本
每一个上线发布的离线包,都会下发到绝大部分用户的手机中,而很多用户可能根本不是这个业务的目标用户。同时下发的过程也会带来额外的带宽成本,尤其是在高峰期上线时。
目前我们确实还没有精细化提升带宽使用率的能力,未来可能会看情况,采用人群包或其他方式,优化下发资源的精准度。
Webview本身的限制
综上,哪些业务适合接入离线包呢?离线包通常适合以下类型的业务:
当我们将越来越多资源提前缓存在用户手机里,免去网络IO这一最大的速度瓶颈,就会发现其实它跟客户端界面业务差距并不会很大。这个存在形态位于Native和H5之间,它牺牲了一些发版的便利性,但获得了更快的加载速度。虽然我们无法修改承载H5的Webview本身,也必须要承受Webview带来的性能问题,但我们也很好奇如果将一个页面优化到极致,是否真的可以让用户无法通过加载速度区分哪些页面是Native实现,哪些页面是H5实现呢?
后续我们将在一些页面上实验这样的速度优化策略:
全局性能表现:
Android:
IOS:
整体看,离线包对Android系统的页面加载提升服务比较明显,对IOS系统的提升相对幅度小一些,但也有可感知的速度提升。
从单独业务角度观察,从番剧片单页来看,离线包的速度提升效果如下:
在其他更复杂的页面上(更多图片、更多JS),离线包可以取得更好的性能提升效果。
综合来看,B站的离线包方案有以下亮点:
错峰发布:通过错峰发布,充分利用CDN闲时流量,可以用较低的成本下发资源
版本控制策略:通过版本控制策略,避免旧版本滞留,符合前端业务一次发布所有用户更新的常规心智,对于需要紧急更新或下线的场景比较重要
调试流程优化:扫码调试可以明显提高接入效率,保证线上稳定性是性能优化的第一前提。
AB实验能力:保证页面重大改版期间性能不下降,且能回收AB实验数据,兼顾用户体验和业务诉求
同时,有些业界常见的方案我们选择不去实现,例如:
Webview预载
接口预请求
同样也是收益不够高的问题。这里我们谈的是在Webview初始化时提前发起接口请求的方案。在一个充分优化的离线包上,理论上在html和js加载后,会很快开始接口请求,所以有概率在请求还没返回时,前端就开始从客户端取数据,处理这个中间状态有一定的成本,也会带来额外的调试心智负担。理论的性能优化表现应该在50-150ms左右,相对来说动力不足。
有一些技术是我们已经实现了,但其他人也都有类似实现的:
公共包技术:集合公共JS资源,避免多份打包,同时利用平台能力,自动完成公共资源更新
增量包下发:对每个版本的包,都会生成这个包和前N个版本差分计算的增量包,根据客户端当前版本下载增量包,降低CDN压力
热更新推送:可以对某些高优先级业务,开启热更新推送,助力快速更新
仅Wifi下载和版本限制:对下发条件做一定的限制,节约用户流量
简介:对于Feed流或者频繁打开同类页面的场景,可以保持Webview常驻后台,点击链接时将Webview拉到前台,并替换页面的URL参数,页面拉取接口数据并展现。
差异点:
总体看,B站的离线包方案为了方案通用性,牺牲了一些性能表现,好处是可以比较简单地应用在各业务场景上,带来相对普遍的价值。
简介:货拉拉方案与本文提到的方案在某些方面是有共通之处的,在基础加速层面上除了底层的技术实现思路采用了加载本地路径的方案,其他包括资源下发、多层降级机制等方面基本类似。
由于技术实现原理类似,因此在性能和通用性上,两者差异较小。我们从另外的方面比较:
另外我们观察到货拉拉方案里离线包的URL映射是动态下发的,B站的离线包URL映射则是打包到包里的,另外通过一个动态接口控制当前可用的版本。
这两个方案都是可行的,总体来看,有这些差异:
支付宝的离线包的定位并非是加速线上业务的能力,而是更接近离线H5应用的模式。在离线包未下载的时候,他会请求提前部署的降级资源,也就是使用的业务并不需要自行部署一套页面在线上,需要有一个域名和url,而是只需要使用它提供的方案就可以完成部署。
其运行机制,类似货拉拉方案,是用file协议加载,向H5页面提供一个虚拟域名供识别。
在API能力上,配备了一套JSAPI来满足业务的使用,所以看起来比较接近小程序的实现思路。
总体来讲,支付宝因为业务都是以一个个独立的H5App来承载的,H5App之间彼此关联和交互较少,所以方案会接近小程序,而后续支付宝也确实往前走了一步,实现了小程序的能力。
B站由于页面基本都是自有的,且Native跳H5、H5跳Native的场景很多,需要为用户提供一个整体性的体验,所以方案更偏向“加速”,而非独立H5App
UC浏览器在新闻feed流页面加载中采用了NSR(NativeSideRendering),首先在列表页中加载离线页面模板,通过Ajax预加载页面数据,通过Native渲染生成Html数据并且缓存在客户端。
NSR本质是分布式SSR,将服务器的渲染工作放在了一个个独立的移动设备中,实现了页面的预加载,同时又不会增加额外的服务器压力。
下面看方案对比:
VasSonic除了常规的资源预载之外,还做了以下事情:
VasSonic的方案整体思路和效果非常不错,特别是对于大部分web场景,通常我们的模板较少发生变化,大部分是数据部分变化,能够很好的通过局部刷新做到秒开效果。对于首次加载而言,通过并发请求和webview创建带来了不错的性能提升,还能无缝的支持离线包策略。
但是VasSonic定义了一套特殊的注释标记及拓展了头部,需要包括后台在内的前后端进行改造,对web侵入性非常强,接入的工作量及维护成本会非常大。
总的来讲,离线包加速方案的各个技术决策其实就是在平衡通用性和性能。
通用性越好,能做的事情越少,方案约需要遵循Web标准,当然性能的提升幅度也会小一些。
抛开通用性,如果针对业务特定定制更多的优化方案,那么优化效果一定是可以做的更好的,当然也就更专用,难以大规模铺开。
B站的离线包在决策时更多考虑了通用性,因此得以在各业务线都能比较低成本地接入,有比较广泛的使用群体。在此基础上,例如电商业务也针对他们的业务特点做了更专用化的定制,例如Webview常驻方案,得到了更好的效果。所以可以认为离线包作为一个通用方案,提升了性能的下限,它并不与专用方案冲突,可以再额外使用专用方案来提升性能的上限。
B站离线包在研发阶段基本遵循净室研发规则,除参考了业界方案的一些思路外,具体的方案设计和实现均为自研。