·更新于 2026年10月25日· 8 次浏览 Nuxt 3Vue 3SSRHydration深色模式工程实践

Nuxt 3 深色模式不再闪:我用 cookie 预设主题和内联脚本稳住首屏

深色模式切换常见闪白或闪黑,根因是服务端不知道用户偏好。这里记录我把主题从 localStorage 迁到 cookie,用 SSR 首屏直出正确 class,再靠客户端增强避免水合问题的做法。

Nuxt 3 深色模式不再闪:我用 cookie 预设主题和内联脚本稳住首屏 封面
ZHOUYI

作者

ZHOUYI

之前站点深色模式有个老毛病:页面打开时先白一下,再突然变黑。体验像灯开关迟到了半拍。一开始我以为是 CSS 过渡没写好,后来发现是 SSR 首屏根本不知道用户偏好,只能先渲染默认浅色,等浏览器执行 JS 后读 localStorage 再补 class。

为什么会闪

localStorage 只在浏览器可用,服务端渲染时拿不到。Nuxt 首屏返回浅色 HTML,客户端水合后再改成深色,就会出现闪烁。更麻烦的是,如果服务端和客户端首次渲染结果不一致,还可能引发 Vue 水合警告。主题这种影响 HTML class 的状态,最好在服务端就能确定。

我的处理思路

  1. 把主题偏好写到 cookie,而不是只存 localStorage,因为 cookie 会随请求带到服务端。
  2. 服务端根据 cookie 决定 html 是否带 dark class,让首屏 HTML 直接命中最终状态。
  3. 客户端切换主题时同步更新 cookie、DOM class 和持久化状态,避免刷新后回退。
  4. 用 useHead 放一段很小的内联脚本做兜底,首次访问也可跟随系统偏好,防止 class 没跟上。

关键代码

ts
const theme = useCookie<'light' | 'dark'>('theme', { default: () => 'light', path: '/' })
const isDark = computed(() => theme.value === 'dark')
const inlineTheme = `try{var m=document.cookie.match(/(?:^|; )theme=(dark|light)/);var d=m?m[1]==='dark':matchMedia('(prefers-color-scheme: dark)').matches;document.documentElement.classList.toggle('dark',d);}catch(e){}`
useHead({ htmlAttrs: { class: isDark.value ? 'dark' : '' }, script: [{ innerHTML: inlineTheme }] })
const toggleTheme = () => { theme.value = isDark.value ? 'light' : 'dark'; if (import.meta.client) document.documentElement.classList.toggle('dark', isDark.value) }

这里 cookie 是关键:它让服务端首次渲染就有依据。内联脚本不是主逻辑,只是尽早把 class 对齐,防止缓存页或脚本顺序导致 class 没跟上。切换时不要等 mounted 再判断,直接更新 cookie 和 DOM,后续请求服务端也能拿到最新偏好。具体 useHead 写法以官方文档为准。

踩坑和小结

踩过两个坑:一是把 dark class 放在组件根节点上,受局部状态影响,不如直接放在 html 上稳定;二是只改 localStorage,结果服务端永远拿不到偏好,刷新后又闪一次。如果你的站点还有匿名缓存,记得把主题 cookie 纳入缓存策略评估,别让个性化状态混进公共缓存。主题这类影响首屏 HTML 的偏好,尽量服务端可见,客户端只做增强。

Guestbook

留言

若这篇文章也曾停在你心上,留一行字吧,我会看的。

先确认一下邮箱

多这一步,是为了少些机器骚扰,也让我能更踏实地读每一条真人写下的字。验证码 10 分钟内有效;同一邮箱 60 秒内只能发一封。

本文留言

共 0 条已加载 0 条

这里还静悄悄的——愿意做第一个留下脚印的人吗?