2026年11月7日,Datasette 创始人 Simon Willison 在检查 Firefox 网络面板时抓到了一个隐形性能黑洞。每次打开 Datasette 的数据表页面,Firefox 都会在后台自动发起一次 HTTP 预取请求,把下一页的表格数据提前拉下来,平白多出约 80 KB 的 HTML 传输量。绝大多数访客根本不会点击下一页,这意味着客户端白耗了带宽,服务端还要多跑一轮完整的数据库查询和页面渲染,最后把算出来的结果直接扔进垃圾桶。
更有戏剧性的是溯源过程。罪魁祸首是 Datasette 在 HTTP 响应头里附带的 link: <...>; rel="next"。Willison 顺着线索查到 Mozilla 的 Bugzilla 仓库,发现关于这一行为的 Bug 175418 已经开放了整整 24 年。而在该问题早期的讨论区里,赫然贴着 Willison 自己在 2002年10月17日 写的一篇博客链接——那篇文章正是吐槽 Mozilla 不该把 rel="next" 当成预加载指令。二十四年前扔出的回旋镖,精准砸中了二十四年后自己写的开源框架。
标题写着针对 Firefox,代码却切开了视图与 API 的混用
看一眼 Datasette 的 Issue #3046 标题,写的是停止向 Firefox 的 HTML 页面发送 link: rel=next 响应头。如果只看字面,很容易以为他写了一段嗅探 Firefox User-Agent 的补丁。
翻看实际提交的修复代码(Commit 77bc27bd17d89c1d7996df6905249eeecccf6452),手法要干脆得多。在 datasette/views/table.py 中,Willison 直接把判断逻辑改成了 if next_url and format_ != "html":,并同步更新了 tests/test_table_api.py 的测试用例。他没有去识别浏览器身份,而是对所有 HTML 响应一刀切移除了该响应头,只将其保留给 JSON 等 API 格式。
这种改法暴露了现代 Web 框架常见的一个工程偷懒。很多后端系统为了省事,会让人类访问的 HTML 网页和供机器调用的 JSON API 共用同一套输出管道。API 客户端确实需要 HTTP 头里的 rel="next" 来做游标分页,但普通浏览器渲染 HTML 页面时,页面上已经有可视化的下一页按钮,HTTP 头里的分页链接纯属冗余。
直接按格式切开,既避开了脆弱的客户端嗅探,又顺手还清了架构上视图与接口混用的技术债。
为什么只有 Firefox 会触发这个隐形黑洞
同样是收到 Link: <...>; rel="next" 的 HTTP 响应头,为什么 Chrome 和 Safari 相安无事,唯独 Firefox 会在后台默默拉取数据?
翻看三大浏览器引擎的底层源码,分歧点一目了然:
- Firefox (Gecko):在
nsContentSink.cpp的实现中,引擎明确把 HTTP Link 头里的next和prefetch混为一谈,统统丢进预取队列。 - Chrome (Chromium):在 HTTP 响应头层面,明确要求关系类型为
prefetch才会触发预加载,直接忽略rel="next"(不过其link_loader.cc对 HTML 标签里的<link rel="next">仍保留了独立处理逻辑,不能简单混同)。 - Safari (WebKit):同样不会把 HTTP 响应头里的
rel="next"当作下一页预取的触发器。
我更在意的是,这个在 2002 年就被指出的语义混淆,为什么能在 Mozilla 的缺陷列表里躺满 24 年而不被修复。
这背后是两代 Web 架构的正面冲撞。在 2002 年的静态网页时代,网页大多是预先生成好的静态文件,挂在目录里等访问。Mozilla 早期的工程师做了一个极其乐观的启发式假设:既然页面标明了有下一页,用户大概率会点进去,不如趁着网络空闲先帮用户下载好,还能博一个翻页秒开的爽快感。
静态时代的激进优化,成了动态计算时代的算力刺客。
到了今天,像 Datasette 这样的动态 Web 应用,每一次 URL 请求背后都是实打实的实时数据库查询、权限校验和模板渲染。当年顺手省下的几百毫秒翻页等待,在今天直接异化成了服务端双倍的算力消耗。
更麻烦的是,这笔冤枉账存在严重的防御成本错位。很多后端开发者第一反应是加一行 Cache-Control: no-store 响应头来阻止预取。但这招在物理逻辑上根本走不通——Firefox 必须先向服务器发出请求、触发完后端的数据库查询和页面渲染,才能拿到目标页面的响应头。等浏览器看到 no-store 决定不缓存时,服务端的算力早就烧完了。
而在客户端一侧,Firefox 只在 about:config 里提供了一个名为 network.prefetch-next 的全局布尔开关。普通用户根本不知道它的存在,开发者也无法通过标准协议在单页面粒度上劝阻浏览器的自作主张。
《淮南子》有言,苟利于民,不必法古;苟周于事,不必循旧。Web 标准把描述文档先后顺序的元数据 rel="next",与明确下达预加载指令的 rel="prefetch" 拆成两个词,本意就是让职责各归其位。Gecko 引擎死守二十四年前的旧假设,把逻辑关系强行当成执行指令,最终逼得下游框架只能用砍掉 HTML 响应头的方式自保。
- 提醒.如果你的后端框架同样复用了 API 分页逻辑,顺手往 HTML 响应头或页面
<link>标签里塞了rel="next",现在就可以去查一眼服务器日志了——来自 Firefox 的流量,可能正在让你的数据库白白承受双倍查询。
