Houdini: desmistificando o CSS

Você já pensou na quantidade de trabalho que o CSS faz? Você muda um único atributo e, de repente, todo o site aparece em um layout diferente. É uma espécie de magia. Até agora, nós, a comunidade de desenvolvedores da Web, só conseguimos testemunhar e observar a magia. E se quisermos criar nossa própria magia? E se quisermos ser o mágico?

Houdini!

A força-tarefa da Houdini é composta por engenheiros da Mozilla, Apple, Opera, Microsoft, HP, Intel e Google trabalhando juntos para expor determinadas partes do mecanismo CSS aos desenvolvedores Web. A força-tarefa está trabalhando em uma coleção de rascunhos com o objetivo de fazer com que eles sejam aceitos pelo W3C para que se tornem padrões reais da Web. Eles definiram algumas metas de alto nível, as transformaram em esboços de especificação, que, por sua vez, deram origem a um conjunto de esboços de especificação de suporte de nível inferior.

A coleta desses rascunhos é o que geralmente é mencionado quando alguém fala sobre "Houdini". No momento em que este artigo foi escrito, a lista de rascunhos estava incompleta, e alguns dos rascunhos eram meros marcadores de posição.

As especificações

Worklets (spec)

Os worklets por si só não são muito úteis. Eles são um conceito introduzido para tornar muitos dos rascunhos posteriores possíveis. Se você pensou em "Web Workers" ao ler "worklet", não está errado. Eles têm muitas sobreposições conceituais. Por que algo novo se já temos workers?

O objetivo do Houdini é expor novas APIs para que os desenvolvedores da Web conectem o próprio código ao mecanismo CSS e aos sistemas relacionados. Provavelmente não é irrealístico supor que alguns desses fragmentos de código precisarão ser executados em cada frame. Alguns deles precisam por definição. Citando a especificação do worker da Web:

Isso significa que os workers da Web não são viáveis para as coisas que o Houdini planeja fazer. Portanto, os worklets foram inventados. Os worklets usam classes ES2015 para definir uma coleção de métodos, com assinaturas predefinidas pelo tipo do worklet. Eles são leves e de curta duração.

API CSS Paint (especificação)

A API Paint é ativada por padrão no Chrome 65. Leia a introdução detalhada.

Objeto de composição

A API descrita aqui está obsoleta. O worklet do compositor foi redesenhado e agora é proposto como "worklet de animação". Leia mais sobre a iteração atual da API.

Embora a especificação do worklet do compositor tenha sido movida para o WICG e vai ser iterada, é essa especificação que mais me empolga. Algumas operações são terceirizadas para a placa de vídeo do computador pelo mecanismo CSS, embora isso dependa da placa de vídeo e do dispositivo em geral.

Um navegador geralmente pega a árvore DOM e, com base em critérios específicos, decide dar a algumas ramificações e subárvores a própria camada. Esses subárvores são pintadas nela (talvez usando um worklet de pintura no futuro). Como etapa final, todas essas camadas individuais, agora pintadas, são empilhadas e posicionadas uma sobre a outra, respeitando os índices z, transformações 3D e assim por diante, para gerar a imagem final que fica visível na tela. Esse processo é chamado de composição e é executado pelo compositor.

A vantagem do processo de composição é que você não precisa fazer com que todos os elementos sejam repintados quando a página rola um pouco. Em vez disso, você pode reutilizar as camadas do frame anterior e apenas executar o compositor novamente com a posição de rolagem atualizada. Isso torna tudo mais rápido. Isso nos ajuda a alcançar 60 fps.

Worklet do compositor.

Como o nome sugere, o worklet do compositor permite conectar-se ao compositor e influenciar a forma como a camada de um elemento, que já foi pintada, é posicionada e sobreposta às outras camadas.

Para ser um pouco mais específico, é possível informar ao navegador que você quer se conectar ao processo de composição de um determinado nó do DOM e solicitar acesso a determinados atributos, como posição de rolagem, transform ou opacity. Isso força esse elemento à própria camada e, em cada frame, seu código é chamado. Você pode mover a camada manipulando a transformação de camadas e mudando os atributos dela (como opacity), permitindo que você faça coisas legais a 60 fps.

Confira uma implementação completa de rolagem de paralaxe usando o worklet do 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 escreveu um polyfill para o worklet do compositor. Você pode testá-lo, obviamente com um impacto de desempenho muito maior.

Worklet de layout (especificação)

O primeiro rascunho de especificação real foi proposto. A implementação está longe de acontecer.

Novamente, a especificação para isso está praticamente vazia, mas o conceito é intrigante: escreva seu próprio layout! O worklet de layout permite fazer display: layout('myLayout') e executar o JavaScript para organizar os filhos de um nó na caixa dele.

É claro que executar uma implementação completa de JavaScript do layout flex-box do CSS é mais lento do que uma implementação nativa equivalente, mas é fácil imaginar um cenário em que cortar elementos pode gerar um ganho de desempenho. Imagine um site com apenas blocos, como o Windows 10 ou um layout de alvenaria. A posição absoluta e fixa não é usada, nem z-index, nem os elementos se sobrepõem ou têm qualquer tipo de borda ou overflow. A capacidade de pular todas essas verificações no redimensionamento pode resultar em um ganho de desempenho.

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 digitado (spec)

O CSSOM tipado (modelo de objetos do CSS ou modelo de objetos de folhas de estilo em cascata) aborda um problema que provavelmente todos nós já encontramos e aprendemos a lidar. Vou ilustrar com uma linha de JavaScript:

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

Estamos fazendo cálculos, convertendo um número em uma string para anexar uma unidade apenas para que o navegador analise essa string e a converta de volta em um número para o mecanismo de CSS. Isso fica ainda mais feio quando você manipula transformações com JavaScript. Não mais. O CSS está prestes a começar a digitar.

Este rascunho é um dos mais maduros, e um polyfill já está sendo trabalhado. (Isenção de responsabilidade: o uso do polyfill obviamente adiciona ainda mais sobrecarga computacional. O objetivo é mostrar como a API é conveniente.)

Em vez de strings, você vai trabalhar com o StylePropertyMap de um elemento, em que cada atributo CSS tem a própria chave e o tipo de valor correspondente. Atributos como width têm LengthValue como tipo de valor. Um LengthValue é um dicionário de todas as unidades CSS, como em, rem, px, percent e assim por diante. Definir height: calc(5px + 5%) geraria um LengthValue{px: 5, percent: 5}. Algumas propriedades, como box-sizing, aceitam apenas determinadas palavras-chave e, portanto, têm um tipo de valor KeywordValue. A validade desses atributos pode ser verificada no momento da execução.

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

Propriedades e valores

(especificação)

Você conhece as propriedades personalizadas do CSS (ou o alias não oficial "Variáveis CSS")? Elas são iguais, mas com tipos. Até agora, as variáveis só podiam ter valores de string e usavam uma abordagem simples de pesquisa e substituição. Esse rascunho permitiria não apenas especificar um tipo para suas variáveis, mas também definir um valor padrão e influenciar o comportamento de herança usando uma API JavaScript. Tecnicamente, isso também permite que propriedades personalizadas sejam animadas com transições e animações CSS padrão, o que também está sendo considerado.

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

Métricas de fonte

As métricas de fonte são exatamente o que parecem. Qual é a caixa delimitadora (ou as caixas delimitadoras) quando renderizo a string X com a fonte Y no tamanho Z? E se eu usar anotações em Ruby? Isso foi muito solicitado, e o Houdini finalmente vai realizar esses desejos.

Mas não é só isso.

Há ainda mais especificações na lista de rascunhos do Houdini, mas o futuro delas é bastante incerto e elas não são muito mais do que marcadores de posição para ideias. Exemplos incluem comportamentos de overflow personalizados, API de extensão de sintaxe CSS, extensão do comportamento de rolagem nativa e recursos ambiciosos semelhantes, que permitem coisas na plataforma da Web que não eram possíveis antes.

Demonstrações

Abri o código do código da demonstração (demonstração ao vivo usando polyfill).