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. É mais ou menos mágica. Até agora, nós, a comunidade de desenvolvedores da Web, só conseguimos presenciar e observar a magia. E se quisermos inventar nossa própria magia? E se quisermos se tornar o mágico?

Entre no Houdini!

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

A coleção desses rascunhos é o que geralmente significa quando alguém fala sobre "Houdini". No momento em que este artigo foi escrito, a lista de rascunhos está incompleta, e alguns deles são apenas marcadores de posição.

As especificações

Worklets (spec)

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 nos Web Workers ao ler "worklet", não está enganado. Eles têm muitas sobreposições conceituais. Então, por que uma coisa nova se já temos trabalhadores?

O objetivo da Houdini é expor novas APIs para permitir que os desenvolvedores da Web conectem o próprio código ao mecanismo CSS e aos sistemas ao redor. Provavelmente, não é irrealista presumir que alguns desses fragmentos de código precisarão ser executados em todos os frames. Algumas têm isso por definição. Citando a especificação do Web Worker:

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

API CSS Paint (spec)

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

Worklet de composição

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

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

Em geral, o navegador usa a árvore do DOM e, com base em critérios específicos, decide criar uma camada própria para algumas ramificações e subárvores. Essas subárvores pintam a si mesmas nelas (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 índices Z, transformações 3D e assim, para gerar a imagem final 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 pintados novamente quando a página rolar um pouco. Em vez disso, você pode reutilizar as camadas do frame anterior e apenas executar novamente o compositor com a posição de rolagem atualizada. Isso torna as coisas mais rápidas. Isso nos ajuda a atingir 60 fps.

Worklet de composição.

Como o nome sugere, a worklet do compositor permite conectar-se a ele e influenciar a forma como a camada de um elemento, que já foi pintada, é posicionada e colocada sobre as outras camadas.

Para ser um pouco mais específico, você pode informar ao navegador que 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 para a própria camada e em cada frame que o código é chamado. Você pode mover sua camada Manipulando as camadas para transformar e mudar os atributos dela (como opacity), permitindo que você faça coisas elegantes a uma incrível taxa de 60 QPS.

Veja uma implementação completa para rolagem de paralaxe usando o worklet de 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 para que você possa testá-lo, obviamente com um impacto muito maior na performance.

Worklet de layout (spec)

O primeiro rascunho de especificação real foi proposto. A implementação é boa enquanto estiver ausente.

Novamente, a especificação para isso está praticamente vazia, mas o conceito é intrigante: crie seu próprio layout. O worklet de layout precisa permitir que você execute display: layout('myLayout') e execute o JavaScript para organizar os filhos de um nó na caixa do nó.

Obviamente, executar uma implementação completa do JavaScript do layout flex-box do CSS é mais lento do que executar uma implementação nativa equivalente. No entanto, é fácil imaginar um cenário em que cortes possam aumentar o desempenho. Imagine um site composto apenas por blocos, como o Windows 10 ou um layout no estilo alvenaria. O posicionamento absoluto e fixo não é usado, nem z-index, nem os elementos se sobrepõem ou têm qualquer tipo de borda ou estouro. Conseguir pular todas essas verificações no novo layout pode gerar um ganho de performance.

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

O CSSOM tipado (CSS Object Model ou Cascading Style Sheets Object Model) resolve um problema que provavelmente todos já encontramos e acabamos de aprender a lidar. Vou ilustrar com uma linha de JavaScript:

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

Estamos fazendo a matemática, convertendo um número em uma string para anexar uma unidade só para que o navegador analise essa string e a converta de volta em um número para o mecanismo do CSS. Isso fica ainda mais pior quando você manipula transformações com JavaScript. Não há mais! O CSS está prestes a ser digitado.

Esse rascunho é um dos mais avançados, e um polyfill já está em desenvolvimento. Exoneração de responsabilidade: o uso do polyfill obviamente vai aumentar o overhead de computação ainda. O objetivo é mostrar como a API é conveniente.

Em vez de strings, você vai trabalhar no StylePropertyMap de um elemento, em que cada atributo CSS tem a própria chave e um 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 durante a 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

(spec)

Você conhece as propriedades personalizadas do CSS ou o alias não oficial "Variáveis CSS"? São eles, mas com tipos! Até agora, as variáveis só podiam ter valores de string e usavam uma abordagem simples de pesquisar e substituir. Esse rascunho permite que você não apenas especifique um tipo para suas variáveis, mas também defina um valor padrão e influencie o comportamento de herança usando uma API JavaScript. Tecnicamente, isso também permitiria que propriedades personalizadas fossem 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 ser. O que é a caixa delimitadora (ou as caixas delimitadoras) quando eu renderizo a string X com a fonte Y no tamanho Z? E se eu usar anotações Ruby? Isso foi muito solicitado, e Houdini finalmente realizará esses desejos.

Mas não é só isso.

Há ainda mais especificações na lista de rascunhos de Houdini, mas o futuro deles é bastante incerto e não são muito mais que espaços reservados para ideias. Exemplos incluem comportamentos de estouro personalizados, API de extensão de sintaxe CSS, extensão do comportamento de rolagem nativa e coisas semelhantes, que permitem coisas que não eram possíveis na plataforma da Web.

Demonstrações

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