DOMException: Quota exceeded 与 不透明响应

315 阅读4分钟

背景

事发于在 Sentry 监控系统中看到一相对高频的异常:
Error: QuotaExceededError: Quota exceeded.

恰好最近的触发者是一位设计部的同事,于是直接到其电脑上复现。发现他开了非常多的标签页,而控制台报错如下:

aaa0.png

Quota exceeded 意为超出配额,发生在 Service Worker 欲写入缓存时,已使用的缓存空间已经超出了浏览器分配的存储额度

aaa0.5.png

虽然他的空间配额属于异常的小(正常应该是 GB 级别),但我们的网页(有20+个页面模块的管理平台)占用了这么大的缓存空间实在不合理

问题排查

项目里 Service Worker 缓存的内容包括 js、css 等预缓存确定资源,以及对后续请求得到的图片等 cdn 资源进行缓存

在控制台中清除缓存数据后,从第一个页面模块开始,观察导致缓存空间大幅增长的页面,对比其共同点都是有额外的 cdn 图片资源请求(如用户头像等)

但奇怪的是,请求的图片资源大小和占用缓存空间的大小严重不匹配
比如某一页面只请求了一张 14K 的图片,带来的缓存空间增长却足足有 7M

从这一不匹配的疑点出发,查找了解到 opaque response,即不透明响应

Why is my service worker storing so much cache?

原理解析

由于页面中的图片资源大都来自 cdn,所以对这些图片资源的访问都是跨域的

并且普通的图片资源请求一般不会加上 crossOrigin=anonymous 属性(通常在 canvas 中使用图片时才会加上)
所以发出的请求头中 Sec-Fetch-Mode: no-cors

而在接口的响应类型 Response.type 规范中,响应 no-cors 的跨域请求时,其值为 opaque,即不透明响应

它不透明的点在于:

JavaScript 不会读取 Response 的任何属性。这样将会确保 ServiceWorker 不会影响 Web 语义 (semantics of the Web),同时保证了在跨域时不会发生安全和隐私泄露的问题

An opaque filtered response is a filtered response whose type is "opaque", URL list is the empty list, status is 0, status message is the empty byte sequence, header list is empty, and body is null.

也就是说,Js 无法读取不透明响应的包体(状态码为 0),也就无法得知该资源的准确大小
所以在处理此类资源的缓存时,浏览器通常只能给它一个固定的较大值(Chrome 是 7MB),不管这个资源实际上会不会只有 1KB

实测也确实如此,当我把缓存中的其它正常资源都删除,只剩下 13 个不透明的图片资源时,总的存储确实为 90+ MB

image.png

image.png

解决方案

在代码中给每个 <img> 标签加上 crossOrigin 属性?不太现实,并且图片也可能是在 CSS 里通过 background 获取的
需要一个统一处理的方法,对资源请求进行拦截处理

而这一问题本就是因为在 Service Worker 中过滤出了静态资源进行缓存才出现,所以只需要对过滤出的请求统一修改请求头即可

在使用 Workbox 的情况下(对应文章),可以通过设置缓存策略类中 fetchOption 来实现:

registerRoute(
  staticNeedCache,  // 见上方文章
  new CacheFirst({
    cacheName: 'static-cache',
    plugins: [
      new CacheableResponsePlugin({ statuses: [200] }),
      new ExpirationPlugin({ maxEntries: 200, maxAgeSeconds: 7 * 24 * 60 * 60 }),
    ],
    fetchOptions: {  // 重点
      mode: 'cors',
      credentials: 'omit',
    },
  }),
);

需要注意的是,如果业务中存在对未知第三方的图片请求,可能因为设置了跨域但是对方的响应未设置 Access-Control-Allow-Origin 导致跨域请求失败
那就需要在 staticNeedCache 中只对自己的 CDN 域名进行过滤

至于用户的旧缓存,因为我的 Service Worker 本身就会在新版本的 activate 生命周期中进行旧缓存的清除,所以无需担心

或者如果像上面代码有如 ExpirationPlugin 的过期策略,那等旧资源缓存自然过期即可

之后新缓存的资源便都如期是 cors,缓存空间也极为显著地减少了

image.png

参考来源