TL

CSS Houdiniとペイント/レイアウトワークレット

CSSの黒箱だった内部を開発者が拡張できる。Typed OMで文字列パースを捨て、@propertyで型付きカスタムプロパティを宣言し、Paint/Layoutワークレットでレンダリング段を直接書く仕組みを解説します。

応用CSSHoudiniレンダリングパフォーマンスブラウザ最終更新: 2026-06-21
TL;DR要点だけ先に
  • 1.CSS Houdiniはブラウザのスタイル計算・レイアウト・ペイントの各段に低レベルなフックを公開する一連のAPI群。従来のJSはレンダリング結果(DOM/CSSOM)を文字列で叩くだけだったが、Houdiniはエンジン内部のパイプライン自体に処理を差し込む。
  • 2.Typed OMはCSS値を文字列ではなく型付きオブジェクト(CSSUnitValue等)で扱い、読み書きのたびのパース・シリアライズを省く。Properties and Values APIは @property でカスタムプロパティに型・初期値・継承を宣言し、アニメ可能化と無効値の早期排除を実現する。
  • 3.Paint/Layoutワークレットは独立した制限付きグローバルスコープでユーザーコードを実行し、ブラウザがペイント段・レイアウト段で直接呼ぶ。DOMに触れず副作用を持てないため、メインスレッドのレイアウトを汚さずGPUへ乗せやすく、JS駆動アニメより安定する。

Houdiniが解く問題:CSSという黒箱

従来、JavaScriptからスタイルを操作する手段は本質的に2つしかありませんでした。element.style.width = "100px" のように文字列でプロパティを書くか、getComputedStyle()文字列を読み返すかです。どちらもブラウザ内部の最終結果(CSSOM)を外側から叩いているだけで、エンジンがスタイルをどう解釈し、どう描くかには一切介入できません。レイアウトやペイントは完全な黒箱で、開発者が新しい挙動を足したければ、JSでスタイルを毎フレーム書き換えて模倣するしかありませんでした。

CSS Houdini は、この黒箱を段階ごとに開ける低レベルAPI群です。レンダリングは「スタイル計算→レイアウト→ペイント→コンポジット」と流れますが(各段の詳細は レンダリングパイプライン詳説)、Houdiniはその各段にフックを公開します。Typed OMはスタイル計算の値表現を、Properties and Values APIはカスタムプロパティの型システムを、Paint/Layoutワークレットはペイント段とレイアウト段の処理そのものを、それぞれ開発者が差し替え・拡張できるようにします。

Typed OM:文字列をやめて型付き値に

CSSOM(element.style)の最大のコストは、値がすべて文字列であることです。el.style.width = "100px" と書くたびに、ブラウザは "100px" をパースして数値100と単位pxに分解し、内部表現に変換します。getComputedStyle(el).width で読むと、内部表現を逆に文字列へシリアライズします。ループで何度も読み書きすると、このパース・シリアライズの往復が無視できない負荷になります。

Typed OM(CSS Typed Object Model)は、値を型付きオブジェクトとして直接扱う代替APIです。el.attributeStyleMap 経由で読み書きし、値は CSSUnitValue(数値+単位)や CSSKeywordValueCSSTransformValue などのオブジェクトとして得られます。

// 従来:文字列。書くたびにパース、読むたびにシリアライズ
el.style.width = "100px";
const w = parseInt(getComputedStyle(el).width, 10); // "100px" → 100

// Typed OM:型付き値。パース往復が消える
el.attributeStyleMap.set("width", CSS.px(100));
const v = el.computedStyleMap().get("width"); // CSSUnitValue {value:100, unit:"px"}
v.value; // 100 を直接読める。文字列パース不要

差は単なる利便性ではありません。v.value数値プロパティなので、計算してそのまま書き戻せます。文字列を介さないぶん、値計算の段(specified→computed→used の過程)に近い表現で操作でき、頻繁なスタイル更新の往復コストが構造的に下がります。

Properties and Values API:カスタムプロパティに型を与える

通常のCSSカスタムプロパティ(--x: 10px)には型がありません。ブラウザにとって --x の値はただの文字列トークンで、10pxredgarbage も区別されません。この「無型」が2つの問題を生みます。第1に、無効な値を入れてもエラーにならず、使う側で var(--x) を展開した時点で初めて無効として扱われます。第2に、型が分からないためアニメーションできません10px から 20px への遷移を、文字列としてのカスタムプロパティに対しては補間できないのです。

@property(Properties and Values API)は、カスタムプロパティに構文(型)・初期値・継承の有無を宣言します。

@property --angle {
  syntax: "<angle>";      /* この型しか受け付けない */
  inherits: false;
  initial-value: 0deg;    /* 無効値はこれにフォールバック */
}

宣言すると3つが成立します。第1に、<angle> 以外の値はスタイル計算の段で無効として早期に弾かれ、初期値に落ちます。第2に、<angle> という型をブラウザが知るため、--angle0deg → 360deg補間でき、トランジションやアニメーションの対象になります。第3に、inherits で継承を制御できます。型なしカスタムプロパティではグラデーション角度のアニメなどが不可能でしたが、@property でこれが宣言的に解けます。

なぜ型がアニメ可否を決めるのか

補間とは「2つの値の中間を計算する」操作です。ブラウザが中間を計算するには、値が数値・色・角度・長さのいずれなのかを知る必要があります。型なしカスタムプロパティは文字列なので「10px20px の中間」を定義できず、補間がスキップされます。@property の syntax は、まさにこの補間アルゴリズムを選ぶための型情報を供給しています。

Paint Worklet:ペイント段に描画コードを差し込む

Paint Worklet(CSS Painting API)は、要素の背景・ボーダー・マスクなどを自前のコードで描く仕組みです。paint(myPainter)background-image 等に指定すると、ブラウザはペイント段でその描画関数を呼び、CanvasRenderingContext2D 風のAPIで要素の領域に直接描かせます。

// worklet ファイル:独立スコープで動く。DOM も window も見えない
registerPaint("checker", class {
  static get inputProperties() { return ["--cell-size"]; } // 監視するプロパティ
  paint(ctx, size, props) {
    const c = props.get("--cell-size").value; // Typed OM 値
    for (let y = 0; y < size.height; y += c)
      for (let x = 0; x < size.width; x += c)
        if (((x / c) + (y / c)) % 2 === 0) ctx.fillRect(x, y, c, c);
  }
});
/* メイン側:ワークレットを登録して paint() で参照 */
.box { background-image: paint(checker); --cell-size: 16px; }

重要なのは実行環境です。ワークレットはワークレットグローバルスコープという制限付き環境で動き、windowdocument・DOMには一切アクセスできません。状態を持てず、入力(要素サイズと inputProperties で宣言したプロパティ値)から出力(描画)を決める純粋関数として設計されています。この制約があるからこそ、ブラウザは描画をメインスレッド外で(実装によってはGPU側で)安全に実行でき、inputProperties のプロパティが変わったときだけ再描画する、という最適化が成り立ちます。

inputProperties が再描画のトリガを定義する

inputProperties で宣言したカスタムプロパティが変化したときだけ、ブラウザはその要素のペイントワークレットを再実行します。@property--cell-size<length> 型にしておけば、その値をアニメーションさせるたびにワークレットが再描画され、CSSアニメーションと連動した動的な描画が、JSの毎フレーム介入なしに実現します。

Layout Worklet:レイアウト段のアルゴリズムを書く

Layout Worklet(CSS Layout API)は、さらに深くレイアウト段に踏み込み、子要素をどこに配置し、自分の寸法をどう決めるかというアルゴリズム自体を実装します。display: layout(masonry) のように指定すると、ブラウザはレイアウト段でその layout() メソッドを呼び、子(LayoutChild)への寸法問い合わせと配置を開発者コードに委ねます。Flexbox や Grid と同格の整形コンテキストを、自前で定義できるわけです(標準レイアウトの解決過程は CSSレイアウトアルゴリズム)。

registerLayout("masonry", class {
  static get inputProperties() { return ["--gap"]; }
  async layout(children, edges, constraints, props) {
    // 各子を計測し、列ごとに最短の列へ積む(masonry の核)
    const fragments = [];
    for (const child of children) {
      const frag = await child.layoutNextFragment({});
      fragments.push(frag); // 配置座標を計算して frag.inlineOffset 等に設定
    }
    return { childFragments: fragments };
  }
});

レイアウトワークレットもペイント同様に隔離環境で動き、DOMに触れません。子の計測は layoutNextFragment() という非同期APIを介してブラウザに依頼します。これにより、ユーザーのレイアウトコードがメインスレッドのレイアウトツリーを直接いじることなく、ブラウザ管理下で安全に整形を駆動できます。

従来のJSスタイル操作との性能差

なぜHoudiniがJS駆動より速くなりうるのか。本質は「どのスレッドで・どの粒度で動くか」です。

観点従来のJSスタイル操作Houdini(Paint/Layout Worklet)
実行スレッドメインスレッド。レイアウト・他JSと競合隔離スコープ。メイン外で実行されうる
値の表現文字列。読み書きでパース/シリアライズTyped OM。型付きオブジェクトで往復なし
駆動の粒度毎フレーム JS が style を書き換え宣言したプロパティ変化時だけ再実行
DOM/レイアウト強制同期レイアウトを誘発しやすいDOM不可視。レイアウトスラッシングが起きない
アニメ連携rAF で手動補間@property の補間にブラウザが任せる

JSでカスタム背景アニメや独自レイアウトを作る典型は、requestAnimationFrame のたびに getComputedStyle() で読み、計算し、style に書き戻す手法です。これは読み(レイアウト確定が必要)と書き(レイアウト無効化)を交互に行うため、**強制同期レイアウト(レイアウトスラッシング)**を誘発し、メインスレッドを長く占有します(コスト構造は リフローとリペイントのコスト、フレーム同期は requestAnimationFrameと描画リズム)。

Houdiniはこの構造を変えます。ペイント/レイアウトのワークレットはDOMに触れず副作用も持たないため、そもそもレイアウトスラッシングが原理的に起きません。さらに描画はメインスレッド外へ追い出せ、再実行は inputProperties の変化時に限られます。アニメーションは @property の型情報をもとにブラウザの補間器に委ねられ、JSが毎フレーム介入する必要が消えます。結果として、同じ視覚効果でもメインスレッドの負荷が下がり、フレーム落ちが減ります。

速いのは前提が揃ったときだけ

Houdiniが速いのは「副作用なし・型付き・宣言的駆動」という前提が成り立つ範囲です。ワークレット内で複雑な計算を毎描画行えば、それ自体が重くなります。また実装によってはワークレットがメインスレッドで動く場合もあり、「常にGPUで動く」と仮定するのは誤りです。利点は構造(スラッシング回避・パース削減・再実行の局所化)から来るのであって、魔法ではありません。

実装状況という現実

Houdini群はAPIごとに普及度が大きく違います。Typed OM・Properties and Values API(@property)は主要ブラウザで広く使え、特に @property は型付きカスタムプロパティの補間として実務でも定着しています。一方、Paint Worklet はChromium系中心で、Layout Worklet はさらに限定的(長くChromium実験的)です。

押さえどころ

頻出は、(1) Houdiniがレンダリングパイプラインの各段(スタイル計算・レイアウト・ペイント)に低レベルフックを公開する点、(2) Typed OMが文字列パース/シリアライズを型付きオブジェクトで置き換える点、(3) @property が syntax・initial-value・inherits を宣言し、無効値の早期排除とカスタムプロパティの補間(アニメ化)を可能にする点、(4) Paint/Layoutワークレットが DOM 不可視の隔離スコープで純粋関数として動き、inputProperties 変化時だけ再実行される点、(5) JS駆動と違いレイアウトスラッシングを起こさずメイン外実行されうる点、の5つです。

まとめ

まとめ

CSS Houdini は、これまで黒箱だったレンダリングパイプラインを段階ごとに開発者へ開放する低レベルAPI群です。Typed OM は値を文字列から型付きオブジェクト(CSSUnitValue 等)へ移し、読み書きのパース往復を消します。Properties and Values API(@property)はカスタムプロパティに型・初期値・継承を宣言し、無効値の早期排除と補間によるアニメ化を実現します。Paint Worklet はペイント段に、Layout Worklet はレイアウト段に、DOM不可視・副作用なしの純粋関数を差し込み、inputProperties の変化時だけ再実行されます。従来のJSスタイル操作が文字列パースとレイアウトスラッシングでメインスレッドを縛るのに対し、Houdiniは構造的にそれらを回避し、描画をメイン外へ逃がせる点に本質的な性能差があります。

Web/フロントエンド Article

CSS Houdiniとペイント/レイアウトワークレットを実務で読む

TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。

解決すること

CSS

比較で見る軸

難易度: advanced / カテゴリ: Web/フロントエンド / タグ数: 5

導入後に効く点

Typed OMはCSS値を文字列ではなく型付きオブジェクト(CSSUnitValue等)で扱い、読み書きのたびのパース・シリアライズを省く。Properties and Values APIは @property でカスタムプロパティに型・初期値・継承を宣言し、アニメ可能化と無効値の早期排除を実現する。

先に潰すリスク

用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。

数字・仕様の読み方
難易度
advanced
カテゴリ
Web/フロントエンド
タグ数
5

判断チェックリスト

  • 自社の用途が「CSS / Houdini」に近いか確認する。
  • 強みである「CSS Houdiniはブラウザのスタイル計算・レイアウト・ペイントの各段に低レベルなフックを公開する一連のAPI群。従来のJSはレンダリング結果(DOM/CSSOM)を文字列で叩くだけだったが、Houdiniはエンジン内部のパイプライン自体に処理を差し込む。」が本当に評価軸になるか確認する。
  • 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
  • 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
  • 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
  • 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。

次に確認する観点

CSSHoudiniレンダリングパフォーマンスブラウザCSSHoudiniレンダリング
参考: 公式情報