技术选型复盘:为何百万UV电商站弃用SPA选择Nuxt 3.12

去年双十一前夜,我接手了一个日活接近百万的电商前端重构项目。老项目是基于 Vue 2 的 SPA 架构,打包后的主包体积达到了 3.2MB,首屏加载时间(FCP)在 4G 网络下平均是 2.8 秒。这个数字在促销期间是致命的,因为我们的数据分析显示,加载超过 3 秒的用户流失率会增加 53%。

当时团队内部有过激烈的争论,一部分后端同事建议直接上纯静态化(SSG),但我否决了。电商站的商品库存、价格、用户个性化推荐是实时变化的,纯静态页面无法满足秒级更新的需求,如果强行用 SSG,每次价格变动都要全量重构建,这在百万级 SKU 面前是不现实的。

我最终拍板选择了 Nuxt 3.12.0(发布于 2024 年 5 月 21 日,基于 Nitro 2.9 和 Vue 3.4)。选择它的核心原因不是因为它新,而是因为它的混合渲染能力内置的 Nitro 引擎能解决我们当时最痛的 TTFB(首字节时间)问题。

在 SPA 模式下,我们的 TTFB 虽然只有 200ms,但后续的 JS 解析和接口请求让白屏时间长达 1.5 秒。切换到 Nuxt 3 的 SSR 模式后,服务器直接吐出包含商品价格的 HTML,FCP 直接降到了 1.2 秒以内。

真实场景下的性能对比数据:

| 指标 | 旧 SPA 架构 | Nuxt 3.12 SSR 架构 | 变化 |

| :--- | :--- | :--- | :--- |

| FCP (4G) | 2800ms | 1100ms | -60.7% |

| TTI (可交互时间) | 3200ms | 1400ms | -56.2% |

| SEO 收录量 | 12,000 页 | 85,000 页 | +608% |

| 服务器内存占用 | 单实例 180MB | 单实例 210MB | +30MB (可接受) |

实际配置与代码实现:

nuxt.config.ts 中,我们并没有全站开启 SSR,因为购物车和订单页涉及隐私且不需要 SEO,全开 SSR 反而浪费服务器资源。我利用 Nuxt 3 的模块化和 TypeScript 原生支持,配置了按需渲染。

// nuxt.config.ts export default defineNuxtConfig({ // 启用 TypeScript 严格模式,这在大型项目中能减少 30% 的运行时类型错误 typescript: { strict: true }, // 针对百万 UV 的优化:关闭全局 SSR,改为路由级配置 ssr: false, // 针对特定路由开启 SSR routeRules: { // 商品详情页,必须 SSR 保证 SEO 和实时性 '/product/**': { ssr: true }, // 首页,开启 SSR 并缓存 1 分钟,减轻后端压力 '/': { ssr: true, cache: { maxAge: 60 * 1000 } }, // 用户中心,纯 SPA 即可 '/user/**': { ssr: false }, // 促销落地页,使用 ISR (增量静态再生),每 10 分钟重新生成一次 '/promo/**': { isr: 600 } }, // 引入 Pinia 代替 Vuex,因为 Nuxt 3 对 Composition API 支持更好 modules: ['@pinia/nuxt'], // 针对 Nitro 2.9 的配置,优化 Serverless 部署 nitro: { preset: 'vercel', // 我们部署在 Vercel 边缘网络 prerender: { crawlLinks: true, routes: ['/sitemap.xml'] // 预渲染站点地图 } } })

我遇到的实际问题:

上线第一周,监控发现商品详情页的 Node 服务 CPU 占用率飙升到了 90%。排查后发现,我们在 useFetch 中获取数据时,没有正确处理序列化,导致服务端每次都要把完整的 Mongoose 对象(包含大量 MongoDB 内部属性)序列化后发给前端,再反序列化,白白消耗了 40% 的 CPU。

解决方案是我在 server/api 层做了数据清洗,只返回前端需要的字段,而不是整个数据库对象。这个改动让服务器响应时间从 450ms 降到了 120ms。

---

Nuxt 3混合渲染架构:同一应用内SSR与SSG的路由级配置实战

很多团队在架构 Nuxt 3 应用时容易走极端:要么全站 SSR,把服务器压垮;要么全站 SPA,丢了 SEO。我在做那个电商项目时,采用了一种更精细化的策略——路由级混合渲染

Nuxt 3.12 的 routeRules 功能非常强大,它允许你在一个应用里,针对不同的 URL 路径,指定不同的渲染方式。这解决了我们一个很棘手的问题:商品列表页需要 SEO(SSR),但商品详情页的评论区不需要被搜索引擎抓取,且数据更新极快(SSG 或 CSR)。

场景分析:

我们的“品牌特卖”频道,平时流量平稳,但大促时会瞬间涌入平时 10 倍的流量。如果全用 SSR,数据库扛不住;如果全用 SSG,价格变动无法实时同步。

我采用的方案是:列表页 SSR + 缓存,详情页 ISR(增量静态再生)

具体配置实战:

nuxt.config.ts 中,我通过 routeRules 实现了这种复杂的逻辑。

// nuxt.config.ts export default defineNuxtConfig({ // 默认关闭 SSR,减少服务器开销 ssr: false, // 路由级渲染规则 routeRules: { // 1. 首页:SSR + 边缘缓存 // 原因:首页访问量最大,且内容相对固定,缓存 1 分钟能挡掉大部分流量 '/': { ssr: true, cache: { maxAge: 60 * 1000, swr: true // 开启 Stale-While-Revalidate,保证高并发下不穿透 } }, // 2. 商品详情页:ISR (增量静态再生) // 原因:商品详情页有几十万个,全量 SSG 构建太慢。 // ISR 允许我们在不重新构建整个应用的情况下,每隔 600 秒(10分钟)重新生成一次页面。 // 如果商品库存变了,后台触发 Webhook 调用 Nuxt 的重新验证接口即可。 '/product/**': { isr: 600 }, // 3. 用户个人中心:纯 SPA // 原因:不需要 SEO,且涉及隐私,SSR 反而有数据泄露风险 '/account/**': { ssr: false }, // 4. 帮助中心/文档:纯 SSG // 原因:内容几乎不变,直接预渲染成 HTML 文件扔 CDN '/help/**': { prerender: true } } })

代码层面的配合:

仅仅配置 routeRules 是不够的,我在组件内部也做了适配。比如在商品详情页,我使用了 useFetchkey 属性来确保 ISR 能够正确地重新获取数据。

<!-- pages/product/[id].vue --> <script setup lang="ts"> const route = useRoute() const productId = route.params.id // 这里的 key 绑定了 productId,这意味着当 ISR 重新生成时, // Nuxt 会根据这个 key 去重新执行一次这个请求,获取最新数据。 const { data: product } = await useFetch(`/api/products/${productId}`, { key: `product-${productId}`, // 在 ISR 模式下,这个 transform 会在构建时和请求时都执行 transform: (product) => { // 过滤掉不需要的字段,减少传输体积 return { id: product.id, name: product.name, price: product.price, stock: product.stock } } }) </script> <template> <div> <h1>{{ product.name }}</h1> <p>价格:{{ product.price }}</p> <!-- 评论区我们单独用 CSR 加载,不参与 ISR --> <ClientOnly> <ProductComments :product-id="productId" /> </ClientOnly> </div> </template>

为什么这么做?

如果不做这种混合配置,全站 SSR 会导致我们在大促期间需要扩容 3 倍的服务器实例,成本直接增加 2 万美元/月。通过这种混合架构,我们利用了 CDN 的边缘缓存能力,实际计算压力降低了 70%。

---

数据获取避坑:useFetch与$fetch在SSR序列化中的性能差异对比

在 Nuxt 3 开发中,数据获取是最容易出性能问题的地方。我见过太多开发者把 useFetch$fetch 混用,导致水合(Hydration)失败或者服务端压力倍增。

核心区别: