题目与背景
你的站点用 Service Worker 拦截导航请求,以便离线回退和统一缓存。用户发现首次导航必须等待 Service Worker 启动后才开始网络请求。请设计 navigation preload,要求兼容不支持该 API 的浏览器,并避免重复请求、旧缓存和响应竞态。
面试官考察什么
面试官要确认你知道 navigation preload 是浏览器在 Service Worker 启动期间并行发起的导航请求,不是预缓存,也不是任意资源的 link rel=preload。答案应覆盖 activate 阶段启用、fetch 事件读取 preloadResponse、网络与缓存的优先级、Service-Worker-Navigation-Preload 请求头、超时和失败回退,以及指标验证。
先问清楚的澄清问题
页面与缓存目标
确认导航是 SSR HTML、SPA shell 还是离线页,哪些路径可缓存,用户身份数据是否必须绕过共享缓存。导航 preload 不应把个性化响应写入公共缓存。
生命周期与浏览器支持
确认 Service Worker 的注册、更新和控制范围,以及目标浏览器对 navigationPreload 的支持。未控制页面的第一次导航不能假定会经过当前 Service Worker。
网络与一致性策略
明确网络优先、缓存优先或 stale-while-revalidate,及网络失败时的离线响应。还要确定服务器如何读取自定义 preload 头并防止它改变缓存键。
30 秒回答框架
“在 Service Worker 的 activate 事件中检测并启用 navigation preload。浏览器会在 Service Worker 启动时并行请求导航;fetch 事件先读取 event.preloadResponse,没有结果再按策略查缓存或发起普通 fetch。只接受与当前 URL、身份和缓存策略匹配的响应,避免 preload 与普通 fetch 重复。通过支持率、Service Worker 启动耗时、首字节时间、缓存命中和错误回退对比启用前后效果。”
深入解答步骤
第一步:在 activate 中启用
等待 registration.navigationPreload 存在后调用 enable(),通常放在 activate 的 waitUntil 中,确保新 Worker 激活前完成配置。可以用 setHeaderValue() 设置可识别的请求头,但不要把用户隐私或不可缓存的状态放进头值。
第二步:在 fetch 事件消费 preloadResponse
仅对导航请求读取 event.preloadResponse。它是一个 Promise,可能解析为 Response,也可能为 undefined。优先使用合法的 preload 响应;没有响应时再查缓存或发起 fetch,避免同时启动第二个网络请求。
第三步:定义缓存与身份边界
SSR HTML 若包含用户身份、地区或实验分组,应按现有缓存键和响应头处理,不能把 preload 响应无条件写入 Cache Storage。静态 shell 可以缓存优先,个性化页面可以网络优先;两者都要验证 Vary、Cookie 和 Authorization 的影响。
第四步:处理头值和服务器路由
服务器可读取 Service-Worker-Navigation-Preload 头来识别并行请求,决定是否跳过某些昂贵工作或采用专门缓存。头值改变不应导致错误的跨用户缓存共享;代理和 CDN 规则也要明确是否转发、忽略或纳入缓存键。
第五步:处理竞态、超时和错误
如果 preload 失败、超时或返回不适合当前请求的状态,按统一策略转向缓存或普通 fetch。不要让一个已消费的 Response 再次读取 body;需要并行分支时使用 clone(),并控制超时和取消。网络错误后再尝试离线页,但不要覆盖真实的服务器错误。
第六步:渐进降级与更新
不支持 navigation preload 时继续使用普通 Service Worker fetch;不支持 Service Worker 时直接走网络。更新 Worker 时保持旧版本的缓存协议,清理动作放在确认新版本可用之后,避免 activate 期间删除仍被页面使用的缓存。
第七步:验证真实性能与正确性
对照冷启动、热启动、慢网、离线、未控制首访和更新中的 Worker,记录导航开始到响应、TTFB、Service Worker 启动、preload 命中、缓存命中、重复请求和错误回退。检查页面身份、缓存隔离、跨浏览器支持及服务器头值,不只看平均加载时间。
高质量示例回答
我会在 activate 中 feature-detect 并启用 navigation preload,在导航 fetch 事件先等待 preloadResponse,只有它没有可用响应时才走缓存或普通 fetch。缓存策略按页面是否个性化拆分,避免把带 Cookie 的 SSR HTML 写入共享缓存;服务器可用 Service-Worker-Navigation-Preload 头识别请求,但不能让它破坏缓存键。
不支持该 API 的浏览器保留普通 Service Worker 或网络路径。测试覆盖冷启动、慢网、离线、未控制首访、Worker 更新和重复请求,比较 TTFB、启动时间、preload 命中及错误回退,确认性能收益没有牺牲一致性。
常见错误
- 错误: 把 navigation preload 当成静态资源预缓存。→ 原因: 它只针对导航请求并与 Worker 启动并行。→ 改进: 在 fetch 事件消费
preloadResponse。 - 错误: preload 没结果时无条件再发一个网络请求。→ 原因: 可能造成重复请求和副作用。→ 改进: 明确 Promise、缓存和普通 fetch 的顺序。
- 错误: 将个性化 HTML 写入共享 Cache Storage。→ 原因: Cookie、Authorization 或 Vary 边界被忽略。→ 改进: 按身份和缓存键设计网络优先策略。
- 错误: 只比较平均加载时间。→ 原因: 冷启动、未控制首访和重复请求问题会被掩盖。→ 改进: 分层记录 TTFB、启动、命中和错误指标。
追问与回答
追问 1:为什么不直接在页面使用 link preload?
link rel=preload 由页面声明并针对资源;navigation preload 是浏览器为导航请求在 Service Worker 启动期间发起的并行请求。两者目的、生命周期和消费 API 不同。
追问 2:preloadResponse 为什么可能是 undefined?
浏览器可能不支持该能力、请求不是导航、preload 被禁用,或网络请求在 Worker 事件前失败。代码必须把 undefined 当作正常分支,继续执行缓存或 fetch 策略。
追问 3:如何避免 Response body 被消费两次?
一个 Response body 通常只能读取一次。若需要把同一响应交给页面并写入缓存,先使用 clone(),并确保两个读取分支都处理异常和取消。
追问 4:首次访问为何仍可能没有加速?
页面尚未受 Service Worker 控制时,导航不会经过当前 Worker;注册、安装和激活也需要时间。应把未控制首访单独统计,不能把它误判为 navigation preload 失败。