Nuxt 3 服务端渲染(SSR)高并发下的跨请求状态污染(Cross-Request State Pollution)与内存泄漏如何排查?

在 Nuxt 3 生产环境中,最严重的两类线上故障是 跨请求状态污染(用户 A 刷出了用户 B 的登录态或订单数据) 与 SSR 内存持续上涨直至容器 OOM 重启。深入理解 Nuxt 3 基于 unctx 的上下文隔离机制与内存泄漏排查手段是高级面试必考题。

一、 为什么在 <script setup> 之外或 Composable 顶层定义 ref 会导致跨用户串号?

在 Node.js 服务端,所有 ES Module(如 composables/useAuth.ts)在进程启动时只会被加载和执行一次。如果把 const user = ref(null) 写在模块顶层作用域,该 ref 就是所有并发 HTTP 请求共享的全局单例!

// ✗ 严重线上事故写法:模块顶层 ref 在 Node.js 进程中是全局单例,并发请求会互相覆盖!
import { ref } from 'vue'
const leakedGlobalUser = ref(null) // 危险!所有访客共用同一个引用

export function useBadAuth() {
  return { user: leakedGlobalUser }
}

// ✔ 正确写法:使用 Nuxt 3 的 useState(key, init) 或挂在每个请求独立的 Pinia 实例上
export function useSafeAuth() {
  // useState 内部通过 useNuxtApp() 将状态存储在当前请求的 nuxtApp.payload.state[key] 中,
  // 请求结束即随请求上下文一起被 GC 回收,100% 物理隔离!
  const user = useState<{ id: string; role: string } | null>('auth-user', () => null)
  return { user }
}

二、 Nuxt 3 异步上下文丢失问题(Nuxt instance is unavailable)与 unctx 原理

Nuxt 3 依赖 unctx 在同步调用栈中自动传递当前的 nuxtApp 实例。如果在 Composable 内部执行了原生 await fetch(),在 await 恢复执行后,JavaScript 调用栈已经脱离了之前的同步上下文,此时再调用 useRuntimeConfig() 或 useRouter() 就会抛出 Nuxt instance is unavailable!

// 解决 await 之后 Nuxt 上下文丢失的黄金法则:在第一个 await 之前同步获取所有组合式上下文!
export async function useOrderLoader(orderId: string) {
  // ✔ 必须在第一个 await 之前同步调用所有 use* 函数
  const config = useRuntimeConfig()
  const router = useRouter()

  const res = await $fetch(`${config.public.apiBase}/orders/${orderId}`)
  if (!res) {
    // 这里复用前面已拿到的 router 引用,安全无报错
    await router.replace('/404')
  }
  return res
}

三、 SSR 内存泄漏三步定位法

  1. 检查服务端是否意外启动了定时器或全局事件:在 SSR 期间执行的代码绝不能调用未清理的 setInterval 或向全局 process / 单例 EventEmitter 持续 on() 添加监听。
  2. 检查第三方客户端插件:将非 SSR 兼容的图表/动画插件标记为 .client.ts(如 plugins/echarts.client.ts),防止其在 Node 端模拟 DOM 堆积内存。
  3. 使用 node --inspect + autocannon 压测对比 Heap Snapshot:压测 1,000 次请求并手动触发 GC 后,在 Chrome DevTools Memory 面板按 Retained Size 排序查找未释放的 VueApp 或闭包引用。

同分类其他题目