Houdini - 揭秘 CSS

您有没有想过 CSS 所做的工作量很大?当您更改单个属性时,整个网站突然以不同的布局显示。就像魔法一样。到目前为止,我们(网络开发者社区)只能见证并观察这种奇妙之处。如果我们想构思出自己的魔法,该怎么办?如果我们想成为魔术师,该怎么办?

胡迪尼上场!

Houdini 任务组由来自 Mozilla、Apple、Opera、Microsoft、HP、Intel 和 Google 的工程师组成,他们齐心协力,向 Web 开发者公开 CSS 引擎的某些部分。该任务组正在筹备草稿集合,目标是让 W3C 接受这些草稿,从而成为实际的 Web 标准。他们为自己设定了一些高级目标,将其转换成了规范草案,进而催生了一系列较低级别的配套规范草案。

当有人谈论“Houdini”时,通常指的是收集到这些草稿。在撰写本文时,草稿列表并不完整,部分草稿只是占位符。

规范

Worklet(spec

Worklet 本身的用处并不大。我们引入这些概念是为了支持后期的许多草稿。如果您在阅读“worklet”时想到 Web Worker,没有错。它们在概念上有很多重叠。既然已经有 worker,又何必另辟蹊径呢?

Houdini 的目标是公开新的 API,以允许 Web 开发者将自己的代码连接到 CSS 引擎和周围系统。假设某些代码段必须单独运行,可能不切实际。其中一些必须按照定义来回答。引用 Web Worker 规范

这意味着 Web Worker 无法实现 Houdini 打算执行的操作。因此,我们发明了 Worklet。Worklet 使用 ES2015 类定义方法集合,其签名按 Worklet 类型预定义。它们是轻量级的,而且生命周期很短。

CSS Paint API(spec

在 Chrome 65 中,Paint API 默认处于启用状态。阅读详细说明

合成器 Worklet

此处介绍的 API 已过时。合成器 Worklet 经过了重新设计,现已提议为“动画 Worklet”。详细了解 API 的当前迭代

尽管合成器 Worklet 规范已移至 WICG 并将不断迭代,但它是最让我兴奋的规范。某些操作由 CSS 引擎外包到计算机的显卡,但这通常取决于您的显卡和设备。

浏览器通常会采用 DOM 树,并根据特定条件决定为一些分支和子树提供自己的层。这些子树会自行绘制到其上(将来可能会使用绘制 Worklet)。最后,所有这些层(现已绘制)都会堆叠并相互叠加,并遵循 Z 索引、3D 转换等因素,以生成屏幕上显示的最终图像。此过程称为“合成”,由合成器执行。

合成过程的优势在于,您无需在页面稍微滚动时让所有元素自行重绘。相反,您可以重复使用上一帧中的层,并且只需使用更新后的滚动位置重新运行合成器即可。这样可以提高速度。这有助于我们达到 60fps。

合成器 Worklet。

顾名思义,合成器 Worklet 可让您连接到合成器,并影响已绘制的元素层、在其他层之上放置和分层的方式。

为了更具体一点,您可以告知浏览器您希望接入某个 DOM 节点的合成进程,并且可以请求访问某些属性,例如滚动位置、transformopacity。这会强制将此元素置于其自己的层,并在调用您的代码的每一帧上。您可以通过操纵层转换和更改其属性(如 opacity)来移动层,从而以高达 60 fps 的速度实现奇特的优化。

以下是使用合成器 Worklet 实现视差滚动的完整实现。

// main.js
window.compositorWorklet.import('worklet.js')
    .then(function() {
    var animator = new CompositorAnimator('parallax');
    animator.postMessage([
        new CompositorProxy($('.scroller'), ['scrollTop']),
        new CompositorProxy($('.parallax'), ['transform']),
    ]);
    });

// worklet.js
registerCompositorAnimator('parallax', class {
    tick(timestamp) {
    var t = self.parallax.transform;
    t.m42 = -0.1 * self.scroller.scrollTop;
    self.parallax.transform = t;
    }

    onmessage(e) {
    self.scroller = e.data[0];
    self.parallax = e.data[1];
    };
});

Robert Flack 已为合成器 Worklet 编写了一个 polyfill,因此您可以试用一下;这显然对性能有更好的影响。

布局 Worklet(spec

第一个真正的规范草稿已提出。实施是一个不错的选择。

同样,它的规范实际上为空,但其概念很有趣:编写您自己的布局!布局 Worklet 应该使您能够执行 display: layout('myLayout') 并运行 JavaScript,以便在节点的框中排列节点的子项。

当然,运行 CSS flex-box 布局的完整 JavaScript 实现比运行等效的原生实现要慢,但很容易想象这样一种情形:切入角落可以提升性能。假设某个网站只有图块,例如 Windows 10 或砖石式布局。既不使用绝对定位和固定定位,z-index 也不会使用任何元素,也不会使元素发生重叠或具有任何类型的边框或溢出。能够在重新布局上跳过所有这些检查可以实现性能提升。

registerLayout('random-layout', class {
    static get inputProperties() {
        return [];
    }
    static get childrenInputProperties() {
        return [];
    }
    layout(children, constraintSpace, styleMap) {
        const width = constraintSpace.width;
        const height = constraintSpace.height;
        for (let child of children) {
            const x = Math.random()*width;
            const y = Math.random()*height;
            const constraintSubSpace = new ConstraintSpace();
            constraintSubSpace.width = width-x;
            constraintSubSpace.height = height-y;
            const childFragment = child.doLayout(constraintSubSpace);
            childFragment.x = x;
            childFragment.y = y;
        }

        return {
            minContent: 0,
            maxContent: 0,
            width: width,
            height: height,
            fragments: [],
            unPositionedChildren: [],
            breakToken: null
        };
    }
});

类型化 CSSOM(spec

类型化 CSSOM(CSS 对象模型或级联样式表对象模型)解决了我们可能都遇到过并且刚刚学会应付的问题。让我们用一行 JavaScript 来说明一下:

    $('#someDiv').style.height = getRandomInt() + 'px';

我们正在进行数学运算,将数字转换为字符串以附加一个单位,这只是为了让浏览器解析该字符串,并将其转换回供 CSS 引擎使用的数字。如果使用 JavaScript 操控转换,情况会更糟。不行了!CSS 即将进行一些输入。

此草稿是较成熟的草稿之一,并且已经在使用 polyfill。(免责声明:使用 polyfill 显然会增加更多计算开销。关键在于展示该 API 有多方便。)

您处理的不是字符串,而是元素的 StylePropertyMap,其中每个 CSS 属性都有自己的键和对应的值类型。width 等属性使用 LengthValue 作为其值类型。LengthValue 是所有 CSS 单元(如 emrempxpercent 等)的字典。设置 height: calc(5px + 5%) 会生成 LengthValue{px: 5, percent: 5}。某些属性(如 box-sizing)只接受某些关键字,因此具有 KeywordValue 值类型。这样便可在运行时检查这些属性的有效性。

<div style="width: 200px;" id="div1"></div>
<div style="width: 300px;" id="div2"></div>
<div id="div3"></div>
<div style="margin-left: calc(5em + 50%);" id="div4"></div>
var w1 = $('#div1').styleMap.get('width');
var w2 = $('#div2').styleMap.get('width');
$('#div3').styleMap.set('background-size',
    [new SimpleLength(200, 'px'), w1.add(w2)])
$('#div4')).styleMap.get('margin-left')
    // => {em: 5, percent: 50}

属性和值

(spec)

您了解 CSS 自定义属性(或其非官方别名“CSS 变量”)吗? 这些只不过是类型而已!到目前为止,变量只能使用字符串值,并使用了简单的“搜索并替换”方法。此草稿不仅可以让您指定变量的类型,还可以定义默认值并使用 JavaScript API 影响继承行为。从技术上讲,这还可以使自定义属性通过标准的 CSS 过渡和动画获得动画效果,这也属于考虑范畴。

["--scale-x", "--scale-y"].forEach(function(name) {
document.registerProperty({
    name: name,
    syntax: "<number>",
    inherits: false,
    initialValue: "1"
    });
});

字体指标

字体指标正如其名。当我以 Z 字号渲染字体 Y 的字符串 X 时,边界框(或边界框)是什么?如果我使用 ruby 注解,会怎么样?人们呼声很高,Houdini 终于可以实现这些愿望了。

但不止如此!

Houdini 的草稿列表中还有更多规范,但这些规范的未来相当不确定,它们不仅仅是想法的占位符。 例如,自定义溢出行为、CSS 语法扩展 API、原生滚动行为的扩展,以及类似的雄心勃勃的功能,所有这些都可以在网络平台上实现以前无法实现的功能。

样本歌曲

我已开源演示代码(使用 polyfill 进行实时演示)。