Skip to content

CSS Houdini, algo más que magia

Posted on:4 de enero de 2020

Este post fue originalmente publicado en Octuweb en octubre de 2018.

Para mucha gente CSS es cosa de magos, incluso habrá quien crea que es brujería, pero eso es por que no conocen los hechizos necesarios para tener un auténtico control sobre el CSS.

CSS Houdini es como el libro mágico que nos lleva al siguiente nivel en nuestro control de la magia, una serie de nuevos hechicos para controlar las diferentes fases en el proceso de renderizado.

.CSS

Empecemos por conocer el escenario actual.

Rendering progress

En el diagrama podemos ver el proceso de renderizado que hace el navegador, convirtiendo el html en los pixels que acaban representados en el dispositivo.

Una vez empieza el proceso no tenemos ningún control, en ninguna de las fases, sobre cómo el navegador analizar el HTML y CSS y lo convierte en el modelo de objetos DOM y CSSOM respectivamente.

Si queremos modificar el comportamiento de alguna propiedad CSS, ya sea para corregir un bug o poder adaptar el comportamiento a nuestro contenido y diseño, lo tenemos que hacer con Javascript una vez tenemos el DOM construido, seleccionar el elemento para aplicar las nuevas propiedades CSS y generar un nuevo CSSOM.

Rendering process polyfilled

En este otro diagrama podemos ver que con el Javascript podemos modificar el DOM y/o CSSOM, disparando de nuevo el proceso de Cascada, Layout, Paint y Composite. Esto representa un coste adicional al navegador, y aunque nos parezca que es un coste totalmente asumible en nuestra flameante máquina para desarrollar, hemos de tener en cuenta que el mayor consumo de contenidos proviene de los dispositivos móviles, donde debemos prestar especial atención a minimizar el impacto en el rendimiento.

.CSS-Houdini

The objective of the CSS-TAG Houdini Task Force (CSS Houdini) is to jointly develop features that explain the “magic” of Styling and Layout on the web. - CSS Houdini

CSS Houdini es una coleción de nuevas APIs que nos permite un acceso (parcial) al motor CSS del navegador.

CSS Houdini rendering process

Aquí podemos ver cómo cambia el tema, proponiendo una API para acceder a cada una de las fases del proceso de renderizado (los recuadros de color gris indican especificaciones planeadas, pero aún no hay nada definido).

Veamos qué APIs tenemos “disponibles” actualmente.

Esto nos permite extender CSS mediante Javascript.

.Extendiendo-CSS

La mejor manera de entender el potencial que tiene es con ejemplos, así que vamos a empezar a ver cómo trabajar con algunas de las nuevas APIs.

Para los ejemplos vamos a utilizar Google Chrome Canary y el flag Experimental Web Platform features activado (chrome://flags/#enableexperimental-web-platform-features), ya que es el navegador con más soporte de las nuevas APIs, como veremos más adelante.

& .Worklets

Un worklet es básicamente lo que vamos a utilizar para conectar con el motor de CSS del navegador. Se trata de un módulo en Javascript donde definiremos la “magia” que podremos usar en CSS. No tenemos control sobre la ejecución de los worklets, el motor de renderizado es quien hace las llamadas cuando es necesario.

El punto más interesante de un worlet es que no tiene impacto en el rendimiento, porque cada worklet funciona dentro de su propio hilo de ejecución. La potencia de Javascript con la suabidad de CSS.

Worklets

Para añadir un módulo worklet lo haremos con el método addModule.

<script>
  if ("paintWorklet" in CSS) {
    CSS.paintWorklet.addModule("PlaceholderBoxPainter.js");
  }
</script>

En este caso comprobamos que disponemos de la Paint API con el condicional if ('paintWorklet' in CSS), esto nos permitiría poder presentar una alternativa y no cargar el módulo si no está soportado.

Este sería el contenido del módulo PlaceholderBoxPainter.js

class PlaceholderBoxPainter {
  paint(ctx, size) {
    // Magic 🎩
  }
}

registerPaint("placeholder-box", PlaceholderBoxPainter);

No sufras si no entiendes algo, con el ejemplo de la Paint API encajarán las piezas.

& .Paint-API

Esta ha sido la primera API en llegar a la versión estable de Chrome (versión 65, Marzo 2018).

En el punto aterior hemos visto cómo cargar un módulo worlet, ahora vamos a ver un ejemplo de Paint API.

Lo primero que vamos ha hacer es cargar nuestro worklet, si está soportado por el navegador.

<script>
  if ("paintWorklet" in CSS) {
    CSS.paintWorklet.addModule("PlaceholderBoxPainter.js");
  } else {
    document.querySelector("html").classList.add("no-support");
    // Aquí añadirmos una clase al <html> para mostrar un mensage
    // o la alternativa como fallback
  }
</script>

<div class="placeholder"></div>

Este es el contenido de nuestro módulo PlaceholderBoxPainter.

class PlaceholderBoxPainter {
  paint(ctx, size) {
    ctx.lineWidth = 2;
    ctx.strokeColor = "#FC5D5F";

    // Dibuja una linea desde arriba a la izquierda
    // hasta abajo a la derecha.
    ctx.beginPath();
    ctx.moveTo(0, 0);
    ctx.lineTo(size.width, size.height);
    ctx.stroke();

    // Dibuja una línea desde arriba a la derecha
    // hasta abajo a la izquierda
    ctx.beginPath();
    ctx.moveTo(size.width, 0);
    ctx.lineTo(0, size.height);
    ctx.stroke();
  }
}

registerPaint("placeholder-box", PlaceholderBoxPainter);

El primer detalle que podemos ver es que estamos definiendo una clase, y no necesitaremos un transpiler como Babel, ya que el navegador lo interpreta de forma nativa.

En el ejemplo tenemos un sólo método, el paint() que es el que se ejecutará cada vez que el motor CSS necesite repintar el elemento. Tenemos dos parámetros iniciales, ctx el contexto donde podremos utilizar una versión limitada del objeto CanvasRenderingContext2D (no podremos dibujar texto o trabajar con bitmaps) y el segundo parámetro es size donde tenemos el ancho y alto del elemento.

Como indica en los comentarios del código, nuestro nuevo worklet lo que hará es pintat una X, como se suele utilizar como placeholder en los mockups para hacer referencia a una imagen.

Hemos definido ctx.lineWidth = 2 y ctx.strokeColor = '#FC5D5F' como valores por defecto para el grosor y color.

Por último, con registerPaint('placeholder-box', PlaceholderBoxPainter) estamos registrando el módulo, donde pasamos como primer parámetro el nombre que utilizaremos en el CSS y el nombre de la clase que acabamos de definir.

Veamos como queda el CSS.

.placeholder {
  background-image: paint(placeholder-box);
  ...;
}

Aquí es donde vemos una nueva función, paint(placeholder-box) con el parámetro que hemos utilizado en el registro del módulo.

Como cualquier propiedad CSS, podemos definir un fallback añadiento una líena previa.

.placeholder {
  background-image: linear-gradient(to bottom, #fc5d5f 0%, #a53d3d 100%);
  background-image: paint(placeholder-box);
  ...;
}

Aquí puedes ver el ejemplo Placeholder Box

&-CustomProperties

Vamos a hacer nuestro módulo más dinámico añadiendo Custom Properties a nuestra clase.

CSS
.placeholder {
  --line-with: 2;
  --stroke-color: #fc5d5f;
  background-image: linear-gradient(to bottom, #fc5d5f 0%, #a53d3d 100%);
  background-image: paint(placeholder-box);
  ...;
}

Aquí hemos añadido dos nuevas propiedades CSS y les hemos asignado un valor. Habrás visto que no son propiedades que reconozcas, no te preocupes en un momento hablaremos de ellas.

Worklet
class PlaceholderBoxPainter {
  static get inputProperties() {
    return ["--line-with", "--stroke-color"];
  }

  paint(ctx, size) {
    ctx.lineWidth = properties.get("--line-with");
    ctx.strokeColor = properties.get("--stroke-color");

    // Dibuja una linea desde arriba a la izquierda
    // hasta abajo a la derecha.
    ctx.beginPath();
    ctx.moveTo(0, 0);
    ctx.lineTo(size.width, size.height);
    ctx.stroke();

    // Dibuja una línea desde arriba a la derecha
    // hasta abajo a la izquierda
    ctx.beginPath();
    ctx.moveTo(size.width, 0);
    ctx.lineTo(0, size.height);
    ctx.stroke();
  }
}

registerPaint("placeholder-box", PlaceholderBoxPainter);

Como habrás visto, hemos añadido varias líneas de códido a nuestro módulo worklet.

Por un lado hemos añadido un método estático para obtener las propiedades que tengamos definidas en el contexto de la declaración CSS. Aquí definimos las propiedades que queremos utilizar en nuestro worklet, en este caso --line-with y --stroke-color.

static get inputProperties() {
  return [
    '--line-with',
    '--stroke-color'
  ];
}

También hemos cambiado la asignación de grosor y color de la línea.

ctx.lineWidth = properties.get("--line-with");
ctx.strokeColor = properties.get("--stroke-color");
JS
CSS.registerProperty({
  name: "--line-with",
  syntax: "<number>",
  inherits: false,
  initialValue: 1,
});

CSS.registerProperty({
  name: "--stroke-color",
  syntax: "<color>",
  inherits: true,
  initialValue: "rebeccapurple",
});

Aquí entra en escena otra de las nuevas APIs, la Properties & Values API (operativa en la versión Canari de Google Chrome). Con la que podemos registrar nuestras propias propiedades CSS. Creo que esta API es brutal, analicemos el objeto que le estamos pasando como argumento.

Al definir estas propiedades, las podemos utilizar como si de variables se trataran para tenerlas disponibles en nuestro módulo worklet.

Puedes ver los cambios aquí

.Soporte

Sé que esta parte es la que desanima a mucha gente, pero como cualquier avance en los desarrollos de la web tenemos un período de implementación en los navegadores. Surma mantiene la página Is Houdini ready yet‽, donde presenta una tabla donde podemos ver el soporte de las APIs en los diferentes navegadores.

Is Houdini ready yet‽

Sí, aún está todo muy verde, bueno muy rojo 😅.

La única API con Candidate Recommendation en la W3C es la Paint API, que ya está disponible en las versiones estables de Chrome y Opera, como también lo está la API de Type OM, aun con un estado de Working Draft en la W3C. Activado los flags experimentales en Canary, podremos empezar a jugar con Layout API, Properties & Values API y Animation Worklet.

.Conclusión

He leído varios artículos definiendo a CSS Houdini como la solución para desarrollar polyfills para CSS o como el Babel para CSS (cosa que creo que es un efoque totalmente erróneo).

Es posible que te estés preguntando, como seguro lo hace Naiara, Diana o Ángel 😉, si podremos definir comportamientos personalizados que no están definidos en la especificación de la W3C, ¿eso no se está alejando de la filosofía de los estándares web?

IMHO, creo que con esta propuesta, nos están dando acceso a unas nuevas APIs que nos ofrecen más “poder” y control. Y como toda nueva tecnología tendrá un período de aceptación donde veremos ejemplos de una implementación no muy consensuada… pero de momento, *disfrutemos de la opción de mejorar nuestra magia con estos nuevos hechizos.

.Referencias

Os dejo una lista de recursos donde encontraréis enlaces a documentación, artículos, vídeos y lo más importante: ejemplos donde poder analizar el comportamiento (recordad utilizar Chrome Canary con el chrome://flags/#enableexperimental- web-platform-features activado).