一粟

如何从客户端角度优化H5启动性能

2021.01.09

Hybird Web页面的性能问题一直都非常困扰开发者,本文讲述如何从客户端角度优化App内的H5页面启动性能。介绍一些常见的方法,供大家参考,希望可以有帮助。

从客户端角度的优化思路

可以先看下大致的启动流程,如下图,整个过程基本是串行的,HTML需要在Webview加载完加载,js需要根据HTML的内容加载,首屏数据也需要执行js之后再请求。但是如果我们可以利用客户端的能力,可以让一些耗时的过程并行起来,这样可以节省用户等待的时间。

image-20210109205142579

比如,说这里 webview的启动和html的下载都需要时间,但是像Webview启动的时候,其实并没有占用任何网络的资源,这时候网络是空闲的。那么,如果能够并行的去请求首屏需要的数据,那么我们就能够在js加载完成之后,立刻拿到首屏的数据,从而提高用户首屏的速度。

同时,对于客户端而言,它还可以做一些资源的缓存,比如html下载和js下载,这些资源如果我们下载过一次之后就把它存储在客户端。那么,等下次加载的时候,其实就不用重新下载。

有了缓存能力,那么我们其实也可以有预加载资源的能力。当还没有打开页面的时候,提前预加载一些资源。减少页面打开耗时。

image-20210109205647666

所以,总结来说,就是利用客户端的能力来实现并行、预加载、缓存等功能,减少H5启动流程中的耗时阶段。

并行接口请求

image-20210109205801908

如前面所说,H5加载是一个串行的过程。我们利用客户端能力去异步请求首屏数据,然后再把首屏数据交给H5,实现并行请求。

image-20210109205927349

整个流程可以由一个jsbridge完成getPreloadedData(callback).

image-20210109210230304

启动时,客户端并行执行。H5启动和首屏数据请求。这里首屏数据的接口信息,可以通过一些配置关联起来。比如页面url里或者一个单独的配置文件。

对客户端而言,H5初始化的时间和数据请求的时间先后不确定。所以,如果客户端先拿到数据,会把数据缓存在内存里,等待H5来调用jsbridge。如果H5先来询问数据,客户端会把H5的callback缓存下来,等数据获取之后再回调。

image-20210109210449436

在H5启动中,不但会调用上面的jsbridge向客户端要数据,同时也会自己发起首屏请求,所以这里需要有个竞速逻辑,使用最先返回的数据渲染。

WebView容器化

我们都知道如果页面有预加载渲染,用户打开时,页面速度就会很快展示,对于体验提升非常明显。但是,我们要慎用预加载,为什么?

  1. 预加载会占用更多内存,消耗更多流量。
  2. 预加载可能抢占客户端资源,导致用户当前的页面卡顿。
  3. 不可能所有页面都预加载,未预加载的页面还是很慢。

所以,我们在思考,是否有一种方式,只预加载公共的部分,业务的部分等打开页面的时候再去加载,这样对于页面的性能也会有提升,同时,也会减少预加载带来的副作用。

所以,我们提出了一个WebView容器化的方案。

image-20210109210929053

只预加载公共的部分,减少因为这部分导致的耗时,比如:webview初始化时间、框架库加载时间等。如果业务页面之间相似度高,还可以有更深层的定制。

image-20210109211112165

客户端会预创建缓存一个WebView容器,供使用。当加载一个页面后,会消耗一个容器,客户端会延迟2s再创建一个。

如下是,WebView容器化需要的能力,总体来说是,需要一个容器页面项目和业务项目。容器需要有一些通用的能力,核心是能够动态加载js然后执行渲染逻辑。业务项目,比普通的项目需要多一个编译入口,让页面能够编译成一个单独的js,并把组件暴露出来渲染。比如,webpack的umd导出方式:文档链接

image-20210109211711068

image-20210109212215673

加载时,客户端主要工作就是从预加载的webview池中取出一个可以用的webview,然后执行jsBridge,通知对应的js地址。这里可以有多种实现方式,比如传业务js链接过去,然后Web代码请求js内容。也可以客户端直接把js的内容提前下载下来,直接把内容给webview。可以更快完成加载。

上图的js伪代码是以动态加载js链接为例。

WebView容器化结合SSR

当页面变成SSR直出场景,会有什么不同吗?

首先,客户端加载的内容不同。普通页面加载的内容是静态资源,不包含数据。而SSR里不但有数据,还有预渲染的dom节点。

但是,在SSR场景下把用户的HTML缓存下来会有什么问题吗?比如,我们这样来做优化:每次把用户的HTML缓存下来,等用户下载打开页面的时候,直接加载上次缓存的HTML内容,加快首屏速度。

因为直出的HTML中包含了用户的首屏数据和dom,所以,直接加载上次的HTML内容,会包含上次页面的数据,大概率和当前的数据不一样。就是说,每次用户打开页面的时候,都还需要请求一次新的直出HTML,再触发页面整体重新渲染(类似页面reload),才能看到正确的数据。

这里有个优化的方式,就是把HTML拆分得细一点。分为数据和模板两部分。数据是每次变化的部分,而模板是不常变的,以此来减少页面的整体重新渲染。

模板和数据的拆分逻辑,可以参考: 开源库VasSonic

那么,分得细了之后,缓存命中情况也会复杂一些:

image-20210109213332203

接下来,通过不同的缓存命中情况流程图,来详细分析:

image-20210109213413313

首次加载本地没有缓存,需要全量从服务端拉取,加载后把下载的HTML分模块和数据缓存起来。

image-20210109213452530

完全命中缓存的情况,因为没有任何变化,加载缓存即可。这里比对的是两个hash,HTML的hash和模板的hash,就是先判断整体内容有没有变化,没有变化就304。

image-20210109213645534

仅数据变化的情况,就是HTML的hash发生了变化,然后对比模板的hash,发现是一样的,就只需要返回数据给客户端。客户端再把数据透传给H5,H5通过修改业务数据,实现页面更新。这种只js层面的数据变化,对页面的变化影响比较小,用户感知不明显。

image-20210109213832904

未命中缓存的情况,就是HTML的hash和模板的hash都发生了变化。这是服务器会返回完整的HTML内容,客户端通过重新加载HTML来实现页面的更新。

image-20210109213953335

整体流程图如上,主要增加了降级逻辑,在没有预加载webview的降级情况下,客户端拉取数据会和Webview并行。

总结

主要讲了三种优化手段。

  1. 首屏数据并行加载,WebView初始化资源下载 与 首屏数据请求并行,缩短用户等待时间。
  2. WebView容器化,预加载通用的WebView,减少资源占用,同时达到预加载的目的。
  3. WebView容器化+SSR,结合SSR场景和缓存逻辑,实现更快的首屏速度。

当然,还有其他的一些优化手段,比如 React Native、PWA等,也对首屏优化非常明显。欢迎有兴趣的同学一起交流学习。

本文首发于 一粟(http://zeyio.com),欢迎转载,但是必须保留本文的署名和链接。

本文永久链接:http://zeyio.com/202101/optimize-h5-by-native/


沧海一粟,欢迎来访。