
现象:点一次链接,屏幕闪一下
站点默认是深色主题。用户切到浅色之后,每次点进一篇文章,页面都会先按深色画一遍,再跳回浅色——就是那一下白/黑闪烁。
用无痕窗口、清空缓存都没用,因为它不是缓存问题。
一、原因:默认值在服务端,偏好却在页脚才读
主题的配色机制是两段式的:
- 服务端按主题设置输出一个默认值:
<body data-scheme="dark">
- 前端 JS(挂在页脚)读
localStorage里的用户偏好,再改回来。
问题是:页脚 JS 执行时,首屏早就按深色绘制完了。所以每次导航都会闪一下。
这类问题有个通名:FOUC(Flash of Unstyled/Unstyled Content,样式闪动)。
二、思路:把"读偏好"提前到首屏绘制之前
要让偏好生效,就得在 <head> 里同步执行——在浏览器开始绘制之前。
但这里有个鸡生蛋问题:<head> 执行时,<body> 元素还不存在,你没法直接改它的属性。
解法是用 MutationObserver 监听 body 一被插入就动手:
<script>
( function () {
try {
var s = localStorage.getItem( 'data-scheme' );
if ( s !== 'light' && s !== 'dark' && s !== 'auto' ) { return; } // 没设置过就不管
var root = document.documentElement;
var apply = function () {
var b = document.body;
if ( ! b ) { return false; } // body 还没出现
if ( s === 'auto' ) { b.removeAttribute( 'data-scheme' ); } // 跟随系统 = 去掉属性
else { b.setAttribute( 'data-scheme', s ); }
return true;
};
if ( ! apply() ) {
// body 还没插入:一出现就立刻改
new MutationObserver( function ( records, observer ) {
if ( apply() ) { observer.disconnect(); }
} ).observe( root, { childList: true } );
document.addEventListener( 'DOMContentLoaded', apply ); // 兜底
}
} catch ( e ) {} // localStorage 被禁用时不能报错中断
} )();
</script>
三个细节:
try/catch不能省:隐私模式下访问localStorage可能直接抛异常,不包住的话这段脚本会中断,后面的都不执行;auto时是"移除属性"而不是设成auto:要和主题自身的语义保持一致(跟随系统就是不加属性,让 CSS 的媒体查询生效);MutationObserver+DOMContentLoaded双保险:不同浏览器对 body 插入时机的处理略有差异。
三、顺带解决:顶部 logo 也会闪错版本
主题在深色/浅色下用的是两张不同的 logo 图(.logo-light / .logo-dark),靠 CSS 类切换。
既然我们在 body 属性被写入的瞬间就能介入,那就顺手用 CSS 按属性控制显示哪一个:
body[data-scheme="light"] .logo-dark { display: none !important; }
body[data-scheme="light"] .logo-light { display: inline-block !important; }
body[data-scheme="dark"] .logo-light { display: none !important; }
body[data-scheme="dark"] .logo-dark { display: inline-block !important; }
⚠️ 这里有个必须注意的点
CSS 必须挂在 body[data-scheme] 上,不能挂在"启动脚本写的一次性标记"上。
因为主题自己的 JS 在切换配色时改的正是 body 的 data-scheme 属性,同时给 .logo-* 增删自己的类名。
如果你把 CSS 挂在一个只写一次的标记上(比如 <html class="scheme-ready">),那 !important 会永久压住主题的类名——用户切到深色之后,logo 就再也换不回来了。这个 bug 很难当场发现,因为初始加载看起来完全正常。
四、和页面缓存兼容吗
兼容,而且是刻意设计成兼容的。
这段脚本只读 localStorage、不读 cookie。所以服务端输出的 HTML 对所有访客完全相同——不管访客选了什么配色,拿到的都是同一份缓存文件,配色在浏览器端决定。
反例:如果你的方案是"服务端读 cookie 决定输出哪种配色",那这份 HTML 就因人而异,页面缓存会直接失效(或者给用户返回别人的配色)。
主题页脚那段 JS 照旧执行,两者结果一致(幂等),不会互相打架。
五、验证
- 切到浅色 → 刷新 → 进文章时不应该闪;
- 切到深色 → 同样不应闪;
- 切一次配色,确认 logo 正确跟着换(这是第 3 节那个坑的验收点);
- 开一个无痕窗口(没有 cookie/localStorage)→ 应该正常按服务端默认值渲染,不报错。
六、回滚
删掉插件文件,恢复"先按服务端默认值绘制、页脚 JS 再切换"的原始行为。回滚后闪烁会回来——这不是故障,是变回了原状。
一句话总结:配色闪烁的本质是"默认值在服务端、偏好却在页脚才读"。解法是把读取提前到 <head> 的同步脚本里,用 MutationObserver 在 body 出现的瞬间写入属性;顺带用 body[data-scheme] 控制 logo——但千万别挂在一次性标记上,否则切回去就再也换不回来了。

评论 (0)