CSS Houdini:JS in CSS 的魔术师
Houdini是一组底层API,它们公开了CSS引擎的各个部分,从而使开发人员能够通过加入浏览器渲染引擎的样式和布局过程来扩展CSS。 Houdini是一组API,它们使开发人员可以直接访问CSS 对象模型 (CSSOM),使开发人员可以编写浏览器可以解析为CSS的代码,从而创建新的CSS功能,而无需等待它们在浏览器中本地实现。
背景
前端开发者需要更多的CSS控制“权限”

浏览器渲染网页通常会经历 HTML/CSS parse -> style calculation -> layout -> paint -> composite 等阶段。HTML 和 CSS 分别被解析为 DOM 与 CSSOM,并进一步参与渲染树的构建。
前端开发者通常通过 JavaScript 操作 DOM 与 CSSOM 来影响页面,但对后续的 Layout、Paint 与 Composite 阶段控制有限。某些样式变更会触发布局或绘制,从而产生较高的性能开销。
由此前端开发者抛出了问题:浏览器有没有能直接操作前面这些环节的方法呢?有没有方法不用js操作DOM改变style或者切换class来改变样式呢?
CSS新特性的痛点
js的新特性,往往能通过babel或其他途径实现polyfill,从而快速将新特性应用到生产环境中。不同于js,而CSS并非js这样的动态语言,我们无法简单的提供polyfills,顶多利用postCSS、Less/Sass/Stylus 等工具转译出浏览器能接受的 CSS,是否能兼容就是浏览器的事了,然而现实是各家浏览器的版本、更新进度很是缓慢。
由此前端开发者又抛出了问题:能否让开发人员有能力创建他们自己自定义的CSS特性,并让这些特性在浏览器的原生渲染管道中高效运行。这样就可以实现CSS的polyfill,让这些新特性在浏览器的原生渲染管道中高效运行。
这些痛点就是CSS Houdini诞生的原因。
介绍
Houdini 是美国的伟大魔术师,擅长逃脱术。
CSS Houdini 是由一群来自W3C, Mozilla, Apple, Opera, Microsoft, HP, Intel, IBM, Adobe 与 Google 的工程师所组成的工作小组,志在建立一系列的 API,让开发者能够介入浏览器的 CSS engine 操作,带给开发者更多的解決方案,用来解決 CSS 长久以来的问题:
- Cross-browser issues
- CSS Polyfill
特性和优势
与使用JavaScript.style进行DOM样式更改相比,Houdini有着更快的解析时间。浏览器在应用在脚本中找到的任何样式更新之前解析CSSOM(包括布局、绘制和组合进程)。此外,布局、绘制和合成过程会重复进行JavaScript样式的更新。Houdini代码不会等待第一个渲染周期完成。相反,它包含在第一个循环中——创建可渲染、可理解的样式。Houdini提供了一个基于对象的API,用于在JavaScript中处理CSS值。
上手
Houdini 的 CSS Typed OM 是一个 CSS 对象模型,包含类型和方法,将值作为 JavaScript 对象公开,使 CSS 操作比基于字符串的 HTMLElement.style 更加直观。元素和样式表规则可以通过相应的样式映射访问这些值。
CSS Houdini的一个特性是工作集。使用工作集,您可以创建模块化CSS,需要一行JavaScript来导入可配置的组件:不需要前处理器、后处理器或JavaScript框架。
添加的模块包含 registerPaint() 函数,用于注册可配置的 Paint Worklet。
CSS paint()函数参数包括工作集的名称以及可选参数。工作集还可以访问元素的自定义属性:它们不需要作为函数参数传递。
如:
1 2 3 4 5 6 7 8 9 10
| <script> CSS.paintWorklet.addModule('csscomponent.js'); </script> <style> li { background-image: paint(myComponent, stroke, 10px); --highlights: blue; --lowlights: green; } </style>
|
API
可访问文档>>
CSS Parser API
一种更直接地公开CSS解析器的API,用于将任意类CSS语言解析为一种轻微类型化的表示。
(目前尚未为此API编写指南或引用,可以看https://drafts.css-houdini.org/css-parser-api/。)
CSS Properties and Values API
定义用于注册新CSS属性的API。使用此API注册的属性提供了定义类型、继承行为和初始值的解析语法。关于CSS3变量的详细情况可见另一篇笔记《【笔记】使用CSS变量》
使用如:
1 2 3 4 5 6 7
| $font-size: 10px; $brightBlue: blue;
.m-mark { font-size: 1.5 * $font-size; color: $brightBlue }
|
相比于预处理工具的变量,CSS变量有着优势。比如动态调整:
1 2 3 4 5 6 7 8
| .u-txt { --box-shadow-color: yellow; box-shadow: 0 0 30px var(--box-shadow-color); } .u-txt:hover { --box-shadow-color: red; }
|
或者使用 JavaScript 控制:
1 2 3
| const textBox = document.querySelector('.u-txt'); const boxShadowColor = getComputedStyle(textBox).getPropertyValue('--box-shadow-color'); textBox.style.setProperty('--box-shadow-color', 'new color');
|
兼容情况
CSS 变量的移动端兼容情况尚可,桌面端除 IE 外支持较好。

*注册属性
目前没有完全稳定的兼容使用
使用如:
1 2 3 4 5 6
| CSS.registerProperty({ name: '--myprop', syntax: '<length>', initialValue: '1px', inherits: true });
|
如果inherits: true那就是我们看见的作用域的效果,如果不是true则不会被父作用域影响,而且取默认值。
这个自定义属性,精辟在于,可以用永久循环的animation驱动一次性的transform。换句话说,我们如果用了css变量+transform,可以靠js改变这个变量达到花俏的效果。但是,现在不需要js,只要css内部消化,transform成为永动机。
1 2 3 4 5 6 7 8 9
| ['x1','y1','z1','x2','y2','z2'].forEach(p => { CSS.registerProperty({ name: `--${p}`, syntax: '<angle>', inherits: false, initialValue: '0deg' }); });
|
样式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
| <style> #myprop, #myprop1 { width: 200px; border: 2px dashed #000; border-bottom: 10px solid #000; animation:myprop 3000ms alternate infinite ease-in-out; transform: rotateX(var(--x2)) rotateY(var(--y2)) rotateZ(var(--z2)) } @keyframes myprop { 25% { --x1: 20deg; --y1: 30deg; --z1: 40deg; } 50% { --x1: -20deg; --z1: -40deg; --y1: -30deg; } 75% { --x2: -200deg; --y2: 130deg; --z2: -350deg; } 100% { --x1: -200deg; --y1: 130deg; --z1: -350deg; } }
@keyframes myprop1 { 25% { --x1: 20deg; --y1: 30deg; --z1: 40deg; } 50% { --x2: -200deg; --y2: 130deg; --z2: -350deg; } 75% { --x1: -20deg; --z1: -40deg; --y1: -30deg; } 100% { --x1: -200deg; --y1: 130deg; --z1: -350deg; } } </style>
<div id="myprop"></div> <div id="myprop1"></div>
|
CSS Typed OM
Typed OM 将 CSS 属性和值存放在 attributeStyleMap 对象中,可以直接操作类型化的值,避免频繁解析字符串。attributeStyleMap 存储的是 CSS 数值对象而不是普通字符串,并支持算术运算和单位换算。
最主要的功能在于将 CSSOM 使用的字符串值转换成具有类型意义的 JavaScript 表示形式。所有 CSS Values 都有一个基础接口:
1 2 3 4 5
| interface CSSStyleValue { stringifier; static CSSStyleValue? parse(DOMString property, DOMString cssText); static sequence<CSSStyleValue>? parseAll(DOMString property, DOMString cssText); };
|
你可以如下操作 CSS style:
1 2 3 4 5 6 7 8 9 10 11 12 13
| const map = document.querySelector('.example').attributeStyleMap; console.log(map.get('font-size'));
console.log( new CSSUnitValue(5, "px") );
elem.attributeStyleMap.set('transform', new CSSTransformValue([ new CSSTranslate(CSS.px(0), CSS.percent(100 - currentPercent), CSS.px(0)) ]));
|
计算单位:
1 2 3 4 5 6
| if ('CSS' in window) { CSS.px(1); CSS.number(0); CSS.rem(2); new CSSUnitValue(2, 'percent'); }
|
数学运算:
1 2
| new CSSMathSum(CSS.rem(10), CSS.px(-1)) new CSSMathMax(CSS.px(1), CSS.px(2))
|
CSS Layout API
这个 API 旨在提高 CSS 的可扩展性,使开发者能够编写自己的布局算法,例如 masonry 或 line snapping。它目前仍属于实验性能力,使用前需要检查浏览器支持情况。开发者可以通过 registerLayout 函数传入布局名称和 JavaScript 类,定义可赋给 display 属性的布局逻辑。下面以 block-like 布局为例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| registerLayout('block-like', class extends Layout { static blockifyChildren = true; static inputProperties = super.inputProperties; *layout(space, children, styleMap) { const inlineSize = resolveInlineSize(space, styleMap); const bordersAndPadding = resolveBordersAndPadding(constraintSpace, styleMap); const scrollbarSize = resolveScrollbarSize(constraintSpace, styleMap); const availableInlineSize = inlineSize - bordersAndPadding.inlineStart - bordersAndPadding.inlineEnd - scrollbarSize.inline; const availableBlockSize = resolveBlockSize(constraintSpace, styleMap) - bordersAndPadding.blockStart - bordersAndPadding.blockEnd - scrollbarSize.block; const childFragments = []; const childConstraintSpace = new ConstraintSpace({ inlineSize: availableInlineSize, blockSize: availableBlockSize, }); let maxChildInlineSize = 0; let blockOffset = bordersAndPadding.blockStart; for (let child of children) { const fragment = yield child.layoutNextFragment(childConstraintSpace); fragment.blockOffset = blockOffset; fragment.inlineOffset = Math.max( bordersAndPadding.inlineStart, (availableInlineSize - fragment.inlineSize) / 2); maxChildInlineSize = Math.max(maxChildInlineSize, childFragments.inlineSize); blockOffset += fragment.blockSize; } const inlineOverflowSize = maxChildInlineSize + bordersAndPadding.inlineEnd; const blockOverflowSize = blockOffset + bordersAndPadding.blockEnd; const blockSize = resolveBlockSize( constraintSpace, styleMap, blockOverflowSize); return { inlineSize: inlineSize, blockSize: blockSize, inlineOverflowSize: inlineOverflowSize, blockOverflowSize: blockOverflowSize, childFragments: childFragments, }; } });
|
使用如:
1 2 3
| .wrapper { display: layout('block-like'); }
|
(目前尚未为此API编写指南或引用。)
CSS Painting API
开发用于提高CSS的可扩展性-允许开发人员编写JavaScript函数,这些函数可以通过paint() CSS函数直接绘制到元素的背景、边框或内容中。
比如定义 Paint Method:
1 2 3 4 5 6 7 8 9 10
| registerPaint('simpleRect', class { static get inputProperties() { return ['--rect-color']; } paint(ctx, size, properties) { const color = properties.get('--rect-color'); ctx.fillStyle = color.cssText; ctx.fillRect(0, 0, size.width, size.height); } });
|
使用如:
1 2 3 4 5 6 7 8 9 10 11 12 13
| .demo-1 { --rect-color: red; width: 50px; height: 50px; background-image: paint(simpleRect); } .demo-2 { --rect-color: yellow; width: 100px; height: 100px; background-size: 50% 50%; background-image: paint(simpleRect); }
|
如此.demo-1 与 .demo-2 就可以拥有各自定义宽高颜色的方形 background-image。
兼容情况
除了新版Chrome和较新的Android,其他没啥兼容的。

Worklets
用于在呈现管道的不同阶段运行脚本的API,独立于主JavaScript执行环境。工作集在概念上类似于Web工作者,由呈现引擎调用并扩展。在上述的 Layout API 与 Paint API 中,我们都有写一个 JS文件,用来定义新的属性,然后在 CSS 文件中取用,你可能会觉得那个 JS 文件就直接像一般 Web 嵌入 JS 的方式一样即可, 但实际上并非如此,我们需要通过 Worklets 來帮我们载入。以上面的 Painting API 为例:
1 2 3 4 5 6 7 8 9 10 11 12
| paintWorklet.addModule('simpleRect.js'); registerPaint('simpleRect', class { static get inputProperties() { return ['--rect-color']; } paint(ctx, size, properties) { const color = properties.get('--rect-color'); ctx.fillStyle = color.cssText; ctx.fillRect(0, 0, size.width, size.height); } });
|
Worklets 的特点就是轻量以及生命周期较短
总体兼容

使用
封装库
三方封装库extra.css
使用
使用它有两个步骤:
- HTML包括工作集和自定义属性文件
<script src='<packageName></script>
- CSS使用后台访问paint工作集:
paint(<workletName>);
URL的格式如下:https://unpkg.com/extra.css/.js单击上面的链接可获取任何正确的URL以获取CDN链接。如果您转到这个链接您的URL栏,您将自动链接到JS包的最新版本号,其中包括所有注册的自定义属性和工作集。这是指向最新版本(即https://unpkg.com/extra.css@1.1.0/.js)的链接,但如果您始终想要evergreen最新版本,则可以跳过版本号。“演示”选项卡将打开一个解释性的代码笔项目,所有内容都已正确连接。
下面是一个示例,说明跨路径的HTML和CSS的示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76
| <h1>Hello<br/> World</h1> <p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Velit <span>asperiores</span> cupiditate dolor quo iste animi, eveniet in inventore <span>obcaecati</span> quisquam architecto pariatur et perspiciatis, deleniti voluptatum consequuntur <span>voluptas</span> voluptatibus repellat!</p>
<script src="https://unpkg.com/extra.css/crossOut.js"></script>
<style> // This is where we use the custom properties
@supports (background: paint(something)) { h1 {
--extra-crossColor: #fc0; --extra-crossWidth: 3;
background: paint(extra-crossOut); line-height: 1.5; }
h2 { margin: 10rem auto; display: block; width: 80vw; max-width: 900px; height: 400px; display: flex; justify-content: center; align-items: center; }
span { --extra-crossColor: #d4f;
background: paint(extra-crossOut); } }
// Additional Styling
body { font-family: monospace; text-align: center; padding: 1rem; margin: 0; }
p { max-width: 660px; margin: 0 auto; font-weight: 300; font-size: 1.2rem; line-height: 1.5; text-align: left; }
h1 { font-size: 4rem; display: inline-block; font-weight: 100; margin: 1rem auto; width: 100%; max-width: 760px; position: relative; }
h2 { font-size: 2.5rem; font-weight: 100; display: inline-block; position: relative; } </style>
|
Demo
有个很好的库:https://github.com/GoogleChromeLabs/houdini-samples
相关链接
Author
My name is Micheal Wayne and this is my blog.
I am a front-end software engineer.
Contact: michealwayne@163.com