Houdini: Desmitificación de CSS

¿Alguna vez pensaste en la cantidad de trabajo que realiza CSS? Cambias un solo atributo y, de repente, todo tu sitio web aparece en un diseño diferente. Es una especie de magia. Hasta ahora, nosotros, la comunidad de desarrolladores web, solo hemos podido presenciar y observar la magia. ¿Qué sucede si queremos crear nuestro propio código mágico? ¿Qué sucede si queremos convertirnos en el mago?

¡Presentamos Houdini!

El grupo de trabajo de Houdini está formado por ingenieros de Mozilla, Apple, Opera, Microsoft, HP, Intel y Google que trabajan juntos para exponer ciertas partes del motor de CSS a los desarrolladores web. El grupo de trabajo está trabajando en una recopilación de borradores con el objetivo de que W3C los acepte para que se conviertan en estándares web reales. Se fijaron algunos objetivos de alto nivel, los convirtieron en borradores de especificaciones que, a su vez, dieron lugar a un conjunto de borradores de especificaciones de nivel inferior y de respaldo.

La colección de estos borradores es lo que se suele entender cuando alguien habla de "Houdini". Al momento de la redacción, la lista de borradores está incompleta y algunas de ellas son meros marcadores de posición.

Las especificaciones

Worklets (especificación)

Los worklets por sí solos no son muy útiles. Son un concepto que se introduce para permitir muchos de los borradores posteriores. Si pensaste en los trabajadores web cuando leíste "worklet", no te equivocas. Tienen mucha superposición conceptual. ¿Por qué hay algo nuevo si ya tenemos trabajadores?

El objetivo de Houdini es exponer nuevas APIs para permitir que los desarrolladores web conecten su propio código en el motor de CSS y en los sistemas que los rodean. Es probable que no sea irreal suponer que algunos de estos fragmentos de código se tendrán que ejecutar en cada fotograma. Algunos de ellos tienen que ser así por definición. Cita de la especificación de Web Worker:

Eso significa que los Web Workers no son viables para lo que Houdini planea hacer. Por lo tanto, se inventaron los worklets. Los worklets usan clases ES2015 para definir una colección de métodos, cuyas firmas están predefinidas por el tipo de worklet. Son ligeros y de corta duración.

API de CSS Paint (especificaciones)

La API de Paint está habilitada de forma predeterminada en Chrome 65. Lee la introducción detallada.

Worklet de Compositor

La API que se describe aquí está obsoleta. Se rediseñó la worklet de compositor y ahora se propone como "Animation Worklet". Obtén más información sobre la iteración actual de la API.

Aunque la especificación de la worklet del compositor se trasladó a WICG y se iterará, es la especificación que más me entusiasma. El motor de CSS subcontrata algunas operaciones a la tarjeta gráfica de tu computadora, aunque eso depende de la tarjeta gráfica y del dispositivo en general.

Por lo general, un navegador toma el árbol del DOM y, en función de criterios específicos, decide asignarles su propia capa a algunas ramas y subárboles. Estos subárboles se pintan en él (tal vez con una tarea de pintura en el futuro). Como paso final, todas estas capas individuales, ahora pintadas, se apilan y se colocan una encima de la otra, respetando los índices z, las transformaciones 3D y demás, para generar la imagen final que se ve en la pantalla. Este proceso se denomina composición y lo ejecuta el compositor.

La ventaja del proceso de composición es que no tienes que hacer que todos los elementos se vuelvan a pintar cuando la página se desplace un poco. En su lugar, puedes volver a usar las capas del fotograma anterior y volver a ejecutar el compositor con la posición de desplazamiento actualizada. Esto hace que todo sea más rápido. Esto nos ayuda a alcanzar los 60 fps.

Worklet del compositor

Como su nombre lo sugiere, la worklet del compositor te permite conectarte al compositor y, así, influir en la forma en que se posiciona y se coloca en capas sobre las otras capas la capa de un elemento que ya se pintó.

Para ser un poco más específico, puedes indicarle al navegador que deseas conectarte al proceso de composición de un nodo DOM determinado y solicitar acceso a ciertos atributos, como la posición de desplazamiento, transform o opacity. Esto obliga a este elemento a su propia capa y en cada fotograma se llama a tu código. Puedes mover tu capa manipulando la transformación de capas y cambiar sus atributos (como opacity), lo que te permite hacer cosas muy sofisticadas a una velocidad de 60 fps.

Esta es una implementación completa del desplazamiento de paralaje con el worklet del compositor.

// 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 escribió un polyfill para la worklet del compositor para que puedas probarlo, por supuesto, con un impacto de rendimiento mucho más alto.

Worklet de diseño (especificación)

Se propuso el primer borrador real de las especificaciones. Queda poco para la implementación.

Nuevamente, la especificación para esto está prácticamente vacía, pero el concepto es interesante: escribe tu propio diseño. Se supone que el worklet de diseño te permite realizar display: layout('myLayout') y ejecutar JavaScript para organizar los elementos secundarios de un nodo en el cuadro del nodo.

Por supuesto, ejecutar una implementación completa de JavaScript del diseño flex-box de CSS es más lento que ejecutar una implementación nativa equivalente, pero es fácil imaginar una situación en la que recortar las esquinas puede generar un aumento de rendimiento. Imagina un sitio web que solo contenga tarjetas, como Windows 10 o un diseño de estilo ladrillo. No se usa el posicionamiento absoluto ni fijo, tampoco z-index, ni los elementos se superponen ni tienen ningún tipo de borde o desbordamiento. Poder omitir todas estas verificaciones durante el rediseño podría aumentar el rendimiento.

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 tipado (especificación)

El CSSOM tipado (modelo de objetos de CSS o modelo de objetos de hojas de estilo en cascada) aborda un problema que probablemente todos hayamos encontrado y que aprendimos a soportar. Te lo ilustro con una línea de JavaScript:

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

Estamos haciendo operaciones matemáticas, convirtiendo un número en una cadena para agregar una unidad solo para que el navegador analice esa cadena y la vuelva a convertir en un número para el motor de CSS. Esto se vuelve aún más complicado cuando manipulas transformaciones con JavaScript. ¡No más! CSS está a punto de escribir.

Este borrador es uno de los más maduros y ya se está trabajando en un polyfill. (Renuncia de responsabilidad: Por supuesto, el uso del polyfill agregará aún más sobrecarga computacional). El objetivo es mostrar lo conveniente que es la API).

En lugar de cadenas, trabajarás en el StylePropertyMap de un elemento, en el que cada atributo CSS tiene su propia clave y el tipo de valor correspondiente. Los atributos como width tienen LengthValue como su tipo de valor. Un LengthValue es un diccionario de todas las unidades de CSS, como em, rem, px y percent, entre otras. Configurar height: calc(5px + 5%) generaría un LengthValue{px: 5, percent: 5}. Algunas propiedades, como box-sizing, solo aceptan ciertas palabras clave y, por lo tanto, tienen un tipo de valor KeywordValue. Luego, se podría verificar la validez de esos atributos durante el tiempo de ejecución.

<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}

Propiedades y valores

(especificación)

¿Conoces las propiedades personalizadas de CSS (o su alias no oficial "variables de CSS")? Estos son todos, pero con tipos. Hasta ahora, las variables solo podían tener valores de cadena y usaban un enfoque simple de búsqueda y reemplazo. Este borrador te permitiría no solo especificar un tipo para tus variables, sino también definir un valor predeterminado y, además, influir en el comportamiento de herencia con una API de JavaScript. Técnicamente, esto también permitiría animar las propiedades personalizadas con transiciones y animaciones estándar de CSS, lo que también se tiene en cuenta.

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

Métricas de fuentes

Las métricas de fuente son exactamente lo que parecen. ¿Cuál es el cuadro de límite (o los cuadros delimitadores) cuando renderiza la cadena X con la fuente Y con el tamaño Z? ¿Qué sucede si uso anotaciones de Ruby? Esto se solicitó mucho y Houdini debería hacer que estos deseos se hagan realidad.

Espera, hay más calcomanías.

Hay aún más especificaciones en la lista de borradores de Houdini, pero su futuro es bastante incierto y no son mucho más que marcadores de posición para ideas. Entre los ejemplos, se incluyen comportamientos de desbordamiento personalizados, la API de extensión de sintaxis CSS, la extensión del comportamiento nativo de desplazamiento y otros elementos igualmente ambiciosos, que permiten realizar acciones en la plataforma web que antes no eran posibles.

Demostraciones

El código de la demostración es de código abierto (demostración en vivo con polyfill).