BFCache 阻塞清单里最扎眼的一条:open WebSocket——Chrome 157 进缓存前先断开它,返回页面从整页重载变成 0 次网络请求

一条没关的 WebSocket 连接,曾经直接把整页挡在 Back/Forward Cache 之外:用户在详情页点返回,浏览器只能重新下载、重新解析、重新执行。Chrome 157 把这件事改了——页面进缓存之前先把 WebSocket 断开,同时给没有重连逻辑的老应用留了一个企业策略开关。10 月 14 日进 Beta,11 月 3 日进 Stable。

挡在 BFCache 门外的到底是什么

BFCache 不是 HTTP 缓存,它是把整页连同 JS 堆、定时器、未完成任务一起冻结在内存里,用户点返回时直接解冻。浏览器不敢随便冻住一页的原因是:页里可能还有任务在跟同源其他标签页共用的 API 打交道,冻住它会把别的标签页也卡住。

web.dev(2026-07-02 更新)给的阻塞清单是这几条:

  • 打开着的 IndexedDB 连接
  • 进行中的 fetch() / XMLHttpRequest
  • 打开着的 WebSocket 或 WebRTC 连接

这不是小众场景。Chrome 的使用数据显示,桌面端每 10 次导航里有 1 次、移动端每 5 次里有 1 次是前进/后退;web.dev 收录的 Yahoo! JAPAN News 案例里,A/B 测试中用上 bfcache 的页面广告收入高了 9%。也就是说,一条 WebSocket 卡掉的不是一次点击,而是一条能直接算成钱的路径。

Chrome 157 改了什么(before / after)

Chrome 156 及以前 Chrome 157
页面有活跃 WebSocket 文档被标记为不可缓存,整页进不了 BFCache 进缓存前主动断开 WebSocket,页面照常缓存
用户点返回 重新下载、重新解析、重新执行 从内存恢复,不走网络
WebSocket 的收尾 页面销毁时连接一起没,业务层感知不到「恢复」 恢复之后补发 error / close,要自己重连
出问题怎么退回 — 企业策略 BackForwardCacheForWebSocketsAllowed 设为 Disabled

机制写在 WHATWG 规范里,两条 PR 都还开着:

  • HTML 仓库 PR #12349(2026-04-09 提交):删掉「把文档标记为 unsalvageable」那一步,改成进入 BFCache 时调用 WebSocket 上下文清理步骤,并注明这是照着 WebTransport 的既有做法来的。
  • WebSockets 仓库 PR #74(2026-04-13 提交):新增的清理章节由 HTML 规范的 unloading document cleanup steps 调用;它触发连接失败,排队的 error 和 close 任务只在页面从 BFCache 恢复之后才派发。

一句话概括前后差异:以前是「连接活着,页面就不许缓存」,现在是「缓存前先把连接断掉,恢复之后再补一张通知给你」。

策略开关的措辞也很直白:BackForwardCacheForWebSocketsAllowed 设为 Disabled,就能回到旧行为,官方给的理由是「防止缺少 WebSocket 重连逻辑的老应用出问题」——换句话说,Chrome 自己也默认这会对老代码造成破坏。

时间线:同一条改动在发布说明里出现过两次

  • Chrome 149 的发布说明里就有「Disconnect WebSockets on bfcache entry」,跟踪号 467838624,措辞是「关闭连接,而不是把文档标为不可缓存」。
  • Chrome 150 到 156 的发布说明里没有再提这条。
  • Chrome 157 再次出现,措辞变成「此前活跃的 WebSocket 会阻止页面进入 BFCache」,并补上了上面那个企业策略。
  • 而 web.dev 的 bfcache 文档(2026-07-02 更新)现在还写着「Chrome(自 149 起)和 Safari 不会因为开着的 WebSocket 拦截,其他浏览器会」。

发布说明和文档在同一件事上还没有对齐,所以别拿记忆当下结论,以你自己跑出来的测试结果为准(下面第三步)。

代码:恢复之后自己把连接接回来

场景:聊天、行情、协作编辑这类页面,页面里常驻一条 WebSocket,切走再回来必须接着收消息。157 之后断开是浏览器替你做的,重连得你自己做。

// 场景:直播间聊天页,常驻一条 WS,连上后订阅频道,服务端还会推心跳
let ws = null;
let heartbeatTimer = null;

function connect() {
  // 恢复时 onclose 和 pageshow 可能同时触发,先挡住第二条连接
  if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) return;

  ws = new WebSocket('wss://api.example.com/stream');

  ws.onopen = () => {
    ws.send(JSON.stringify({ type: 'subscribe', room: 'live' })); // 断线期间错过的订阅要重放
    heartbeatTimer = setInterval(() => ws.send(JSON.stringify({ type: 'ping' })), 25000);
  };

  // 157 之后,从 BFCache 恢复时补发的 error/close 会落到这里
  ws.onclose = () => {
    clearInterval(heartbeatTimer);
    setTimeout(connect, 1000);
  };

  ws.onmessage = (e) => render(JSON.parse(e.data));
}

// 正常加载时连,从 BFCache 恢复时也连
window.addEventListener('pageshow', (e) => {
  if (e.persisted || e.target === window) connect();
});

// 页面进缓存前停掉心跳,别让被冻结的定时器留着旧状态
window.addEventListener('pagehide', (e) => {
  if (e.persisted) clearInterval(heartbeatTimer);
});

两个容易踩的点:

  1. 不要两处各写一套重连。web.dev 特意提醒恢复时要确保「不要打开多条连接」,onclose 自动重连和 pageshow 重连必须共用一个入口(上面的 connect() 开头那行判断)。
  2. 订阅状态要重放。连接断开期间服务端的订阅关系就没了,光重连不重发 subscribe,页面会表现成「连上了但收不到消息」——这比直接报错难查得多。

下一步:先跑一次测试再决定要不要改代码

打开你的聊天/行情/协作页面,DevTools → Application → Back/forward cache → Run Test,Chrome 会自己跳走再跳回,成功就显示 “Restored from back-forward cache”,失败会列出原因,能改的会标成 Actionable。线上真实阻塞原因用 notRestoredReasons API 收集,确认 WebSocket 确实在阻塞清单里,再按上面的代码补 pagehide / pageshow。

评论区

0 条评论

登录后可评论。

阿速·性能优化 63 阅读