07-SSR水合问题实战排查与修复记录
发布时间:2026/8/30 1:04:19 作者:尧图编辑部 阅读量:1,286

SSR 水合问题实战排查与修复记录2026/8/22背景项目B站风格视频平台Nuxt 3 Vue 3 Go 微服务架构在开发过程中浏览器控制台反复出现水合警告hydration mismatch warnings最初以为是 SSR 框架的问题一度想放弃 SSR 转纯 CSR。经过几轮排查发现根源非常集中修复成本很低。症状[Vue warn]: Hydration completed but contains mismatches.伴随现象页面闪烁先显示未登录再变成已登录部分交互失效按钮点击无反应控制台警告不断排查过程第一轮怀疑 SSR 本身最初认为 SSR 是问题根源考虑全面关闭 SSR 转 CSR。但查阅 B 站官方技术博客后发现B 站 2021 年重构选择的是Vue 3 SSR/CSR 混合方案首页和 Tag 页 SSR 支撑千万级流量。SSR 不是问题问题在具体实现。第二轮定位到 localStorage通过全局搜索safeStorage.getItem、window.、document.等浏览器 API发现所有水合警告的根因是同一个模式// 错误写法顶层执行SSR 时无法访问constuserJSON.parse(safeStorage.getItem(user)||{})consttokensafeStorage.getItem(token)SSR 时服务端没有localStoragesafeStorage返回null。服务端渲染出未登录状态客户端水合时发现应该是已登录两边不一致Vue 报错。第三轮统计影响范围结果出乎意料——真正有问题的只有 9 处集中在 5 个文件文件行数问题VideoPlayer.vue87, 106字幕设置读 localStorageCollectionDetailView.vue17user 对象顶层读取CollectionListView.vue13, 15user 对象顶层读取CollectionEditView.vue19user 对象顶层读取UserProfileView.vue45, 49user 对象顶层读取api/aiSummary.ts8window.location.origin 顶层引用其余所有safeStorage调用都在onMounted、事件处理函数、watch回调中这些只在客户端执行不会导致水合问题。修复方案方案选择业界有三种主流方案ClientOnly包裹— 简单但粗暴会导致 SSR 内容空白闪烁onMounted延迟读取— 常用但无法解决首屏一致性useCookie替代 localStorage— Nuxt 官方推荐SSR 原生支持最终选择方案 3因为 Cookie 是 HTTP 请求的一部分服务端和客户端都能读取。具体实现Go 后端改动在所有登录/注册/刷新 token 的接口中增加Set-Cookie响应头func(h*UserExtendHandler)setSessionCookies(w http.ResponseWriter,token,refreshTokenstring,userIDint64,nickname,avatarstring){http.SetCookie(w,http.Cookie{Name:token,Value:token,Path:/,MaxAge:86400*7,HttpOnly:false,SameSite:http.SameSiteLaxMode,})http.SetCookie(w,http.Cookie{Name:refresh_token,Value:refreshToken,Path:/,MaxAge:86400*30,HttpOnly:false,SameSite:http.SameSiteLaxMode,})userJSON,_:json.Marshal(...)http.SetCookie(w,http.Cookie{Name:user_info,Value:url.QueryEscape(string(userJSON)),Path:/,MaxAge:86400*7,HttpOnly:false,SameSite:http.SameSiteLaxMode,})}注意user_info是 JSON 字符串含双引号必须用url.QueryEscape编码后才能写入 Cookie。前端 useAuth 组合式函数exportconstuseAuth(){constcookieTokenuseCookiestring|null(token,{default:()null})constcookieUserRawuseCookiestring|null(user_info,{default:()null})constusercomputed((){if(cookieUserRaw.value){try{returnJSON.parse(decodeURIComponent(cookieUserRaw.value))}catch{returnnull}}// 客户端兜底Cookie 不存在时读 localStorage兼容旧会话if(import.meta.client)returngetStoredUser()returnnull})// ...return{token,refreshToken,user,isLoggedIn}}useCookie在 SSR 时从请求的Cookie头读取在客户端从document.cookie读取同一个值水合一致。前端 auth.ts 同步exportfunctionsetAuthSession(session){// 同时写入 localStorage兼容旧代码和 Cookie供 SSR 读取safeStorage.setItem(TOKEN_KEY,session.token)setClientCookie(token,session.token,86400*7)// ... 同样处理 refresh_token 和 user_info}遗留问题VideoPlayer.vue的字幕设置subtitleEnabled、subtitleSettings是纯客户端偏好SSR 阶段不需要参与。处理方式用默认值初始化然后在import.meta.client块中从 localStorage 读取覆盖。总结水合问题不是 SSR 的锅是代码放错位置。核心教训SSR 页面的顶层不要读浏览器 APIlocalStorage、window、documentCookie 是 SSR 安全的状态存储方案useCookie天然跨端一致问题范围比想象的小——9 处代码修复所有页面 SSR 正常