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