Skip to content

深入理解 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 操作

作用

  1. 突破 CSS 能力边界:不再受限于浏览器内置的属性和布局,可自定义扩展
  2. @property 已可生产使用:让自定义属性支持类型、默认值和动画,是最实用的 Houdini API
  3. 性能提升:Worklet 运行在独立线程,不阻塞主线程渲染
  4. 前瞻性知识:理解渲染管线各阶段的可编程性,有助于深入理解浏览器工作原理
CSS 渲染管线(Houdini 可介入的环节):
┌──────────┐  ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌──────────┐
│  解析     │→│  级联     │→│  布局     │→│  绘制     │→│  合成     │
│ Parser   │  │ Cascade  │  │ Layout   │  │ Paint    │  │ Composite│
│ API      │  │          │  │ API      │  │ API      │  │ Worklet  │
└──────────┘  └──────────┘  └──────────┘  └──────────┘  └──────────┘
      ↑                            ↑            ↑            ↑
  Properties &              Layout       Paint      Animation
  Values API               Worklet      Worklet     Worklet

1. 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();

浏览器支持现状

APIChromeFirefoxSafari
@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(实验性)                    │
│ • 自定义布局算法 / 合成线程动画                              │
│ • 目前不建议生产使用                                        │
└──────────────────────────────────────────────────────────┘