写这篇文章,是想分享下团队目前基于 service worker 缓存所做的性能优化工作,我们的方案覆盖了包括 html 缓存的直出和非直出场景,欢迎拍砖交流 👏
报告老板,下半年的性能优化 KPI 有着落了 🎉
性能优化
让我们从一个小故事谈起:
老板:阿特!下半年先定一个小目标,实现页面秒开。
阿特想起了学过的各种性能优化手段,心里暗喜,这里终于不怕完不成目标啦。
阿特回来后就在项目上加上了预加载,懒加载,雪碧图等功能,忙活了好几天。
几天后
项目终于上线了,看到测速数据,阿特很疑惑:服务端渲染都加上了,为什么我的页面还是没有秒开呀?
要弄懂这个问题,让我们先看看页面打开时都经历了什么
假设有一个移动端app的内嵌页面,打开这样一个页面,需要经过
打开webview -> http 请求主文档 -> 解析并渲染
(http请求到页面渲染的部分其实很复杂,这里就不展开了)
webview 的打开时间取决于客户端,从前端的角度来看,考虑如何尽量减少 webview 打开到页面展示的时间。简单来说,需要在 http 请求前/请求同时 展示页面。
目前已经有一些成熟的方案,例如手 Q 里的 sonic,webso,以及部分 app 支持的离线包等
然而它们并没有这么完美……
业务痛点
目前的方案具有以下几个缺陷:
- 是场景覆盖,现有的解决方案都深度依赖客户端能力,而在第三方app内,例如微信,浏览器等场景下,没有很好的缓存方案。导致分享页等多场景页面的体验欠佳。
- 目前使用的方案具有并发限制,并发资源数越多,加载时间也会成线性增加。
- 使用现有的缓存解决方案,本地资源的更新取决于客户端拉取间隔(通常是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 架构
基于以上的构想,我们设计出这样一套架构:

- 构建阶段:在构建阶段,在 webpack 构建时引入我们封装的插件
@tencent/pwa-plus-plugin。插件会根据设置的规则,提取符合规则的构建产物生成pwa-manifest.json并同步到配置平台。 - 线上环境:用户首次访问页面时,页面注册的 service worker 会发送请求到配置平台,获取该页面的 PWA 开关状态,如果 PWA 状态是开启的,再拉取该项目的缓存资源的列表,请求资源并缓存到 cache 中。用户二次访问页面时, service worker 会代理所有请求,优先使用本地的缓存资源。
缓存 HTML 文档
PWA方案中,除了常规的静态资源缓存,我们更希望能够 缓存 html 主文档,从而实现 二次用户的秒开。
那么问题就来了
何时应该更新缓存的 html 文档呢?
如何做到无痛更新?
我们针对直出(服务端渲染)和非直出(浏览器端渲染)两种场景分别定制了更新方案:
直出场景
由于直出的页面,html 文档中已经包含了数据,我们只要处理好数据的更新即可:
- 用户第一次访问页面时,service worker 将直出的 HTML 文档(带dom节点和数据)缓存到 cache 中。
- 用户第二次访问页面时,service worker 拦截页面的 HTML 请求,并行做两件事:
- 在 cache 中查找,并返回缓存的 HTML 文档给用户
- 进行网络请求,获取最新的 HTML,并替换 cache 中的缓存
- 浏览器获取到 service worker 返回的主文档,进行正常的 html 解析流程
- 此时用户看到的还是旧的 HTML 文档,为了获取最新的数据,我们需要在 js 中插入一段和 sw 通信的逻辑。可以在执行时从 sw 中最新的 HTML 取出 __initialState (服务端渲染时的数据标识位),并将数据返回给页面。
- 页面拿到最新的数据后,通过最新的数据来更新页面。
- 当现网需要紧急 fix 时,可以将 PWA 开关关闭,此时进入页面的二次用户不会走到有问题的缓存,而会强制刷新,保证使用线上的最新代码。
非直出场景
非直出的方案 html 文档中没有数据和 DOM 节点,更新机制相比直出页面要复杂一些:
- 用户第一次访问页面时,在数据加载完成后,我们将该页面的 outerHTML 取出,传递给 service worker,将其作为主文档缓存到 cache。
- 用户第二次访问页面时,service worker 拦截页面的 HTML 请求,并行做两件事:
- 在 cache 中查找,并返回缓存的 HTML 文档给用户
- 进行网络请求,获取最新的 HTML
- 浏览器获取到 service worker 返回的主文档,进行正常的 html 解析流程,请求 cgi 更新数据。
- 我们的 webpack 插件在构建时会在 html 中插入一段 hash。此时 service worker 会对比缓存中的 HTML 和网络请求的最新 HTML hash是否一致,如果本地缓存的版本不是最新的,就将缓存替换为网络请求的最新 HTML。
效果如何?
根据我们的数据,在接入PWA-PLUS方案后:
- 直出页面的首屏时间优化了 300ms
- 非直出页面的首屏时间提高了 500ms
阿特:提前完成下半年KPI,计划通り