:defined 是唯一能可靠判断自定义元素是否完成注册并升级的 css 伪类,它绑定 custom elements 生命周期,未调用 customelements.define() 则永不匹配,可精准防 fouc。

:defined 是唯一能可靠判断自定义元素是否完成注册并升级的 CSS 伪类,它直接绑定 Custom Elements 生命周期,不是“写了标签就能用”的开关——没调用 customElements.define(),:defined 永远不匹配。
为什么 :defined 能防 FOUC(无样式内容闪烁)
浏览器解析 CSS 是同步的,但 customElements.define() 是异步执行的。如果样式表提前加载,而组件 JS 还没注册完成,<my-button></my-button> 就会以裸标签形式渲染,没有样式、没有布局、甚至可能塌陷成一行空白。
- 常见错误现象:页面刚打开时看到一堆未样式化的
<my-card></my-card>占位块,0.1~0.3 秒后才“啪”一下弹出完整样式 - 根本原因:CSS 规则如
my-card { width: 200px; }在元素未升级前就已应用,但此时它只是个未知 HTML 元素,浏览器按未知标签默认样式渲染(display: inline) -
:defined把样式生效时机卡在“注册完成且升级完毕”这一确切节点,比任何 JS 标记(如加 class)都精准
:not(:defined) 必须配合使用,否则占位失效
只写 my-card:defined 不够——你得先给未定义状态留出合理占位,否则用户看到的是空白或错位布局。
- 正确姿势:
my-card:not(:defined) { display: block; min-height: 48px; background: #f0f0f0; } - 不能只靠 JS 插入 loading class:class 可被误删、覆盖、延迟添加;
:not(:defined)是浏览器原生保障,无需 JS 干预 - 注意选择器顺序:
my-card:not(:defined)应写在my-card:defined之前,否则后者权重更高会覆盖占位样式 - 别用
visibility: hidden或opacity: 0做占位:它们不占空间,会导致后续内容上移,重排更明显
哪些情况会让 :defined 失效
它不是魔法,依赖三个硬性前提,缺一不可。
- 标签名必须含短横线(如
my-button),button或MyButton都不合法 -
customElements.define('my-button', MyButton)必须成功执行,控制台报错(如 “Failed to define 'my-button'”)意味着它永远不匹配 - CSS 必须在
内或至少在注册语句之后加载;若 CSS 放且 JS defer,大概率错过 - Safari 对
:defined支持稳定,但若用了is=""属性语法(如<div is="my-button">),Safari 不识别其 <code>:defined状态,需改用<my-button></my-button>标签形式:defined不解决样式穿透,Shadow DOM 才是搭档:defined只管“什么时候开始应用样式”,不管“样式作用范围”。它本身不创建作用域,也不阻止外部 CSS 泄漏进来。- 如果你写
my-dialog:defined h2 { color: blue; },而页面其他地方有h2 { color: red; },后者依然会生效——除非你在<my-dialog></my-dialog>内部用 Shadow DOM 封装 - 文字颜色、字体、行高等继承属性仍会穿透,
:defined无法阻断,得靠组件内部all: initial或显式重置 - 超过 50 个自定义组件同时用
:defined,频繁动态插入/移除节点时,浏览器要逐个检查定义状态,可能引发 layout thrashing,这时该考虑按需注册或分组懒加载
真正容易被忽略的点是:它和 JS 注册时机的耦合比想象中更紧——哪怕只差几毫秒,FOUC 就可能发生;而一旦用了
:defined,就再也不能靠 class 切换来模拟“加载中”状态了,因为那个 class 本身无法表达“是否已升级”这个底层事实。 - 如果你写











