Skip to content

14 KB 法则:把作品集放进一次 TCP 往返

TCP 在连接建立时大约会先发送 14 KB,然后暂停等待 ACK。把整页塞进这个窗口,它就能在一次往返内到达。这个站点首页是这么做到的。

Performance APITCPAstroGitHub Actions

本站首页的 Live Proof 条最终稳定在 14.0 KB 的总传输字节(HTML、CSS、JS 合计)。这不是为了好看选的整数。是你想让页面在一次 TCP 往返内到达就必须遵守的预算。

为什么是 14 KB

TCP 连接建立时,服务器不会一次发完所有数据。它先发一小批,等客户端确认后再继续。这是为了防止一个还不知道带宽多少的发送方淹没网络。

那一小批的大小叫 initial congestion windowinitcwnd)。从 2013 年的 RFC 6928 开始,Linux 与主流 Web 服务器使用 10 段 作为默认 initcwnd。多数路径上 Maximum Segment Size 约为 1460 字节,所以:

10 段 × 1460 字节 = 14,600 字节 ≈ 14.3 KB

包头会再吃掉一点点,实用的简化记法是 “前 ~14 KB 的 HTML / CSS / JS 在一次往返内到达;之后的字节要等第二次往返”。

第二次往返的开销,等于你的访客到边缘节点的 round-trip time。Vercel 部署的话,同区域 20 ms,跨大洲 300 ms 都有。守在 14 KB 之下,相当于免掉每位首访者本来要付的那一次往返。

TCP slow-start 的样子

   客户端                                服务端
     │                                     │
     │  ─── SYN ───────────────────────▶   │
     │  ◀──────────────── SYN/ACK ──────   │   (1 RTT,建立连接)
     │  ─── ACK + GET / ──────────────▶    │
     │                                     │
     │  ◀── 10 段(~14 KB)─────────────   │   (1 RTT,第一批)
     │  ─── ACK ──────────────────────▶    │
     │                                     │
     │  ◀── 后续段 ─────────────────────   │   (第 2 RTT 之后)
     │                                     │

如果页面塞进第一批,浏览器可以立刻开始解析与渲染。塞不下的话,后续字节就被一次你无法加速的往返挡住。

本站如何保持在 14 KB 以下

我在 2026 年 6 月重做了这个站点,并将其迁移到 Astro 7 + Tailwind 4 + TypeScript 6。两轮工作对预算最关键:

1. 去掉那些价值不足以抵消其体积的依赖。 我移除了九个包。每个包在它原本所在的典型页面上的大致重量:

lenis            → 约 9 KB(minified + brotli)
motion           → 约 22 KB
astro-icon       → 约 5 KB(再加每个图标的 SVG 负载)
@iconify-json/mdi → 约 3 KB
astro-i18next    → 约 6 KB
i18next          → 约 14 KB
深浅色切换脚本 + CSS → 约 3 KB
glass-card CSS(旧版) → 约 2 KB
主题检测内联脚本 → 约 1 KB

合计约 65 KB 的 JS 与 CSS 离开,重设计里完全用不到。

2. 让 Tailwind 4 自动剪枝。 Tailwind 4 的 CSS-first 配置只发出 source 文件里实际被引用的 utility。首页上的生成样式表大概 5 KB。没有 tailwind.config.ts,也没有运行时 JIT 编译开销。

3. 能内联就内联。 Astro 的 build.inlineStylesheets: 'auto' 把小样式表塞进 HTML,省掉一个额外请求。自建的 SVG 图标随 HTML 负载一同传输。Google Fonts 通过 preconnect 提前,字体文件并行加载;因为是子资源,不算进第一次往返的预算里。

任何页面都能自测

把这段脚本贴在页面底部,看一下你自己的预算:

<script is:inline>
  (function () {
    function fmtKb(b) { return b < 1024 ? Math.round(b) + ' B' : (b / 1024).toFixed(1) + ' KB'; }
    function fmtMs(ms) { return ms < 1000 ? Math.round(ms) + ' ms' : (ms / 1000).toFixed(2) + ' s'; }

    window.addEventListener('load', function () {
      setTimeout(function () {
        var nav = performance.getEntriesByType('navigation')[0];
        var total = nav && nav.transferSize ? nav.transferSize : 0;
        performance.getEntriesByType('resource').forEach(function (r) {
          if (r.transferSize) total += r.transferSize;
        });
        console.log('页面体积:', fmtKb(total));
        console.log('domInteractive:', fmtMs(nav.domInteractive));
      }, 250);
    });
  })();
</script>

transferSize 已经算上了压缩。如果超预算,DevTools 网络面板顶部的横条会告诉你哪个资源最重。

在 CI 里强制执行预算

进入 14 KB 之后,唯一能守住的办法是让构建在超线的时候失败。一份给 Astro 静态构建的最小 GitHub Action:

name: Page weight budget
on: [push, pull_request]

jobs:
  budget:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
      - run: npm ci && npm run build
      - name: Check homepage weight
        run: |
          BUDGET_BYTES=$((14 * 1024))
          PAGE=$(stat -c %s dist/index.html)
          CSS=$(find dist/_astro -name '*.css' -exec stat -c %s {} + | awk '{s+=$1} END {print s+0}')
          JS=$(find dist/_astro -name '*.js' -exec stat -c %s {} + | awk '{s+=$1} END {print s+0}')
          TOTAL=$((PAGE + CSS + JS))
          echo "homepage payload: $TOTAL bytes (budget: $BUDGET_BYTES)"
          if [ $TOTAL -gt $BUDGET_BYTES ]; then
            echo "::error::page payload exceeds 14 KB budget"
            exit 1
          fi

它在部署之前跑。如果 PR 加进了一个把首页推过线的依赖,合并就会被拦下。

值得知道的几个注意点

  • 14 KB 的数字适用于到你源站的第一次 TCP 连接。已经预热过的连接上拉取的子资源不付同样的代价。
  • HTTP/2 与 HTTP/3 在单连接上多路复用请求,但在初始连接上仍然遵守 TCP slow-start。
  • 与客户端长连接的 CDN(一部分回头访客)会完全跳过 slow-start。这条规则对首次访客与冷缓存场景最有意义。
  • 压缩很重要。14 KB 是 “在网线上的字节”,即 gzip/brotli 之后。同一个站点未压缩可能有 50 KB,也仍然能塞下。

总结

这种优化是 Lighthouse 分数已经把其他都拉满之后才显形的那种。如果你还没定下排版、配色或字体,先做那些。一旦设计稳定,14 KB 法则就是性能里最便宜的赢面之一。你在做减法,不是加法。

本站整页此刻就在预算之下:14.0 KB 传输量,桌面浏览器上 160 ms 的 domInteractive。Live Proof 条每次页面加载都在测。一旦越线,CI 会在你看到之前就把构建拦下。


更多性能笔记,以及偶尔关于 Shopify Plus 的吐槽,都在 X:@jaimesolis。想给自己的项目搭一套带预算约束的构建,联系表单点两下就到。