← 返回博客
EngineeringBuild in PublicSEO

我们的 Next.js 页面对 Bing 来说太大了。RSC 负载占了 HTML 的 58%

三个 Next.js 页面卡在 Bing 的「已发现但未抓取」。App Router 把每个字节发两遍。我们把其中一页压掉 48 KB,Bing 收录了它。

2026年10月8日

我们的 Next.js 页面对 Bing 来说太大了。RSC 负载占了 HTML 的 58%

Katto 是一个用 AI 剪视频的工具。你给它一条长视频,它找出最好的片段,切成带字幕的竖版短视频。我一个人做它,公开地做,用 Next.js 和 App Router。这篇文章讲的是一次改变了我写页面方式的测量,一个我当着所有人的面说得很笃定、结果是错的数字,以及我自己一个只值 108 字节的假设。

症状

Bing 网站管理员工具把我们好几个页面标为「已发现但未抓取」。没有被 robots.txt 挡住,没有 noindex,不慢,也不跳转。发现了,然后就放着。Google 收录了它们。我们的站点地图里有它们。谁来请求它们都返回 200。

被拒绝的页面有一个共同点,而那不是它们的内容。

这条线,以及我们对它了解得多么粗糙

微软没有公布爬虫放弃的尺寸。我们手上只有自己的页面:状态是在 Bing 网站管理员工具里读到的,体积是同一天用 bingbot 的 User-Agent、不压缩量出来的。

线另一侧只有一个页面,构不成定律。但这份清单里有一条观察比其余全部都值钱,因为它是同一个网址出现了两次。/compare/ai-video-clippers 当时输出 172,295 字节,在「已发现但未抓取」上卡了好几周。我们把它压到约 124 KB。今天它被收录了,体积 127,951 字节。同一个网址、同一套模板、同一类内容:先是太大,后来够小。

一周前我会给你另一个数字,而那个数字是错的。我手上有三条状态记录,加上我们页面体积分布里一个显眼的空档,我就从里面读出了一个 125 KB 左右的界线。那三页里有两页此后在不到 128 KB 的体积上被收录了。我们自己的站点真正能说的,是这条线落在 128 KB 到 140 KB 之间,而它的另一侧至今仍然只有一页宽。

如果你是为一个数字来的,诚实的回答是:量你自己的。我能站得住脚的说法反正更窄也更有用:一个卡在「已发现」的页面,值得先称一称,再考虑重写;而一个在被压小之后开始被抓取的页面,是唯一真正算数的证据。

称源文件什么也说明不了

下面这部分是 React Server Components 特有的。App Router 会把渲染出来的树序列化进同一份 HTML 文档,形式是一连串 self.__next_f.push(...) 脚本标签,好让客户端导航不必再往返一次就能接着跑。这段负载不是爬虫可以跳过的另一个请求。它就在文档里。

在我们一个页面上量到:输出 125,981 字节,其中 73,571 字节是这段负载。文档的百分之五十八,是另外百分之四十二已经说过的内容的序列化副本。

所以一个在 24 行循环里只写了一次的工具类,不是被发送 24 次。是 48 次。

我花最久才看明白的一点

它不是把页面翻倍。它是把页面已经承载的每一种表示各翻一倍。

我们的路线图刻意把每一条渲染了两遍:一遍是可见正文,一遍放在 JSON-LD 的 ItemList 块里,好让机器读者拿到名称、日期和状态这样的数据,而不是需要去理解的散文。这很合理。然后我数了数某一条的描述文字在输出文档里出现几次,结果是四次。

两种表示,各自翻倍。同一句话的四份副本,而我从来没想过去数。

办法不是压缩。那段 JSON-LD 里的 description,一字不差地抄了读者在三十行之下看到的同一句话,同一种语言。它什么也没带来,却吃掉了四份副本里的两份。删掉之后,这个块保留名称、日期和状态,而这正是散文不会向机器明说的东西。那个页面从 121,196 字节降到 115,350 字节,没有任何信息离开。两个数字来自同一次构建,就在那一处改动的前后,而这是「前后对比」唯一能有意义的方式。

如果你只从这里带走一个习惯:在想办法减重之前,先数一数你内容里的某一句话在输出文档里出现了几次。这个问题,称量各个区块是答不出来的。

两根更大的杠杆

把重复的 class 属性搬进样式表规则。 Tailwind 的工具类写起来便宜得让人高兴,而它们会跟着每一个元素出去,两遍。把重复最多的那些搬进 @apply 规则,让我们的价格索引从 172,295 字节降到约 142,000 字节。它们大约占了标记的百分之五十一。

把非交互的块移出序列化的树。 说准确一点,因为这点很重要:这些是 Server Components,所以它们永远不会在客户端被水合。代价不是水合,是序列化。每一个元素、每一个 prop、每一个 key 都会被写进 flight 负载,好让客户端导航不必往返就能重建这棵树。一个没有任何客户端行为的静态表格照样要付这笔钱;而当约束就是文档体积时,它根本没有理由继续是 React 元素。现在我们用同样的数据把这类块拼成 HTML 字符串,用 dangerouslySetInnerHTML 放进去,并在拼接时对每一个插值做转义,这样转义就不取决于数据从哪来。价格索引又省了 18 KB。路线图上一个 54 行的列表用同样的办法从 157,308 降到 119,031。

没起作用的那条路

在这些之前,我确信问题出在组件边界上。一个列表里五十四个 next/link 组件,每一个都把自己序列化后的名称、props 和 key 加进负载。我把五十四个全换成了普通的 a 标签。

在 157,200 字节里省下了 108 字节。

Link 很便宜。它包着的内容不便宜。如果我没有在前后各量一次,我会按那个理论花掉一整天去重写组件;而这个否定结果是那天最有用的数字。

有一个地板,而且不低

我们最轻的真实页面 /contact,只放了一个标题、一个短表单和几个问题,却要输出 46,111 字节。其余是导航、页脚、分析和字体。在这个数字以下,一页一页地抠已经没有收益了:那会变成外壳的工程,而不是页面的工程。知道地板在哪,就知道什么时候该停。

翻译花掉的是字节,不是字数

我们把那份路线图做成了十种语言。同样的标记,同样的结构,同样的信息。英文页输出 116,239 字节,法文 119,838,日文 122,155,印地文 135,379。

天城文在 UTF-8 里每个字符要三个字节,而拉丁文本只要一个,然后负载再付一遍。页面层面的任何手法都碰不到这件事:同样的标记,同样的信息,相差十九千字节。

有意思的是印地文那一页,因为 135,379 字节正好落在我们无法判定的那段区间里。Bing 会不会处理它,是我们手上最便宜的实验,代价是一次网址检查。

我这里曾经写过一段,论证超出的部分其实没那么要紧,因为它落在文档末尾,而那一页的末尾是一个标题反正保持英文的列表。我把它删了,因为它预设了爬虫会一直读到预算用完才停。我真正观察到的一切,只说明超过某个体积的页面不会被处理。如果文档是整份被拒的,那它的顺序什么也买不到,而我两个方向的证据都没有。这是一个关于我自己页面的、让人舒服的理论,而这正是最该删掉的那一类。

这是预算,不是修好了

我从 172,295 字节压到 124,202 字节的那个价格索引,今天是 127,951 字节,因为我们一直在往上加:更多工具、更多列、十种翻译。它仍然被收录着,这才是重点;而它同时也已经回到离那条线只有几千字节的地方。没有出什么岔子。我们只是没盯着,又把预算花掉了一次。

这才是真正的结论。在一个会序列化自己输出的框架里,页面体积不是做一次就完事的清理。它是每当有人做了好工作就会往上走的数字,而唯一的防守,是去量实际输出的文档而不是源码,一次只改一处。

怎么量你自己的

相关文章

准备好把你的视频变成爆款短片了吗?

Katto 自动将你的长视频剪辑、加字幕并重新构图,转换为短视频内容。

免费试用 Katto →