查看原文
其他

前端加速利器【离线包】

大前端 哔哩哔哩技术
2024-09-08

摘要


H5页面给人的感觉通常是开发成本低、迭代速度快但使用体验不佳。其中最容易被用户感知的体验问题就是首屏速度,由于H5页面的所有资源都需要实时从网络上下载,提升这些资源的加载速度就可以明显提升首屏速度。我们通常将提前下发资源到App,并在打开H5页面时使用预载资源加速访问的技术称为“离线包”。

B站的离线包技术方案与大部分互联网公司实现的方案在底层逻辑上是一致的,都实现了基础的资源下发和拦截匹配机制。但在此基础上我们也有一些创新,例如页面快照技术、AB实验能力,同时也做了很多优化,包括用扫码调试降低调试成本、版本快速收敛、预约定时错峰发布等。目前我们的离线包技术已经接入183个项目,覆盖12个业务线,在公司内被广泛使用。


页面加载速度的瓶颈分析



当我们用工具分析一个典型H5活动页(2024纪录片开放周)时,可以发现一个页面的速度瓶颈主要在这些方面:

HTML请求

通常按照HTML是否动态生成,将其分为SSR(服务器端渲染)和CSR(客户端渲染)页面。

SSR受服务器负载、页面复杂度和网络拓扑影响,首字节时间相对较晚,但一旦收到HTML响应,浏览器引擎就会立刻开始解析和绘制,所以首屏时间相比CSR会更有优势。

CSR的优势在于可以很容易地使用部署在各地的CDN服务节点来加速,且可以配置一定时间的缓存,所以首字节时间更短,但所有页面结构都需要JS运行后才可以产出,首屏时间会晚很多。

通常CSR页面的响应时间大概在10ms-50ms,SSR页面通常50ms - 数百毫秒。SSR页面在等待服务器渲染上会耗费更多时间,根据之前一些案例的经验结果,SSR对中低端机型的首屏提升比离线包大,离线包在对高端机型的首屏提升比SSR大,其原因在于离线包下渲染页面属于单纯的CPU密集型任务,高端机型处理器较好,相对服务器端需要排队处理的SSR会有更快的响应。



主JS下载&编译

目前前端项目通常都会使用JS框架,叠加引入3-15个左右的第三方库,产出的JS Bundle一般会有数百KB-2M左右的大小,下载需要花费数百毫秒的时间。较大的JS文件也会让JS引擎的分析过程更加漫长,一般看CPU速度,需要十几毫秒到几十毫秒的时间。

在SSR项目中,主JS过大通常导致用户首次交互时间变晚,一些场景下,用户会发现页面一开始无法响应交互,在JS运行完成后才可以正常使用。

在CSR项目中,主JS过大会导致页面白屏时间较长,需要等待一段时间后才能出现页面内容。

请求页面主接口

单个接口通常消耗30ms-100ms的时间。但JS往往会等待接口返回后,才会开始生成实际的框架内虚拟DOM树,所以这个同步等待的时间越长,白屏时间也越长。SSR项目通常会在渲染服务器上调用这个接口,利用内网网络高带宽低延迟的特性,降低这块的耗时。CSR项目可以考虑提前加载时机(接口预取或请求前置)或缓存上一次的结果来尽量减少请求耗时。

应用虚拟DOM树生成

这一步通常都是Vue或者React这类框架在运行应用代码,生成虚拟DOM,进而将虚拟DOM挂载到实际的DOM树中。这个过程伴随大量的CPU密集计算,可能需要几十毫秒到百毫秒的时间才能完成。这一步由于视图结构受业务形态影响很大,通常难以优化,比较可行的方案是先加载首屏的、重要的组件,延后加载非首屏、不重要的组件。

加载首屏资源

当页面实际的DOM构建完成后,浏览器才会知道需要加载哪些资源,并开始下载。这个过程大概需要数百毫秒。这一步通常会使用CDN加速、文件名带哈希+强缓存、优化图片和其他资源体积等方式,优化加载速度。

综上,一般一个页面从开始加载到用户看到相对稳定的页面,通常需要1.2秒到2.5秒左右的时间。其中相当的一部分时间耗费在网络IO(下载资源和请求接口)上,剩余的主要就是JS编译运行的开销,以及浏览内核处理Layout和Render的时间。其中比较容易优化的主要是网络IO时间。而在JS代码运行和界面布局效率方面,在不同的业务场景下有较大的差异,需要根据业务场景量身定制。


离线包技术方案简介


离线包的技术基础就在于Webview为调用层提供的请求劫持能力,匹配资源路径后可直接返回本地文件内容。

Android系统可以直接使用shouldInterceptRequest API来实现,可以拦截https或者私有协议的请求。而IOS是基于WKURLSchemeHandler hook系统方法来实现https的拦截,但由于系统Bug在post请求中存在body丢失和上传图片可能导致进程崩溃的问题。对于这个问题,我们一方面要求H5页面使用JSAPI来发起POST请求,另一方面限制仅在命中离线包的情况下开启https拦截。在此限制下,因此这套方案基本可用。

解决了请求劫持问题,接下来就可以实现透明的资源加速能力了。我们需要先完成资源的准备:


代码资源的准备


资源准备的过程比较简单,与构建工具无关,只要收集页面所需的资源文件,并且配置好URL到本地路径的映射关系就可以了。

通常会有两种代码来源:

  1. 代码工程:由前端工程构建产出的文件。这类情况下,枚举每个产出的文件,去掉一些过大或不常用的文件,加上一些需要额外加载的线上资源,就可以打包发布了

  2. 线上抓取:其他无法控制源代码的线上业务页面,例如低代码平台产出的页面。这类情况下,可以使用模拟浏览器环境,抓取实际请求的资源列表,按照用户定义的规则去掉部分文件后,就可以打包发布了

最后打包的目录,大概包含以下内容

  • config.json用来描述客户端需要拦截的URL列表,拦截的一些基础信息和资源映射表

  • html 入口文件,客户端根据config.json的描述找到这个入口文件,并交给webview加载这个文件

  • 其他资源文件(css、js、图片、字体、svg等),这些资源会在页面运行时发起请求,由客户端查表拦截,并返回给webview使用

一个典型的代码产出目录如图所示:



config.json示例:



离线包的下发机制


离线包的下发依赖Fawkes(客户端统一平台)的ModManager,使用与客户端统一的下发渠道,有利于统一调度,控制下载期间的CPU、带宽占用和存储空间占用。

ModManager是用于下发客户端资源的一个统一渠道,绝大部分需要动态下发的资源都会通过ModManager统一管理和分发,并具备了诸如增量包生成、资源错峰下发、热推送下发、版本和网络环境限制等诸多能力。复用这套基础能力省去了我们额外建立一套下发链路的成本,也能更容易地管控CDN流量以及客户端存储空间占用。

离线包平台有三种发布模式,分为常规发布、错峰发布和预约发布,这些发布模式的实现方案有一定的差异:



常规发布


代码包将直接发布到线上,所有符合限制条件(版本、时间、灰度等策略)的冷启动App的用户都将按照资源包优先级逐步下载到新版本。

如果用户本地没有这个包的旧版本,那么会下载全量包;如果已经存在较近的旧版本,则会下载增量包。但即使有增量更新能力,在用户使用的高峰时段,常规发布仍会带来较大的带宽压力,所以高峰期发布需要额外审批。



如图所示,在常规发布后,资源包的下载会快速制造一个流量高峰,而后随着覆盖率的提升,缓慢下降,直到大部分用户完成更新。因此,我们除了常规发布外,还需要更智能的发布策略来降低成本。


错峰发布


错峰发布模式下,会设置离线包延后1-3天生效,在此期间,会借用流量低谷下载离线资源包。从而有效利用CDN的闲时流量,节约成本。

错峰发布适合第一次发布的包,可以利用闲时流量缓解第一次全量下载对用户的流量压力。后续版本再发布时则可根据上线时间要求灵活使用发布模式。


预约发布


在离线包的实践中有这样一个问题:直播业务的大部分页面都接入了离线包,但某些页面常常需要在晚上高峰期上线发布,但在高峰期上线离线包又会带来很高昂的带宽费用。如何既保证可以及时发布,又可以兼顾离线包呢?我们设计了预约发布功能。

预约发布允许设置后续24小时内的某个低峰时间点发布。设置后,当前线上使用的包版本可以设置即刻失效,用户降级请求线上页面访问新版本内容。而新版本会在指定时间点发布,在此后更新完成的用户,就可以正常使用加速能力。

预约发布适合需要在高峰时段上线新功能,且对短时间内性能下降不敏感的业务。即可以取得足够快的发布速度,又可以避免较高的带宽费用。缺点是会影响这个时间段内用户访问页面的速度,也可能对线上服务造成突发压力。


资源匹配机制



如图所示,当Webview加载一个URL时,会经历以下环节:

  1. 确认当前是否可以使用离线包。这包括确认全局开关、url开关和协议匹配情况。

  2. 如果当前页面URL命中了离线包,那么则确认本地包版本是否在接口下发的版本白名单中,如果存在,则进入到离线包加载流程,否则就正常走线上请求。

  3. 进入离线包加载流程后,会开启容器的https拦截能力,此后所有实例内的资源请求都会被拦截来确认是否存在对应的离线资源。同时开始加载离线的Html资源。

  4. html加载后,Webview会发起对JS和图片等资源的请求,此时请求拦截器识别到这些网址都在资源映射表中,就会使用本地文件系统的资源返回。对于不在列表的文件,则正常拉取线上资源后返回

  5. 此外还有一种特殊的资源类型,名叫公共资源。这类资源位于单独的公共包中,当页面命中离线包后,如果url符合公共资源的命名要求,就会尝试去加载本地资源。

  6. 对于所有本地资源加载失败的场景,都会回退到请求线上资源,至少保证功能可用。


版本控制和快速回滚策略


离线包由于是预先给客户端下发资源,所以不可避免地会遇到更新时效的问题。在更新时效上,我们之前在使用ModManager时会遇到这些问题:

  1. 更新不及时。比如在修复线上bug或者响应业务的临时改动时,希望用户能尽快更新到新版本,但要等到大部分用户更新到新版本需要很长的时间(一般发布完1小时能覆盖90%,2小时覆盖97%)

  2. 更新触发频率低。常规优先级的包,只会在冷启动时才会拉取,所以很多用户如果一直开着App,就会一直停留在老版本

  3. 版本残留。由于更新时间是在App冷启动时,而冷启动后可能有很多资源包需要更新,所以往往会在没有更新完成前就使用到了对应的H5页面,访问到旧版本内容。

  4. 下线后资源包被删除。当用户有紧急情况需要临时下线资源包时,如果一个包的所有版本都被下线,则会删除这mo d个包的资源。再次启用这个包时,就需要一次全量下载。

考虑到前端离线包的主要目的是加速,不同于客户端包下载到资源才可以使用对应功能的限制,我们可以灵活利用线上兜底策略,来解决这几个问题。

于是在ModManager机制的基础上,我们又额外引入了一个接口(x/offline/version接口),用来告知客户端当前可以使用的版本白名单。客户端会在每次冷启动和切后台返回时刷新接口数据(有较短时间的限频和缓存逻辑),从而可以实现以下能力:

  1. 发布新版本时设置旧版本过期时间。灵活控制旧版本的下线时间,如果在指定时间后,用户仍然没有更新到新版本,那么就会直接请求线上资源。一般设置2个小时过期,就能保证大部分用户仍然可以享受到离线包的提速,也能保证不存在版本残留问题和更新不及时的问题。

  2. 快速下线。在出现线上紧急Bug需要修复时,可以使用版本白名单,强制客户端走线上请求,但仍然保留本地离线资源包,从而方便后续发布新版本修复问题。

  3. 预约发布。预约在指定时间发布离线资源包,在此期间会强制用户走线上请求,从而兼顾上线速度和流量成本。

版本接口的大概形式是这样的:



这个接口采取版本白名单机制,在接口参数中确定了App环境信息后,会查询出这个环境这个App下可用的包版本列表。在发布新版本时,白名单里会同时出现2个版本,从而允许旧版本离线包仍然使用一段时间,在指定时间后,会只下发最新版本号,旧版用户在更新完成前会请求线上资源。

版本接口为了提供较高的时效性,每次切后台返回或者冷启动都会请求刷新,会有较高的QPS,我们在接口上做了充足的缓存策略,所有数据均是异步定时更新并完成计算,在请求到达时始终从内存缓存中返回数据,服务资源占用率比较可控



上图表示了一个设置了1小时旧版本过期的项目在占有率变化上的趋势。版本控制功能主要解决旧版本过期问题,可以使得旧版本在过期后立刻失效,而新版本不受影响。


调试流程优化


离线包由于需要等待客户端将包下载完成才可以体验,因此在开发调试阶段会带来很大的痛苦。为此,我们仿照小程序的开发流程,设计了扫码调试功能,在开发、预览等场景上,均可以使用扫码调试功能快速预览页面在离线包下的效果。



扫码后,在预览调试页面,会调用JSAPI要求客户端下载指定的zip资源包,放置到特定的调试目录。后续在App存续期间再次打开对应的H5页面时,会优先使用调试目录内的资源,而其他机制保持一致,因此在大部分场景下可以完美还原线上离线包的使用体验。

另外在打开的调试页面上,还会注入离线包调试工具,帮助业务发现有哪些未命中离线的资源以及查看Performance Timing API数据,帮助优化离线包的文件组合,提升加速效果。


HTML快照


在很多需要优化首屏速度的场景,使用SSR都是一个可行的方式,在服务器端提前渲染好整个页面的HTML结构,这样浏览器拉取到HTML后就可以立刻开始渲染,用户就能更快地看到内容。

SSR能够加速的核心条件是提前渲染完的HTML,如果我们可以缓存上一次的页面渲染结果,是否也可以用于加速下一次用户进入时的首屏速度呢?显然是可以的,只不过需要先解决一些问题,例如

  1. DOM一致性问题。比如JS运行时会修改DOM实现动态样式、rem布局等能力,这些修改的副作用需要考虑哪些要留下,哪些要舍弃的问题。

  2. 反复生成快照带来的副作用累积问题。由于每次都会使用当前页面的DOM作为快照,但直接使用快照页面再次加载时,JS逻辑会再次执行一边,导致出现多套副作用累积,导致不可预期的bug。

  3. 缓存问题。如何保证代码上线后,用户不会仍然因为快照访问到旧版本。如何保证不同参数进入页面时,页面数据的隔离性。

  4. 数据时效性问题。一些时效敏感数据,如何针对性地从缓存里去除,避免旧数据对用户的误导

  5. 前后的衔接问题。不像SSR有成熟的方案和框架支持,可以很顺畅地从快照的DOM结构中注入JS交互逻辑。快照页面通常只能重新生成一套DOM,完整替换原来的DOM,所以衔接的时机就很重要

基于上述问题,离线包HTML快照能力做了一些针对性的方案。不过我们可以先看一下这个功能的运行流程:



如图所示,当用户请求一个URL时,会先判断是否命中离线包,如果命中,则检查是否存在HTML快照,如果存在快照,那么使用快照HTML,否则使用离线包内附带的HTML。当页面完成加载后,会在页面上调用一个JSB,用来存储当前页面的快照。

关于快照,还设置了以下限制:

  1. 每个快照使用1次后即删除,必须在加载快照的页面上,调用JSB重新存储当前页面的快照,才能让下一次页面加载时使用。删除的条件为:当前打开的容器,存续时间大于3秒,这个是为了防止用户误操作导致快照被删的问题。

  2. 当离线包更新版本后,将不能使用旧版本离线包下创建的快照,即你需要将离线包版本当作一个key。这是为了保证代码更新后,页面可以正常更新。

  3. 快照需要有缓存key策略,允许前端根据url query、cookie、登录状态等参数区分页面,防止页面数据错乱。

  4. 快照需要设置最大存储时间和存储队列长度。最大存储时间可以由jsb设置,但最长不超过5天,存储队列长度,每个url下(去query的)最大存储20个HTML快照。整体快照大小不超过10MB

除此之外,对于设置的HTML内容,还需要注意以下问题:

  1. 使用原始空壳HTML作为基础,注入当前页面#app节点内的结构。如果使用rem布局则追加设置rem的fontSize,整合后的HTML才用于设置快照

  2. 应用代码在识别到当前使用快照时,先完整生成dom结构,再替换当前#app的内容

  3. 每个应用自行处理好时效敏感数据节点内的数据

这样基本可以解决上述提到的问题。HTML快照的技术基础是在基于一个假设,即页面DOM结构会与数据一一对应,所以相同的数据输入,无论多少次渲染都应该输出相同的DOM结构。那么只要提前缓存DOM结构,就可以在下次访问时先展示旧页面,再无缝切换到新页面。

这个假设虽然大部分情况下虽然成立,但却仍旧缺乏业务实践,具体在实践中会遇到哪些问题,需要在生产应用后才能有相对靠谱的最佳实践。这方面的实践会在今年展开,欢迎持续关注与合作。


AB实验


由于离线包会拦截入口HTML文件请求,在页面做了大改版需要页面级AB实验的场合,就无法在服务端来进行分流决策。为此,我们使用网址Rewrite能力使得同样一个URL在不同用户侧对应两份不同的离线包。

其大概流程如下:


  1. 在每次热启动请求离线包控制接口时,接口计算该用户对应的AB实验分组

  2. 根据对应的分组,下发url重写规则,将地址重写到对应版本离线包的URL

  3. 客户端在打开页面时,会根据映射表,将URL重映射到新的URL上,并且按照参数要求添加分组结果参数

  4. 最后按照修改后的url打开页面,页面上报URL参数里的分组结果

上述方案可以实现页面的AB能力,但由于AB分流决策与页面打开的时机不同,所以无法上报进组数据,只能上报用户实际进入页面的数据,所以会影响部分数据的回收。



新旧版本的配置差异如上图所示。使用两个不同的URL,就可以分别对应两个不同的离线包,从而在不影响离线包本体设计的基础上实现AB能力。


适用场景与局限


离线包提供的几个核心能力,可以有效提升页面的首屏速度,但仍然因为技术实现和架构的原因,存在一些限制:

存在失效场景,需保留线上访问能力

在某些场景下,离线能力会暂时失效,从而会请求线上页面。接入离线包的业务需要自行确保线上URL可访问且功能正常,只能把离线包作为加速手段,而非最终部署目标。

失效的场景包括:App版本过低、尚未更新到可用版本的用户(新下载App用户或长期低活跃用户)、紧急下线或预约发布期间由业务触发的暂时禁用

增加上线复杂度

接入离线包后,需要在上线前完成离线包功能验证,且需要在线上页面上线完成后,再操作离线包发版。会增加一定的上线复杂度。

虽然离线包的资源预载和接口预载能力已经比较成熟,通常不会引入额外的问题,但仍然需要注意自测。

上线链路复杂的问题,通过在公司前端统一发布平台集成离线包发布流程来解决。

带来额外带宽成本

每一个上线发布的离线包,都会下发到绝大部分用户的手机中,而很多用户可能根本不是这个业务的目标用户。同时下发的过程也会带来额外的带宽成本,尤其是在高峰期上线时。

目前我们确实还没有精细化提升带宽使用率的能力,未来可能会看情况,采用人群包或其他方式,优化下发资源的精准度。

Webview本身的限制

目前由于IOS系统在使用https拦截能力时的一些Bug,需要格外注意这两个问题:

  1. 不要在离线包的页面内直接发起post请求,包括post数据或者图片。如果有需要的话,可以使用基于JSAPI请求的公司内NPM包

  2. IOS离线包下,使用history.push,左滑时会退出整个webview容器,而非返回上一页。目前技术无解,只能使用ability.openScheme打开新的容器来替代

综上,哪些业务适合接入离线包呢?离线包通常适合以下类型的业务:

  1. 有一定的用户体量,例如日PV大于5万的业务(不是硬性要求)。这一点主要是出于资源利用率考虑。

  2. 短时间内有大量用户访问的活动页,或者有常驻入口的业务流程页面

  3. 营收相关或者速度敏感的业务,提升用户体验可能对营收有一定的帮助

  4. 相对稳定,改动不频繁的业务


离线化的终点在哪里?


当我们将越来越多资源提前缓存在用户手机里,免去网络IO这一最大的速度瓶颈,就会发现其实它跟客户端界面业务差距并不会很大。这个存在形态位于Native和H5之间,它牺牲了一些发版的便利性,但获得了更快的加载速度。虽然我们无法修改承载H5的Webview本身,也必须要承受Webview带来的性能问题,但我们也很好奇如果将一个页面优化到极致,是否真的可以让用户无法通过加载速度区分哪些页面是Native实现,哪些页面是H5实现呢?

后续我们将在一些页面上实验这样的速度优化策略:

  1. 所有代码自身的资源离线化,这个目前就能实现

  2. 代码通过script标签引入的三方库离线化,通过离线包平台自动化管理三方库的公共包,自动更新自动发布。保证页面本身的代码资源不再需要通过网络加载

  3. 接口数据缓存和stale-while-revalidate策略组合。页面预先使用上一次缓存的接口数据,等接口预载数据Ready后,替换成最新数据。(类似客户端页面进入后,先看到旧数据和loading标志,随后刷新为新数据)

  4. HTML快照策略。充分利用HTML快照,在JS还未执行时就能看到首屏页面

  5. 其他常规优化手段。包括优化第三方库引用、优化代码执行流、观察代码性能瓶颈并解决等。

当这个实验做完后,会再写一篇文章,总结这样综合实践方案下的经验,为其他业务的优化提供参考。我们的目标是树立起H5性能的新标杆,让更多的业务可以使用H5来承载,释放业务迭代的潜力!


性能表现


全局性能表现:

Android:



IOS:



整体看,离线包对Android系统的页面加载提升服务比较明显,对IOS系统的提升相对幅度小一些,但也有可感知的速度提升。

从单独业务角度观察,从番剧片单页来看,离线包的速度提升效果如下:



由于PV数较小的性能数据不可置信,因此看可置信数据而言,在onload事件的平均时间上,非离线包状态的加载时间约为2700ms左右,简单接入离线包后,加载时间约为2300ms,提升400ms。

在经过对资源的详细分析,将所有线上加载的js文件都打入离线包后,加载时间下降到2016ms,可以再提升300ms。总体而言,离线包可以为简单H5页面能提供700ms的加载性能提升(onLoad)。且在用户观感上,页面基本可以在切换动画完成前展现。



配合页面的FCP数据,可以看到在优化后,页面的首屏渲染时间可以从1550下降到1230,能提升300ms,且基本接近“秒开”。

在其他更复杂的页面上(更多图片、更多JS),离线包可以取得更好的性能提升效果。


业界方案对比


B站方案与业界方案的差异点


综合来看,B站的离线包方案有以下亮点:

错峰发布:通过错峰发布,充分利用CDN闲时流量,可以用较低的成本下发资源

版本控制策略:通过版本控制策略,避免旧版本滞留,符合前端业务一次发布所有用户更新的常规心智,对于需要紧急更新或下线的场景比较重要

调试流程优化:扫码调试可以明显提高接入效率,保证线上稳定性是性能优化的第一前提。

HTML快照:利用上一次渲染结果优化下次进入的首屏速度,虽然有较多的调试成本,但也有一试的价值。这个方案在某些社区文章里有被简单提到过。

AB实验能力:保证页面重大改版期间性能不下降,且能回收AB实验数据,兼顾用户体验和业务诉求


同时,有些业界常见的方案我们选择不去实现,例如:

Webview预载

提前初始化一个Webview挂在后台,等需要的时候直接使用。这个方案在前期因为公司内各业务容器不统一而无法落地,现在统一后发现预载收益有限,B站App内的容器二次加载耗时基本在130ms以下,为了这点时间而长期占用较多的内存,在一个视频观看体验优先的App上并不明智。而如果为提升首次初始化耗时而在App启动时预载Webview,又会影响用户的启动速度,因此作罢。

接口预请求

同样也是收益不够高的问题。这里我们谈的是在Webview初始化时提前发起接口请求的方案。在一个充分优化的离线包上,理论上在html和js加载后,会很快开始接口请求,所以有概率在请求还没返回时,前端就开始从客户端取数据,处理这个中间状态有一定的成本,也会带来额外的调试心智负担。理论的性能优化表现应该在50-150ms左右,相对来说动力不足。

对于像Feed流预加载后续文章接口的实现方案,由于通用性不足,不作为全公司级离线包需要考虑的方案,可以在各业务层自行实现。


有一些技术是我们已经实现了,但其他人也都有类似实现的:

公共包技术:集合公共JS资源,避免多份打包,同时利用平台能力,自动完成公共资源更新

增量包下发:对每个版本的包,都会生成这个包和前N个版本差分计算的增量包,根据客户端当前版本下载增量包,降低CDN压力

热更新推送:可以对某些高优先级业务,开启热更新推送,助力快速更新

仅Wifi下载和版本限制:对下发条件做一定的限制,节约用户流量


 Webview常驻方案


简介:对于Feed流或者频繁打开同类页面的场景,可以保持Webview常驻后台,点击链接时将Webview拉到前台,并替换页面的URL参数,页面拉取接口数据并展现。

差异点:



Webview常驻方案

B站离线包方案

性能非常快。由于省去了容器初始化和页面JS初始化的诸多流程,这个方案的性能优于离线包,配合feed流数据接口预取,可以让用户几乎无法感知到加载过程。性能比较常规,在不使用HTML快照这样的特殊方案时,性能与业界方案是类似的。
方案通用性

较差。适用场景相对局限,比较适合频繁往返于列表页到详情页的这类场景,例如新闻阅读类应用、商城类应用等。

通用性好,同样的方案可以在B站绝大部分H5业务上使用。漫画App、云视听小电视、直播姬等App的也已低成本接入
代码侵入性

强。需要代码做针对性适配,以及客户端配合才能完成一个特定场景下的落地

侵入性低,除了需要注意Post请求问题和History API在IOS上无法记录历史记录外,其他代码与线上业务完全相同


总体看,B站的离线包方案为了方案通用性,牺牲了一些性能表现,好处是可以比较简单地应用在各业务场景上,带来相对普遍的价值。


货拉拉的离线方案


简介:货拉拉方案与本文提到的方案在某些方面是有共通之处的,在基础加速层面上除了底层的技术实现思路采用了加载本地路径的方案,其他包括资源下发、多层降级机制等方面基本类似。

差异点:

由于技术实现原理类似,因此在性能和通用性上,两者差异较小。我们从另外的方面比较:



货拉拉方案

B站离线包方案

容器实现位置基于通用容器上的一个独立增强容器,各业务容器基于此拓展直接放入基础容器内,开箱即用
兼容性存在跨域问题基于https拦截,与线上几乎一致
代码侵入性

有少量侵入性,需要对本地加载的路径做一些适配

侵入性低,除了需要注意Post请求问题和History API在IOS上无法记录历史记录外,其他代码与线上业务完全相同


另外我们观察到货拉拉方案里离线包的URL映射是动态下发的,B站的离线包URL映射则是打包到包里的,另外通过一个动态接口控制当前可用的版本。

这两个方案都是可行的,总体来看,有这些差异:



动态下发URL映射

打包URL映射

优势方便灵活,可以很简单地修改映射关系有利于这个包的逻辑独立性,这也是扫码调试功能好实现的一个因素
缺点可追溯性差,配置不断在变化,配置和包资源无法一一匹配时,可能导致过去历史版本可能因为配置变更而不能再看灵活度差,修改URL映射需要发一个新的包


支付宝的Nebula方案离线包能力


支付宝的离线包的定位并非是加速线上业务的能力,而是更接近离线H5应用的模式。在离线包未下载的时候,他会请求提前部署的降级资源,也就是使用的业务并不需要自行部署一套页面在线上,需要有一个域名和url,而是只需要使用它提供的方案就可以完成部署。

其运行机制,类似货拉拉方案,是用file协议加载,向H5页面提供一个虚拟域名供识别。

在API能力上,配备了一套JSAPI来满足业务的使用,所以看起来比较接近小程序的实现思路。

总体来讲,支付宝因为业务都是以一个个独立的H5App来承载的,H5 App之间彼此关联和交互较少,所以方案会接近小程序,而后续支付宝也确实往前走了一步,实现了小程序的能力。

B站由于页面基本都是自有的,且Native跳H5、H5跳Native的场景很多,需要为用户提供一个整体性的体验,所以方案更偏向“加速”,而非独立H5 App


UC的NSR方案


UC浏览器在新闻feed流页面加载中采用了NSR(Native Side Rendering),首先在列表页中加载离线页面模板,通过Ajax预加载页面数据,通过Native渲染生成Html数据并且缓存在客户端。

NSR本质是分布式SSR,将服务器的渲染工作放在了一个个独立的移动设备中,实现了页面的预加载,同时又不会增加额外的服务器压力。

下面看方案对比:



NSR方案

B站离线包方案

性能更快,通过客户端渲染,可以不依赖Webview生命周期就可以提前渲染出所需的HTML一般,仍然需要等待容器初始化+html加载+JS编译执行+接口请求+框架渲染
方案通用性相对一般,有一定的定制性比较通用,整体都是在Web标准上做的一些优化
代码侵入性较强侵入性。一个是代码编写接近常规带SSR的页面,另一方面NSR渲染结果和Webview内展现的过程是有一定逻辑的。

较低侵入性。使用常规方案几乎不需要改代码。

如需更快的性能,代码快照会类似这套方案,只不过复用的是上一次渲染的结果。当然使用代码快照也意味着带来更强的侵入性。

兼容性需要额外考虑在客户端JS环境运行的兼容性较好,与线上页面类似


腾讯的VasSonic方案


VasSonic除了常规的资源预载之外,还做了以下事情:

  • webview 初始化和通过客户端代理资源请求并行

  • 流式拦截请求,边加载边渲染

  • 实现了动态缓存和增量更新

VasSonic 的方案整体思路和效果非常不错,特别是对于大部分 web 场景,通常我们的模板较少发生变化,大部分是数据部分变化,能够很好的通过局部刷新做到秒开效果。对于首次加载而言,通过并发请求和 webview 创建带来了不错的性能提升,还能无缝的支持离线包策略。

但是 VasSonic 定义了一套特殊的注释标记及拓展了头部,需要包括后台在内的前后端进行改造,对 web 侵入性非常强,接入的工作量及维护成本会非常大。


方案总结


总的来讲,离线包加速方案的各个技术决策其实就是在平衡通用性和性能。

通用性越好,能做的事情越少,方案约需要遵循Web标准,当然性能的提升幅度也会小一些。

抛开通用性,如果针对业务特定定制更多的优化方案,那么优化效果一定是可以做的更好的,当然也就更专用,难以大规模铺开。


B站的离线包在决策时更多考虑了通用性,因此得以在各业务线都能比较低成本地接入,有比较广泛的使用群体。在此基础上,例如电商业务也针对他们的业务特点做了更专用化的定制,例如Webview常驻方案,得到了更好的效果。所以可以认为离线包作为一个通用方案,提升了性能的下限,它并不与专用方案冲突,可以再额外使用专用方案来提升性能的上限。


参考文章


B站离线包在研发阶段基本遵循净室研发规则,除参考了业界方案的一些思路外,具体的方案设计和实现均为自研。

在本篇文章的方案对比方面,主要参考了以下文章:

  1. 【移动端h5秒开方案总结】https://blog.towavephone.com/mobile-h5-startup-way/

  2. 【货拉拉H5离线包原理与实践】https://juejin.cn/post/7103348563479887885

  3. 【CSR、SSR、NSR、ESR傻傻分不清楚,一文帮你理清前端渲染方案!】https://juejin.cn/post/6844904178519834638

  4. 【离线化集成方案-安全设计与优化】https://research.szltech.com/?p=1935

  5. 【WebView性能、体验分析与优化】https://tech.meituan.com/2017/06/09/webviewperf.html

  6. 【蚂蚁金服金融科技产品手册】https://docs-aliyun.cn-hangzhou.oss.aliyun-inc.com/assets/attach/87479/AntCloud_zh/1578025862524/10%20H5%20%E5%AE%B9%E5%99%A8%E5%92%8C%E7%A6%BB%E7%BA%BF%E5%8C%85%2020200101.pdf


-End-

作者丨海洛


开发者问答

你觉得和客户端配合还能做哪些事情可以进一步提高页面性能呢?欢迎在留言区告诉我们。转发并留言,小编将选取1则最有价值的评论,送出bilibili Goods Model Lv.1 基础短袖T恤1件(随机色)(见下图)。5月24日中午12点开奖。如果喜欢本期内容的话,欢迎点个“在看”吧!



往期精彩指路


通用工程大前端业务线

大数据AI多媒体


继续滑动看下一个
哔哩哔哩技术
向上滑动看下一个

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存