前端性能优化--容器篇

语言: CN / TW / HK

前面我们讲了很多前端应用内部的性能优化,实际上除了前端自身,我们还可结合容纳 Web 页面本身的客户端一起做优化。

首先,本文中提到的容器,基本上都是指 Web 页面的宿主,比如浏览器、APP 客户端、小程序,它们提供了 WebView 环境来运行 Web 应用。

容器性能优化

由于 Web 应用本身只运行在 WebView 中,而 WebView 的能力又依赖于宿主容器,因此 Web 应用本身很多能力都比较局限。如果宿主容器能配合一起做一些优化,效果要远胜于我们自身做的很多优化效果。

从性能优化的角度来说,宿主容器主要能提供的能力包括:

  • 加速页面打开
  • 加速页面切换

加速页面打开

对前端项目来说,我们常常会对首屏打开做很多的优化,包括尽量减少首屏需要的代码、对首屏渲染的内容进行分片等等(参考 《前端性能优化–归纳篇》 )。

即使前端本身优化到极致,对于资源获取、请求数据等这些耗时占比较大的部分,还是存在的。但是如果容器能提供类似的能力,我们就可以将这部分的耗时做优化了,比如:

  • 提前下载并缓存 Web 相关资源,页面打开时直接获取缓存,比如 HTML/JavaScript/CSS
  • 提前获取和缓存页面渲染相关的请求资源,页面请求时直接返回,或是直接从缓存中获取
  • 提前启动 WebView 页面,并加载基础资源

资源准备

我们可以在客户端即将打开某个 WebView 页面之前,提前将该页面资源下载下来,由此加快 WebView 页面加载的速度。

由于资源请求本身也会消耗一定的资源,一般来说会在比较明确使用的场景下才会使用。也就是说用户很可能会点进去该 WebView 页面,基于这样的前提来做资源准备,比如列表页进入详情页,比如底部 TAB 进入的页面等等。

这些提前下载并临时缓存的资源,可以包括:

  • 页面加载资源,包括 HTML/CSS/JavaScript 等
  • 首屏页面内容的请求数据,比如分片数据的首片数据等

资源预下载要做的时候相对简单,需要注意的是下载后的资源的管理问题,在使用完毕或是不需要的情况下需要及时的清理,如果过多的缓存会占用用户机器的资源。

其实除了依赖客户端,前端本身也有相关的技术方案,比如说可以使用 PWA 提前请求和缓存页面需要的资源。

预加载

在需要的资源已经准备好的前提下,容器还可以提供预加载的能力,包括:

  • 容器预热:提前准备好 WebView 资源
  • 资源加载:将已下载的 Web 资源进行加载,比如基础的 HTML/CSS/JavaScript 等资源

举个例子,小程序中也有对资源预加载做处理。在小程序启动时,微信会为小程序展示一个固定的启动界面,界面内包含小程序的图标、名称和加载提示图标。此时,微信会在背后完成几项工作:下载小程序代码包、加载小程序代码包、初始化小程序首页。

小程序的启动过程也分了两个步骤:

  1. 页面预渲染。这是准备 WebView 页面的过程,由于小程序里是双线程的设计,因此渲染层和逻辑层都会分别进行初始化以及公共库的注入。逻辑层和渲染层是并行进行的,并不会相互依赖和阻塞。
  2. 小程序启动。当用户打开小程序后,小程序开始下载业务代码,同时会在本地创建基础 UI(内置组件)。准备完成后,就会开始注入业务代码,启动运行业务逻辑。

显然,小程序基础库和环境初始化相关的资源,都被提前内置在 APP 中了,并提前准备好相关的资源,使得用户打开小程序的时候,可以快速地加载页面。除此之外,小程序还提供了预加载的能力,业务方只需要配置提前拉取的资源,微信则可以在启动的过程中,提前将相关的资源拉取回来。

很多宿主预加载的方案也类似,比如对 WebView 页面做前置的资源下载和加载,当用户点击时尽快地给到用户体验。

加速页面切换

除了首次打开页面的加速,在页面切换时我们也可以做很多提速的事情。

容器预热

前面讲到,在打开小程序前,其实微信已经提前准备好了一个 WebView 层,由此减少小程序的加载耗时。

而当这个预备的 WebView 层被使用之后,一个新的 WebView 层同样地会被提前准备好。这样当开发者跳转到新页面时,就可以快速渲染页面了。这个过程也可以理解为容器的前置预热。

在这个例子中,小程序针对不同的页面使用了不同的 WebView 进行渲染,因此不管是首次打开,还是跳转/切换新页面,都会准备多一个 WebView 用来快速加载。

但多准备一个 WebView 本身也是对客户端的一种资源消耗,所以其实我们还可以考虑另外一种方案:容器切换。

容器切换

容器切换方案指当页面切换时复用同一个 WebView 资源,可以理解为前端单应用类似的方式在 APP 中做资源切换。

由于需要复用同一个 WebView,因此该方案对资源的管理要求较高,包括:

  • 对页面应用的生命周期管理完善,自顶向下实现初始化、更新和销毁的能力
  • 页面切换时,需要及时清理原有逻辑和资源,比如定时器、页面遗留的 UI 和事件监听等
  • 资源占用、内存泄露等问题,会随着 WebView 复用次数而积累

要达到不同页面和前端应用之间的资源复用,要求比直接准备一个新的 WebView 容器要高很多。即使是不同的页面,也需要有统一的生命周期管理,约定好页面的一些销毁行为,并能执行到每个模块和组件中。

但如果项目架构和设计做得好,效果要远胜于容器预热,因为在进行页面切换的时候,很多资源可以直接复用,比如:

  • 通用的框架库,比如使用了 Vue/React 等前端框架、Antd 等组件库,就可以免去获取和加载这些资源的耗时
  • 公共库的复用,项目中自行封装的一些工具库,也可以直接复用
  • 模块复用,通用的模块比如顶部栏、底部栏、工具栏、菜单栏等功能,可以在页面切换时选择性保留,直接省略这部分模块的加载和页面渲染

看到这里或许有些人会疑惑,如果是这样的话为什么不直接用单页面呢?要知道我们讨论的场景是客户端打开的场景,也就是说 WebView 页面的退出,大多数情况下是会先回到 APP 原生页面中。当用户进入到另外一个 WebView 页面时,才会重新打开 WebView,此时才考虑是用新预热的 WebView,还是直接复用刚才的 WebView。

总的来说,容器切换是一个设计要求高、副作用强、但优化效果好的方案。

客户端直出渲染

在有容器提供资源的基础上,我们还可以在 WebView 页面关闭前,对当前页面做截屏或是 HTML 保存处理。

在下一次用户进入到相同的页面中时,可以先使用上一次浏览的图片或是页面片段先预览,当页面加载完成后,再将预览部分移除。这种预加载(预览)的方案,由于是客户端提供的直出渲染能力,因此也被称为客户端直出渲染。

当然,相对于在页面关闭前保存,其实也可以直接实现直出渲染的能力,这样不管是否已经打开过某个页面,都可以通过容器预热时提前计算出直出渲染的内容,当页面打开时直接进行渲染。

这种方案有一个比较麻烦的地方:当缓存的页面内容发生变化时,需要及时更新直出渲染的内容。

因此,及时用户并不在页面内,也需要定期去获取最新的资源,并生成直出渲染的内容。当需要预渲染的页面多了,维护这些页面的实时性也需要消耗不少的资源,因此更适用于维护成本较低的页面。

结束语

其实,容器的作用不只是加速页面打开速度,由于结合了原生 APP 的能力,我们甚至可以给 WebView 提供完整的离线加载能力。比如在网络离线的情况下,通过提前将资源下载并缓存,用户依然可以正常访问 APP 里的页面。

当然,每一项技术方案都是有利有弊,容器提供了更优的能力,也需要消耗一定的资源,我们可以结合自己项目本身的情况来做取舍。