🗒️NextJS 夜间模式:我如何从 Cookie 和 localStorage 看懂 SSR 边界

这是我重写 NextJS 夜间模式旧文后的复盘:主题切换不是只存一个开关,真正要处理的是服务端渲染、客户端状态和首次加载闪烁之间的边界。
NextJS 夜间模式:我如何从 Cookie 和 localStorage 看懂 SSR 边界

NextJS 夜间模式:我如何从 Cookie 和 localStorage 看懂 SSR 边界

我以前在 React 和 NextJS 项目里做夜间模式,最开始想得很简单:用户点一下按钮,把主题保存起来,下次打开继续使用。
真正写到 NextJS 里,问题才浮出来。
浏览器里的 localStorage 只能在客户端读取。服务端渲染阶段没有 window,也拿不到浏览器本地状态。Cookie 可以随请求到服务端,但静态生成页面又不是每次都有请求上下文。
这件小事让我第一次很具体地感受到:主题切换不是一个按钮,而是 SSR、客户端状态和首次渲染体验之间的边界问题。

我先区分 Cookie 和 localStorage

如果只是客户端交互,localStorage 很顺手:
但它的问题也明显:服务端读不到。
Cookie 不一样。请求页面时,浏览器会把 Cookie 带给服务端,所以服务端有机会在首次渲染时知道用户偏好。
我现在会这样理解:
  • localStorage 更适合纯客户端状态。
  • cookie 更适合服务端也需要知道的偏好。
  • system preference 可以作为没有用户选择时的默认值。
夜间模式刚好卡在中间:它既是用户偏好,又会影响首次渲染的视觉。

旧方案的问题

旧文里我的方案偏向客户端处理:页面渲染后,通过 useEffect 读取 Cookie,再给外层 DOM 添加主题 class。
简化后大概是:
这个思路能工作,但它有一个体验问题:页面首次加载时可能先显示默认主题,然后再切换到用户主题。
也就是常见的闪烁。
当项目只是个人博客时,这可以接受;当站点更重、用户访问更频繁时,我会希望首次 HTML 就带着正确主题。

我现在的判断

如果重新实现,我会把主题分成三层:
  1. 服务端默认主题:从 Cookie 读取,写到 HTML 根节点。
  1. 客户端实时切换:按钮点击后更新 DOM 状态。
  1. 持久化偏好:同步写入 Cookie,必要时也写 localStorage。
核心不是选 Cookie 还是 localStorage,而是看哪一层需要这个状态。
如果服务端要参与首次渲染,就不要只依赖 localStorage。

一个更清晰的结构

在现代 NextJS 项目里,我倾向于让 HTML 根节点尽早拿到主题。
思路可以是:
然后把主题写到外层:
客户端按钮只负责切换和保存:
具体代码会随 NextJS 版本变化,但边界不变:首次渲染靠服务端可见状态,交互切换靠客户端状态。

我的检查清单

我现在做主题系统,会检查这些问题:
  • 首次打开页面是否闪烁。
  • 刷新页面后主题是否保持。
  • 新标签页打开是否继承偏好。
  • 系统深浅色变化是否有默认策略。
  • 服务端渲染和客户端 hydration 是否一致。
  • 用户手动选择是否高于系统默认。
这张清单比某一段代码更重要。
因为 NextJS 的 API 会变,React 的写法会变,但主题系统的问题一直差不多:状态在哪一层产生,哪一层读取,哪一层负责持久化。

我从夜间模式里学到的工程边界

夜间模式看起来是小功能,但它把前端工程里很关键的边界都暴露出来了。
服务端不知道浏览器状态,客户端无法影响已经发出的 HTML,Cookie 可以跨两边,但也有时效和安全策略。
我现在写这类功能,不会先问“有没有现成代码”,而是先问:
这个状态第一次在哪里被需要?
只要这个问题想清楚,Cookie、localStorage、React state、服务端渲染之间的关系就会清晰很多。
上一篇
查理·芒格的智慧箴言录
下一篇
MacOS Big Sur 下使用CVS版本控制
Loading...