从 Vite SPA 到 TanStack Start SSR:一次全栈框架迁移的踩坑
为什么要从 SPA 切到 SSR
纯 Vite + React 的 SPA 开发体验很好,但有两个硬伤一直绕不开:首屏白屏时间随包体增大而变长,以及 SEO 几乎为零——搜索引擎拿到的是空 div。当项目里出现"内容要给外部看到"的页面(官网、帮助中心、可被收录的列表),SSR 就成了必选项。
我选 TanStack Start 而不是 Next.js,是因为它和我们已经用的 TanStack Router 是一套体系,路由、数据加载、SSR 可以打通,心智负担小。
第一个坑:服务端和客户端的边界
SPA 里所有代码默认都在浏览器跑,随便用 window、document 都没事。SSR 下组件会在服务端先渲染一遍,这段环境里没有 window。最常见的崩溃是模块顶层直接读 localStorage,或者服务里直接操作 DOM。
// 错误:服务端渲染时 window 不存在
const token = window.localStorage.getItem("token");
// 正确:放到 effect 或只在客户端执行的分支里
useEffect(() => {
const token = window.localStorage.getItem("token");
if (token) connect(token);
}, []);
原则很简单:任何带浏览器全局对象的逻辑,要么进 useEffect,要么用 "use client" 之类边界隔离,别让它跑到服务端。
第二个坑:数据获取要交给 loader
SPA 习惯在组件里 useEffect 发请求,SSR 下这会导致"先渲染空壳再补数据",丧失 SSR 意义。TanStack Start 用 route loader 在服务端就把数据取好,跟着 HTML 一起到客户端。
export function loader() {
return fetchPosts(); // 服务端执行,结果注入 HTML
}
export default function Posts({ props }: { props: { data: Post[] } }) {
return <ul>{props.data.map((p) => <li key={p.id}>{p.title}</li>)}</ul>;
}
第三个坑:部署与 CORS
我们前端部署在 EdgeOne Pages,后端是独立的 Laravel(api.yfsns.cn),跨域是绕不开的。SSR 的 server 端请求走的是服务端网络,不经过浏览器,所以"前端调后端"其实分两种:服务端 loader 里直接 fetch 后端域名(不带 Origin,无需 CORS);浏览器里客户端组件再请求时,才需要后端配 CORS 白名单。两种场景别混为一谈,否则会卡在 CORS 上反复排查。
小结
从 SPA 到 SSR 最大的成本不是写代码,而是重新理清"哪些逻辑在服务端、哪些在客户端"。边界想清楚,TanStack Start 的 loader + Router 组合能让首屏和 SEO 一步到位,迁移过程比想象中顺。
test1
文章作者
为什么要从 SPA 切到 SSR 纯 Vite + React 的 SPA 开发体验很好,但有两个硬伤一直绕不开:首屏...
- 分类
- 技术
- 发布时间
- 2026-09-11
- 字数
- 约 1375 字
- 阅读
- 0 次
