防闪核心是确保关键样式在首屏渲染前就位:内联关键CSS(≤10KB,置于head开头),异步CSS用media="print"+onload方案,多主题通过disabled切换而非换href,全链路协同避免FOUC。

防止异步引入 CSS 导致页面闪烁,核心是不让浏览器在首次绘制(first paint)时“裸奔”——关键样式必须在 HTML 解析阶段就位,不能等 JS 执行完、组件 mount 后才加载。异步引入本身不是问题,问题出在加载时机和注入方式。
为什么 link 动态插入一定会闪
常见错误是用 document.head.appendChild 在组件 mounted 或 useEffect 里插入 <link rel="stylesheet">。此时 HTML 已解析完毕、DOM 已构建、首屏已渲染,新样式属于“后加载”,必然触发重排重绘。
- 预加载器(preload scanner)根本看不到动态插入的
<link>,无法提前发起请求 - 浏览器不会为它阻塞渲染,但也不会让它参与首次绘制
- SSR 场景下更明显:服务端没输出这部分样式,客户端补上就是肉眼可见的跳变
media="print" + onload 是最稳的异步加载姿势
它让浏览器并行下载 CSS 文件,但不应用,直到加载完成再切换生效,避免阻塞也不引发重绘。
- 写法必须严格:
<link rel="stylesheet" href="module.css" media="print" onload="this.media='all'"> - 缺
media="print"→ 可能阻塞渲染;缺onload→ 样式永远不会生效 - 务必加
<noscript><link rel="stylesheet" href="module.css"></noscript>,否则 JS 失效时样式彻底丢失 - 适用于非首屏模块、异步组件配套样式、主题皮肤等场景
内联关键 CSS 才是防闪底线
所有“异步”方案都建立在关键样式已就位的前提下。如果首屏的 .header、.hero、字体定义、CSS 变量这些都没内联,再怎么优化异步加载也救不了 FOUC。
立即学习“前端免费学习笔记(深入)”;
- 内联位置必须在
<head>最开头,且用<style>标签,不能是data:URL 的<link> - 体积建议 ≤10KB(gzip 前),否则可能阻塞 HTML 解析本身
- 别手写维护——用
critters(Vite)、critical(CLI)或框架内置工具自动生成 - 深色模式变量也要提前写进内联
<style>,比如直接设html[data-theme="dark"]对应的:root值
多主题切换时别换 href,改 disabled
用 document.getElementById("theme-css").href = "dark.css" 这类操作,本质是卸载旧样式、加载新文件,中间必有空白期。
- 正确做法:HTML 中一次性声明所有主题
<link>,只通过disabled属性控制启用状态 - 例如:
<link id="light-theme" rel="stylesheet" href="light.css">和<link id="dark-theme" rel="stylesheet" href="dark.css" disabled> - 切换时只执行:
lightTheme.disabled = true; darkTheme.disabled = false; - 所有主题 CSS 必须能独立生效,不能依赖 JS 注入的 class 或
@media查询动态判断
真正容易被忽略的是:防闪不是“配好某个异步组件”就能解决的事,而是整个资源加载链的协同——从 HTML 结构、关键 CSS 提取、预加载策略到主题切换逻辑,任何一环断掉,FOUC 就会找上门。尤其要注意内联样式是否包含响应式断点和暗色模式规则,Coverage 面板里漏掉的绿色高亮,往往就是闪的源头。


















