PWA+ 老板再也不用担心我的性能优化KPI (方案篇)

性能优化

我们从一个小故事谈起:

老板:阿特,下半年先定一个小目标,实现页面秒开

阿特想起了学过的各种性能优化手段,心里暗喜,这里终于不怕完不成目标啦。

阿特回来后就在项目上加上了预加载,懒加载,雪碧图等功能,忙活了好几天。

几天后

项目终于上线了,看到测速数据,阿特很疑惑:服务端渲染都加上了,为什么我的页面还是没有秒开呀?

要弄懂这个问题,让我们先看看页面打开时都经历了什么

假设有一个移动端 app 的内嵌页面,打开这样一个页面,需要经过

打开webview -> http 请求主文档 -> 解析并渲染

(http请求到页面渲染的部分其实很复杂,这里就不展开了)

webview 的打开时间取决于客户端,从前端的角度来看,考虑如何尽量减少 webview 打开到页面展示的时间。简单来说,需要在 http 请求前/请求同时 展示页面。

目前已经有一些成熟的方案,例如手 Q 里的 sonic,webso,以及部分 app 支持的离线包等

然而它们并没有这么完美

业务痛点

目前的方案具有以下几个缺陷:

  1. 场景覆盖有限,现有的解决方案都深度依赖客户端能力,而在第三方app内,例如微信,浏览器等场景下,没有很好的缓存方案。导致分享页等多场景页面的体验欠佳。

  2. 目前使用的方案具有并发限制,并发资源数越多,加载时间也会成线性增加。

  3. 使用现有的缓存解决方案,本地资源的更新取决于客户端拉取间隔(通常是10-15分钟),这就导致了发布的新资源需要一定的时间覆盖,线上可能出现多版本同时生效的问题。

针对这些痛点,我们自然想到了利用”纯前端”的解决方式 —— PWA 作为优化方案。

还不了解 PWA 的同学可以先戳这里

然而, PWA 真的能解决这些问题吗?

PWA 能解决这些问题吗?

对于上面的问题,我们来逐一攻破:

  • 场景覆盖:PWA 能覆盖绝大多数的安卓场景,从 ios 11.3 开始,safari 也支持了 PWA。而在 PC 端,除 IE 外的绝大部分浏览器都支持了 PWA。

  • 并发限制:service worker 中的请求并发和浏览器并发机制一致,同时 service worker 运行在独立于主线程之外的其他线程,不会阻塞主线程的执行。

  • 更新机制:如果默认缓存项目中的资源,每次修改缓存的资源时都需要进行发布。我们想到,可以将需要缓存的文件列表作为配置下发,实现实时更新。

  • 储存空间,cache 的空间是按照域名划分的,不同浏览器的策略不同。Chrome 的 cache 上限为可用空间的 6%,Safari 的上限为 < 50MB,其中 Chrome 会在耗尽空间后利用 LRU 策略逐出。移动端的场景下还和机器剩余空间以及 app 分配的空间有关。

PWA-PLUS 架构

基于以上的构想,我们设计出这样一套架构:

PWA-PLUS 架构

  • 构建阶段:在构建阶段,在 webpack 构建时引入我们封装的插件 @tencent/pwa-plus-plugin 。插件会根据设置的规则,提取符合规则的构建产物生成 pwa-manifest.json 并同步到配置平台。
  • 线上环境:用户首次访问页面时,页面注册的 service worker 会发送请求到配置平台,获取该页面的 PWA 开关状态,如果 PWA 状态是开启的,再拉取该项目的缓存资源的列表,请求资源并缓存到 cache 中。用户二次访问页面时, service worker 会代理所有请求,优先使用本地的缓存资源。

缓存 HTML 文档

PWA方案中,除了常规的静态资源缓存,我们更希望能够 缓存 html 主文档,从而实现 二次用户的秒开

那么问题就来了
何时应该更新缓存的 html 文档呢?
如何做到无痛更新?

我们针对直出(服务端渲染)和非直出(浏览器端渲染)两种场景分别定制了更新方案:

SSR 服务端渲染

直出场景下的缓存更新方案

由于直出的页面,html 文档中已经包含了数据,我们只要处理好数据的更新即可:

  1. 用户第一次访问页面时,service worker 将直出的 HTML 文档(带dom节点和数据)缓存到 cache 中。
  2. 用户第二次访问页面时,service worker 拦截页面的 HTML 请求,并行做两件事:
  3. 在 cache 中查找,并返回缓存的 HTML 文档给用户
    进行网络请求,获取最新的 HTML,并替换 cache 中的缓存
    浏览器获取到 service worker 返回的主文档,进行正常的 html 解析流程
  4. 此时用户看到的还是旧的 HTML 文档,为了获取最新的数据,我们需要在 js 中插入一段和 sw 通信的逻辑。可以在执行时从 sw 中最新的 HTML 取出 __initialState (服务端渲染时的数据标识位),并将数据返回给页面。
  5. 页面拿到最新的数据后,通过最新的数据来更新页面。
  6. 当现网需要紧急 fix 时,可以将 PWA 开关关闭,此时进入页面的二次用户不会走到有问题的缓存,而会强制刷新,保证使用线上的最新代码。

CSR 浏览器渲染

非直出场景下的缓存更新方案

非直出的方案 html 文档中没有数据和 DOM 节点,更新机制相比直出页面要复杂一些:

  1. 用户第一次访问页面时,在数据加载完成后,我们将该页面的 outerHTML 取出,传递给 service worker,将其作为主文档缓存到 cache。
  2. 用户第二次访问页面时,service worker 拦截页面的 HTML 请求,并行做两件事:
  3. 在 cache 中查找,并返回缓存的 HTML 文档给用户
    进行网络请求,获取最新的 HTML
  4. 浏览器获取到 service worker 返回的主文档,进行正常的 html 解析流程,请求 cgi 更新数据。
  5. 我们的 webpack 插件在构建时会在 html 中插入一段 hash。此时 service worker 会对比缓存中的 HTML 和网络请求的最新 HTML hash是否一致,如果本地缓存的版本不是最新的,就将缓存替换为网络请求的最新 HTML。

效果如何?

根据我们的数据,在接入 PWA-PLUS 方案后:

直出页面的首屏时间优化了 300ms
非直出页面的首屏时间提高了 500ms

具体数据请期待后续的数据篇~

阿特:提前完成下半年KPI,计划通り