深入理解 CSS Houdini
Paint API、Layout API、Properties API、Animation Worklet 与 Typed OM 完整解析
什么是 CSS Houdini?
定义:CSS Houdini 是一组底层浏览器 API 的统称,它让开发者可以直接介入 CSS 引擎的渲染管线——包括解析、级联、布局、绘制和合成五个阶段——从而自定义浏览器原本不可触及的渲染行为。名字取自魔术师 Houdini,寓意"揭开 CSS 的黑盒"。
涉及场景:
- 自定义属性动画:
@property注册类型化的自定义属性,实现渐变角度、颜色等原本无法动画的过渡 - 自定义绘制效果:Paint API 用 Canvas 绘制 CSS 背景(波浪、棋盘、粒子)
- 自定义布局算法:Layout API 实现原生不支持的布局方式(瀑布流 Masonry)
- 高性能动画:Animation Worklet 在合成线程运行动画,不受主线程卡顿影响
- 结构化样式操作:Typed OM 替代字符串式的
element.style操作
作用:
- 突破 CSS 能力边界:不再受限于浏览器内置的属性和布局,可自定义扩展
@property已可生产使用:让自定义属性支持类型、默认值和动画,是最实用的 Houdini API- 性能提升:Worklet 运行在独立线程,不阻塞主线程渲染
- 前瞻性知识:理解渲染管线各阶段的可编程性,有助于深入理解浏览器工作原理
CSS 渲染管线(Houdini 可介入的环节):
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ 解析 │→│ 级联 │→│ 布局 │→│ 绘制 │→│ 合成 │
│ Parser │ │ Cascade │ │ Layout │ │ Paint │ │ Composite│
│ API │ │ │ │ API │ │ API │ │ Worklet │
└──────────┘ └──────────┘ └──────────┘ └──────────┘ └──────────┘
↑ ↑ ↑ ↑
Properties & Layout Paint Animation
Values API Worklet Worklet Worklet1. CSS Properties and Values API(@property)
最成熟的 Houdini API,所有现代浏览器已支持。
css
/* CSS 中注册自定义属性 */
@property --gradient-angle {
syntax: '<angle>'; /* 值类型 */
initial-value: 0deg; /* 默认值 */
inherits: false; /* 是否继承 */
}
/* 现在可以对自定义属性做动画了! */
.gradient-box {
--gradient-angle: 0deg;
background: linear-gradient(var(--gradient-angle), red, blue);
transition: --gradient-angle 1s ease;
}
.gradient-box:hover {
--gradient-angle: 360deg;
/* 渐变角度平滑过渡!普通 CSS 变量做不到 */
}
/* 用 @keyframes 做旋转渐变 */
@keyframes rotate-gradient {
to { --gradient-angle: 360deg; }
}
.animated-gradient {
animation: rotate-gradient 3s linear infinite;
}javascript
// JavaScript 中注册
CSS.registerProperty({
name: '--my-color',
syntax: '<color>',
initialValue: '#000000',
inherits: false
});syntax 支持的类型
css
/* 基本类型 */
syntax: '<length>'; /* 10px, 2em, 3rem */
syntax: '<number>'; /* 0.5, 42 */
syntax: '<percentage>'; /* 50% */
syntax: '<length-percentage>'; /* 10px 或 50% */
syntax: '<color>'; /* red, #fff, rgb() */
syntax: '<image>'; /* url(), gradient() */
syntax: '<url>'; /* url(...) */
syntax: '<integer>'; /* 1, 2, 3 */
syntax: '<angle>'; /* 45deg, 1rad */
syntax: '<time>'; /* 1s, 200ms */
syntax: '<resolution>'; /* 96dpi, 2dppx */
syntax: '<transform-function>'; /* rotate(), scale() */
syntax: '<custom-ident>'; /* 自定义标识符 */
/* 组合 */
syntax: '<color>#'; /* 逗号分隔的颜色列表 */
syntax: '<length>+'; /* 空格分隔的长度列表 */
syntax: '<color> | <url>'; /* 颜色或URL */
syntax: '*'; /* 任意值(通用) */@property 实战
css
/* 颜色渐变动画 */
@property --color-start {
syntax: '<color>';
initial-value: #ff0000;
inherits: false;
}
@property --color-end {
syntax: '<color>';
initial-value: #0000ff;
inherits: false;
}
.card {
background: linear-gradient(135deg, var(--color-start), var(--color-end));
transition: --color-start 0.5s, --color-end 0.5s;
}
.card:hover {
--color-start: #00ff00;
--color-end: #ff00ff;
}
/* 数字计数器动画 */
@property --num {
syntax: '<integer>';
initial-value: 0;
inherits: false;
}
.counter {
--num: 0;
animation: count 2s forwards;
counter-reset: num var(--num);
}
.counter::after {
content: counter(num);
}
@keyframes count {
to { --num: 100; }
}
/* 饼图进度 */
@property --progress {
syntax: '<percentage>';
initial-value: 0%;
inherits: false;
}
.pie {
--progress: 0%;
background: conic-gradient(
#4caf50 var(--progress),
#e0e0e0 var(--progress)
);
border-radius: 50%;
transition: --progress 1s ease;
}
.pie.loaded {
--progress: 75%;
}2. CSS Paint API(Paint Worklet)
在自定义 Worklet 中用 Canvas API 绘制 CSS 背景。
javascript
// paint-worklet.js
class CheckerboardPainter {
// 声明需要监听的 CSS 属性
static get inputProperties() {
return ['--checker-size', '--checker-color1', '--checker-color2'];
}
// 声明接受的参数(paint函数参数)
static get inputArguments() {
return ['<length>'];
}
paint(ctx, size, properties, args) {
const cellSize = parseInt(properties.get('--checker-size')) || 20;
const color1 = properties.get('--checker-color1')?.toString() || '#fff';
const color2 = properties.get('--checker-color2')?.toString() || '#000';
const cols = Math.ceil(size.width / cellSize);
const rows = Math.ceil(size.height / cellSize);
for (let row = 0; row < rows; row++) {
for (let col = 0; col < cols; col++) {
ctx.fillStyle = (row + col) % 2 === 0 ? color1 : color2;
ctx.fillRect(col * cellSize, row * cellSize, cellSize, cellSize);
}
}
}
}
registerPaint('checkerboard', CheckerboardPainter);css
/* 注册 worklet */
/* <script> CSS.paintWorklet.addModule('paint-worklet.js'); </script> */
.element {
--checker-size: 20;
--checker-color1: #f0f0f0;
--checker-color2: #ccc;
background-image: paint(checkerboard);
}Paint API 实战:波浪边框
javascript
// wave-painter.js
class WavePainter {
static get inputProperties() {
return ['--wave-amplitude', '--wave-frequency', '--wave-color', '--wave-offset'];
}
paint(ctx, size, properties) {
const amplitude = parseFloat(properties.get('--wave-amplitude')) || 10;
const frequency = parseFloat(properties.get('--wave-frequency')) || 4;
const color = properties.get('--wave-color')?.toString() || 'blue';
const offset = parseFloat(properties.get('--wave-offset')) || 0;
ctx.fillStyle = color;
ctx.beginPath();
ctx.moveTo(0, size.height);
for (let x = 0; x <= size.width; x++) {
const y = size.height - amplitude -
amplitude * Math.sin((x / size.width * frequency * Math.PI * 2) + offset);
ctx.lineTo(x, y);
}
ctx.lineTo(size.width, size.height);
ctx.closePath();
ctx.fill();
}
}
registerPaint('wave', WavePainter);css
@property --wave-offset {
syntax: '<number>';
initial-value: 0;
inherits: false;
}
.wave-section {
--wave-amplitude: 20;
--wave-frequency: 3;
--wave-color: #2196f3;
--wave-offset: 0;
background-image: paint(wave);
animation: wave-move 3s linear infinite;
}
@keyframes wave-move {
to { --wave-offset: 6.28; }
}3. CSS Typed OM
CSS 值的结构化表示,替代字符串操作。
javascript
// 传统方式(字符串)
element.style.width = '100px';
getComputedStyle(element).width; // '100px'(字符串)
// Typed OM(结构化对象)
element.attributeStyleMap.set('width', CSS.px(100));
element.computedStyleMap().get('width');
// CSSUnitValue { value: 100, unit: 'px' }
// CSS 数值工厂函数
CSS.px(100); // CSSUnitValue { value: 100, unit: 'px' }
CSS.em(2); // CSSUnitValue { value: 2, unit: 'em' }
CSS.rem(1.5); // CSSUnitValue { value: 1.5, unit: 'rem' }
CSS.percent(50); // CSSUnitValue { value: 50, unit: 'percent' }
CSS.deg(45); // CSSUnitValue { value: 45, unit: 'deg' }
CSS.s(1); // CSSUnitValue { value: 1, unit: 's' }
CSS.ms(300); // CSSUnitValue { value: 300, unit: 'ms' }
CSS.number(42); // CSSUnitValue { value: 42, unit: 'number' }
CSS.vw(100); // CSSUnitValue { value: 100, unit: 'vw' }
// 数学运算
const sum = CSS.px(10).add(CSS.px(20)); // CSSMathSum
const product = CSS.px(10).mul(2); // 20px
// 设置多个属性
element.attributeStyleMap.set('transform',
new CSSTransformValue([
new CSSTranslate(CSS.px(100), CSS.px(200)),
new CSSRotate(CSS.deg(45)),
new CSSScale(CSS.number(1.5), CSS.number(1.5))
])
);
// 读取所有属性
for (const [prop, val] of element.computedStyleMap()) {
console.log(prop, val.toString());
}4. Layout API(Layout Worklet)
自定义布局算法(实验性,Chrome 支持)。
javascript
// masonry-layout.js
class MasonryLayout {
static get inputProperties() {
return ['--masonry-gap', '--masonry-columns'];
}
async intrinsicSizes() {}
async layout(children, edges, constraints, styleMap) {
const gap = parseInt(styleMap.get('--masonry-gap')) || 10;
const columns = parseInt(styleMap.get('--masonry-columns')) || 3;
const columnWidth = (constraints.fixedInlineSize - gap * (columns - 1)) / columns;
const columnHeights = new Array(columns).fill(0);
const childFragments = await Promise.all(
children.map(async (child) => {
const fragment = await child.layoutNextFragment({
fixedInlineSize: columnWidth
});
// 找最短列
const shortest = columnHeights.indexOf(Math.min(...columnHeights));
fragment.inlineOffset = shortest * (columnWidth + gap);
fragment.blockOffset = columnHeights[shortest];
columnHeights[shortest] += fragment.blockSize + gap;
return fragment;
})
);
return {
childFragments,
autoBlockSize: Math.max(...columnHeights) - gap
};
}
}
registerLayout('masonry', MasonryLayout);css
.masonry-container {
display: layout(masonry);
--masonry-columns: 3;
--masonry-gap: 16;
}5. Animation Worklet
在合成线程中运行动画,不受主线程卡顿影响(实验性)。
javascript
// animator-worklet.js
class ScrollAnimator {
constructor(options) {
this.rate = options.rate || 1;
}
animate(currentTime, effect) {
// 在合成线程中执行,不阻塞主线程
effect.localTime = currentTime * this.rate;
}
}
registerAnimator('scroll-animator', ScrollAnimator);javascript
// 主线程注册
await CSS.animationWorklet.addModule('animator-worklet.js');
const animation = new WorkletAnimation(
'scroll-animator',
new KeyframeEffect(element, [
{ transform: 'translateY(0)' },
{ transform: 'translateY(-500px)' }
], { duration: 1, fill: 'both' }),
document.timeline,
{ rate: 0.5 }
);
animation.play();浏览器支持现状
| API | Chrome | Firefox | Safari |
|---|---|---|---|
| @property | ✅ 85+ | ✅ 128+ | ✅ 15.4+ |
| Paint API | ✅ 65+ | ❌ | ❌ |
| Typed OM | ✅ 66+ | ❌ | ❌ |
| Layout API | ⚠️ 实验性 | ❌ | ❌ |
| Animation Worklet | ⚠️ 实验性 | ❌ | ❌ |
@property 已经可以安全用于生产环境,Paint API 在 Chromium 系浏览器中可用。
总结
CSS Houdini 核心知识点:
┌──────────────────────────────────────────────────────────┐
│ @property(生产可用 ✅) │
│ • 注册自定义属性的类型、默认值、继承性 │
│ • 核心价值:让自定义属性支持过渡和动画 │
│ • 应用:渐变动画、数字计数、饼图进度 │
├──────────────────────────────────────────────────────────┤
│ Paint API(Chromium 可用) │
│ • 用 Canvas API 绘制 CSS 背景 │
│ • 在独立的 Worklet 线程中运行 │
│ • 应用:自定义图案、波浪效果、粒子背景 │
├──────────────────────────────────────────────────────────┤
│ CSS Typed OM(Chromium 可用) │
│ • CSS 值的结构化表示(替代字符串操作) │
│ • CSS.px() / CSS.em() 等工厂函数 │
│ • 支持数学运算 add/sub/mul/div │
├──────────────────────────────────────────────────────────┤
│ Layout API / Animation Worklet(实验性) │
│ • 自定义布局算法 / 合成线程动画 │
│ • 目前不建议生产使用 │
└──────────────────────────────────────────────────────────┘