先记住这个答案
CSS 自定义属性(--my-var 这种)在动画里频繁修改会卡,根本原因是浏览器把它当成一个「不透明的字符串」。每改一次值,所有引用这个变量的规则都得重新解析、重新计算样式。如果变量最终被用在影响布局的属性上(比如 width、margin),还会连带触发 layout。动画一秒 60 帧,等于一秒做 60 次全量计算,自然就掉帧了。
- 自定义属性改值会触发所有引用处重新计算样式
- 未注册的变量浏览器无法在合成层插值
- 用
@property声明类型后可走 GPU 合成 - 可直接动画
transform时无需变量中转
浏览器怎么处理动画属性
浏览器能高效动画的属性其实很少。transform 和 opacity 这两个属性之所以不卡,是因为浏览器知道它们只影响合成阶段,可以直接交给 GPU 处理,主线程不参与。
自定义属性不在这个「白名单」里。浏览器拿到 --color: hsl(0, 80%, 50%) 时,不知道它是颜色、角度还是长度,没法做插值计算,只能每帧拿到新的字符串后,把整条样式链路重走一遍。这和直接动画 background-color 的性能差距很大——后者至少浏览器知道是颜色,能做颜色插值,只触发 repaint 不触发 layout。
/* 用变量控制渐变角度,每帧都改变量 */
.ring {
--angle: 0deg;
background: conic-gradient(from var(--angle), #3b82f6, #ec4899, #3b82f6);
animation: spin 2s linear infinite;
}
@keyframes spin {
to { --angle: 360deg; }
/* 浏览器不知道 --angle 是角度,无法插值 */
/* 实际效果:开头 0deg,结尾 360deg,中间没有过渡 */
}这段代码在大多数浏览器里不会平滑旋转。--angle 没有被注册为角度类型,浏览器直接从 0deg 跳到 360deg,中间帧没有插值。
用 @property 注册后的变化
CSS Houdini 的 @property 规则可以告诉浏览器这个变量是什么类型。注册之后,浏览器知道它是 <angle>,就能做数值插值,把动画交给合成层处理。
/* 注册 --angle 为角度类型 */
@property --angle {
syntax: '<angle>';
inherits: false;
initial-value: 0deg;
}
.ring {
background: conic-gradient(from var(--angle), #3b82f6, #ec4899, #3b82f6);
animation: spin 2s linear infinite;
}
@keyframes spin {
to { --angle: 360deg; }
/* 注册后浏览器能插值,渐变平滑旋转 */
}注册之后同样的 @keyframes 就能平滑过渡了。浏览器知道 --angle 是角度,可以从 0deg 到 360deg 逐帧插值。打开 DevTools 的 Performance 面板能看到,注册前每帧都有 Recalculate Style,注册后这些几乎消失了。
什么时候该用变量动画
并不是说变量动画完全不能用。如果动画的目标属性没有原生的可动画版本,比如 conic-gradient 的起始角度、渐变色标位置,变量加 @property 是目前唯一的方案。
但如果能直接动画 transform、opacity、scale、rotate 这些原生属性,就不要绕道自定义属性。直接用原生属性省去了变量解析这一步,性能最好。
还有一种情况是用 JavaScript 通过 element.style.setProperty('--x', value) 在每帧更新变量。这种写法和 CSS 动画修改变量的性能问题是一样的,都会触发样式重算。如果 JS 驱动的动画只需要改位移和透明度,用 Web Animations API 直接操作 transform 更合适。
容易答错的地方
- 以为自定义属性和普通属性一样能被浏览器优化
- 没有
@property注册的自定义属性,在浏览器眼里就是字符串替换。不管变量实际存的是颜色还是角度,浏览器都只能做字符串拼接后重新解析,没法走合成层快速通道。 - 以为注册了
@property就万事大吉 @property只是让浏览器能做插值,但最终动画是否走合成层,还取决于变量被用在哪个属性上。如果变量被用在width上,即使注册了类型,改宽度还是会触发 layout。只有最终落到transform、opacity、filter这类合成层属性上,才能真正避免重排重绘。
面试官还会怎么问?
@property 的 inherits 设成 true 和 false 有什么区别?
inherits: true 表示子元素会继承父元素的变量值,这是自定义属性的默认行为。inherits: false 表示每个元素独立,子元素拿到的是 initial-value 而不是父元素的值。做动画时一般设 false,避免父元素的变量变化连带所有子元素一起重算。
怎么判断动画卡不卡?
打开 Chrome DevTools 的 Performance 面板录一段,看每帧的 Recalculate Style 和 Layout 耗时。如果绿色的 Paint 和紫色的 Layout 条很长、占了大半帧时间,说明动画在走主线程。也可以打开 Rendering 面板里的 Paint Flashing,绿色高亮的区域就是在重绘的部分。
CSS 变量能做哪些原生属性做不到的动画?
渐变色标位置的过渡、clip-path 里的百分比变化、box-shadow 的多参数联动——这些属性要么不可动画,要么需要多个值同时变。用自定义属性把它们参数化,再通过 @property 注册类型,就能做出原生属性做不到的过渡效果。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。