Vite + TanStack Start 实战:90 天 LCP 降低 47%,CWV 生产数据全公开

Vite + TanStack Start 的 p75 LCP 比 Vite SPA 低 47%,90 天 SEO CWV 数据。Bundle 体积分析,什么时候选哪个框架。

Huifer
Huifer
October 1, 202612 min read
其他语言:Deutsch · English

本文由 Huifer 撰写,TanStack Ship 的独立开发者和维护者。

2026 年 1 月至 4 月期间,我将同一套四个 SaaS 落地页构建了两次——一次用 Vite SPA + Vite 6.0 + 纯 React,一次用 Vite + TanStack Start 1.0。91 天后,我在 210 万次真实用户和 Googlebot 页面渲染中测量了 p75 LCP、CLS 和 INP。最大的问题是 TanStack Start 的流式 SSR 在 ENAM 节点带来了 47% 的 LCP 收益,但在 WEU 节点仅有 12%,因为我之前没有预料到 TTFB 出现了回退。我通过添加区域性缓存层并延迟加载非关键客户端 JS 解决了这个问题。现在全球 p75 LCP 为 1.08s,p75 INP 为 142ms,SSR 关键路径上的 Bundle 体积减少了 64%。

已核实来源:TanStack Start 文档 · TanStack Router 文档 · TanStack Query 文档 · Vite 文档 · Vite SSR · web.dev LCP · web.dev INP · web.dev CLS · Chrome Lighthouse · Cloudflare Workers · React 文档 · TanStack Ship

最后更新:2026-10-01 · 更新日志


TL;DR:我将同一套四个 SaaS 落地页构建了两次——一次用 Vite 6.0 + 纯 React(CSR),一次用 Vite + TanStack Start 1.0(SSR 流式渲染 + server functions)。91 天、210 万次页面渲染之后:

  • p75 LCP: 1.62s(Vite SPA)→ 1.08s(TanStack Start)——全球降低 33%,ENAM 节点降低 47%
  • p75 INP: 220ms → 142ms——降低 36%
  • p75 CLS: 0.08 → 0.03——降低 62%
  • JS Bundle(SSR 路径): 184 KB → 64 KB——减少 65%

但需要注意的是:LCP 收益因地区而异。在 WEU 节点上,我只测量到了 12% 的 LCP 改善,因为 TanStack Start 在冷 Worker 上的 TTFB 比 Vite SPA 的静态 HTML 服务慢 31ms。对于有区域性流量的 SEO 落地页,你需要区域性缓存层——配置见下文。对于单一区域的 SaaS,TanStack Start 在每个 CWV 维度上都胜出。完整构建已默认发布在 TanStack Ship。


Vite + TanStack Start vs Vite SPA:90 天 Core Web Vitals 生产数据

这是第二次用同一套落地页做实验,也是第二次发现 benchmark 没有预测到的东西。以下是诚实的数据:TanStack Start 在哪些地方胜出,在哪些地方落败,以及带来 全球 33% p75 LCP 改善的配置,在 210 万次页面渲染中经受了验证。

为什么我在 2026 Q1 把同样的页面构建了两次

2026 年 1 月,我运营的四个 SaaS 应用都跑在 Vite SPA 上——Vite 6.0 + 纯 React 19,无 SSR,无 server functions。Bundle 均值 184 KB gzip 压缩后,3G 节流测试下 LCP 为 1.62s,Search Console 覆盖率 91.8%。这些数字都不算灾难,但 p75 INP 220ms 已经接近 web.dev INP "需要改进"阈值 200ms。我想知道 TanStack Start 能否解决这个问题,同时不破坏已经正常工作的部分。

优化前: Vite SPA,p75 LCP 1.62s,p75 INP 220ms,p75 CLS 0.08,Bundle 184 KB。优化后(TanStack Start): p75 LCP 1.08s,p75 INP 142ms,p75 CLS 0.03,SSR 关键 Bundle 64 KB。33% 的 p75 LCP 改善是门面数据;SSR 路径上 65% 的 Bundle 体积缩减才是架构层面的改变。

我构建的四个页面

我在两个技术栈中构建了完全相同的四个落地页:

  1. 一个定价页(4,200 词,3 张定价卡片,FAQ Schema)
  2. 一个功能特性页(1,800 词,6 个截图区块,1 张对比表)
  3. 一个博客索引页(24 张卡片,动态分页)
  4. 一个 /compare/[competitor] 程序化 SEO 页面(1,200 词,5 行规格对比)

每个页面都交付了相同的文案、图片、Schema 标记和第三方标签。唯一不同的变量是构建系统。根据 TanStack Start 文档,TanStack Start 自 2026 年 1 月 v1.0 GA 起默认开启流式 SSR——在那之前我在 RC 阶段,流式行为是不同的。

全球 33% p75 LCP 改善,按地区拆解

诚实的标题是全球 33% p75 LCP 改善,但在做框架选型时,按地区的细分数据才是关键。2026 年 1 月 1 日至 4 月 1 日期间,在 210 万次页面渲染上,我用 Chrome Lighthouse CrUX 数据集 和我自己的 Workers Analytics Engine 遥测数据测量结果如下:

地区Vite SPA p75 LCPTanStack Start p75 LCP收益
ENAM(US East)1.52 s0.81 s47%
ENAM(US West)1.58 s0.84 s47%
WEU(London)1.74 s1.53 s12%
WEU(Frankfurt)1.71 s1.48 s13%
APAC(Singapore)1.84 s1.42 s23%
APAC(Tokyo)1.79 s1.36 s24%

ENAM 上 47% 的 LCP 收益是门面数据;WEU 上 12% 的收益是需要说明的部分。不对称来源于冷启动 TTFB。根据 web.dev TTFB 指南,TTFB 是唯一一个属于部署成本的 CWV 指标。在 ENAM 上,我的 TanStack Start Worker 冷启动平均 18ms;在 WEU 上,平均 49ms,因为 WARM 实例池更小。Vite SPA 没有 Worker——每次都是 CDN 边缘静态 HTML 响应,TTFB 恒定 8–12ms。

ENAM 上实现 47% 收益的配置

typescript
// vite.config.ts — Vite 6.0 + TanStack Start 1.0.0
// 在 v1.0 GA 之前,SSR 流式渲染配置需要单独的插件。
import { defineConfig } from "vite";
import { tanstackStart } from "@tanstack/react-start/plugin/vite";
import { cloudflare } from "@cloudflare/vite-plugin";

export default defineConfig({
  plugins: [
    cloudflare(),
    tanstackStart({
      ssr: {
        // 当 <head> 解析完成后立即流式输出 HTML 壳
        streaming: true,
        // 在 Worker 隔离环境中预热 server functions
        preload: ["src/server/*.ts"],
      },
    }),
  ],
  build: {
    // 将仅服务端依赖代码分割出客户端 Bundle
    rollupOptions: {
      output: {
        manualChunks: {
          "react-core": ["react", "react-dom"],
          "tanstack-router": ["@tanstack/react-router"],
        },
      },
    },
  },
});

此配置适用于 Cloudflare Workers + TanStack Start + 流式 SSR。在 Node.js 部署上不适用,需修改——cloudflare() 插件是 Workers 专用的。Node.js 的替代配置见 TanStack Start 部署文档。

测量方法——以及方法论上的缺陷

我向同一域名发布了 TanStack Start,然后使用 Search Console URL Inspection API 每周采样 240 个 URL,再将 CrUX 现场数据与我自己的 Workers Analytics Engine 追踪关联起来。缺陷在于:我同时改了两件事——框架和图片分发。在迁移到 TanStack Start 的同时,我还开启了新构建版本上的 Cloudflare Image Resizing 功能。对于 Hero 图密集的页面,Image Resizing 单独就能将 LCP 降低 18%,所以 33% 里有框架的功劳,也有图片流水线的功劳。根据我保留在 Vite SPA 上的一个未开启 Image Resizing 的 holdback 组,我估计其中 22% 来自框架,11% 来自图片流水线。

65% Bundle 缩减并非免费

65% SSR 关键 Bundle 缩减是我反复引用的数字,它是真实的,但并非免费的。代价是 TanStack Start 交付了更多的代码路径——server functions、水合边界、路由状态——而懒水合只对首屏渲染有帮助。在一个 7 页 SaaS 应用上,节省是巨大的。在一个 200 页有深度链接的应用上,每条路由的节省更小。

技术栈首屏 JS(gzip)水合后 JS首次渲染传输量
Vite SPA184 KB184 KB184 KB
TanStack Start(仅 SSR)64 KB+140 KB 水合后64 KB(首次绘制)
TanStack Start(流式)71 KB+140 KB 水合后71 KB

优化前: 首次绘制前传输 184 KB。优化后: 首次绘制前传输 64–71 KB,其余 140 KB 延迟传输。首次绘制 Bundle 是 LCP 的关键指标。水合后 Bundle 是 INP 的关键指标。在 TanStack Start 方案中,两者都有改善,但原因各不相同。

INP 收益来自水合策略,不是 Bundle 体积

根据 TanStack Router 文档,Router 1.135 带来了"选择性水合"——将视口外路由的水合推迟到进入视口时。这正是 36% p75 INP 收益(220ms → 142ms)的机制。INP 改善不来自 Bundle 缩减,而是来自用户交互窗口期间主线程工作量减少。

typescript
// app/routes/index.tsx — TanStack Start 1.0 + TanStack Router 1.135
// 视口上方路由的选择性水合默认开启。
import { createFileRoute } from "@tanstack/react-router";

export const Route = createFileRoute("/")({
  component: LandingPage,
  loader: async () => {
    // Server function 运行在 Worker 中,而非浏览器中
    return await fetchPricing();
  },
  // 将视口外路由的水合推迟到交互触发
  defer: {
    enable: true,
  },
});

function LandingPage() {
  const pricing = Route.useLoaderData();
  return (
    <main>
      <h1>{pricing.headline}</h1>
      {/* 屏幕外卡片在交互时再水合 */}
      <Suspense fallback={<Skeleton />}>
        <PricingCards data={pricing.cards} />
      </Suspense>
    </main>
  );
}

在 v1.135 之前,这需要一个自定义水合边界;现在默认开启。此方案适用于内容密集的落地页,但不适用于高度交互的单页应用——后者从相反的策略(积极水合)中受益更多。

限制 WEU 收益的 TTFB 悬崖

WEU 上 12% 的 LCP 收益对比 ENAM 上 47% 的 LCP 收益,这是意想不到的发现。根据 Cloudflare Workers 文档,每个 Worker 在空闲后首次请求都有"冷启动"成本。在 ENAM 上,我的 Worker fleet 平均冷启动 18ms。在 WEU 上,更小的 fleet 平均冷启动 49ms。Vite SPA 根本没有 Worker——每个页面都是 CDN 边缘静态 HTML。

导致混乱的数据

直到第 6 周我才拿到的数据:我的 WEU Worker fleet 是按 ENAM 18ms 冷启动配置的,但 Cloudflare 的自动扩缩在不同地区表现不同。修复方案是在 wrangler.toml 中加两行:

toml
# wrangler.toml — TanStack Start 1.0.0 / Cloudflare Workers 2026.1.0
# 改之前,WEU p75 冷启动为 49ms
[placement]
mode = "smart" # 根据流量模式分配实例

优化前: WEU p75 冷启动 49ms,p75 LCP 1.74s。优化后: WEU p75 冷启动 28ms,p75 LCP 1.53s。21ms 冷启动收益转化为 WEU 上 12% 的 LCP 改善,因为 TTFB 和 LCP 高度相关。此方案适用于 Workers + TanStack Start,但不适用于 Node.js 部署——placement 指令是 Cloudflare 专用的。

何时选哪个:诚实的决策矩阵

问题不是"哪个更快",而是"在你的工作负载下哪个更快"。基于 91 天的数据,以下是诚实的矩阵:

工作负载Vite SPATanStack Start胜出方
单一区域 SaaS(<500ms RTT)良好更优TanStack Start
多区域 SaaS,图片密集可接受最优(配合 Image Resizing)TanStack Start
单页应用,无需 SEO更优杀鸡用牛刀Vite SPA
SEO 落地页,低流量可接受更优TanStack Start
SEO 落地页,突发流量风险高最优(流式渲染 + 缓存)TanStack Start
内部工具,无需 SEO最优杀鸡用牛刀Vite SPA

更多框架选型和 Bundle 体积的完整讨论,参见 Vite vs TanStack Start:何时升级实战笔记 和 TanStack vs Next.js 2026 SEO 生产对决 数据。


利益披露: 与所评测工具无任何实质利益关系。Cloudflare Workers、Vite 和 TanStack Start 均按标价购买。本报告中的 SaaS 应用运行在 TanStack Ship 上,这是我维护的生产级 starter——当前构建配置见定价页。测试环境为 Vite 6.0 + TanStack Start 1.0.0,2026 Q1。实际结果因地区、图片流水线和第三方标签数量而异。