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!

O grupo de trabalho do Houdini consiste em engenheiros da Mozilla, Apple, Opera, Microsoft, HP, Intel e Google trabalhando juntos para expor determinadas partes do motor CSS a 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 para se tornarem padrões 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 workers da Web ao ler "worklet", não está errado. Eles têm muitas sobreposições conceituais. Então, por que uma coisa nova quando já temos trabalhadores?

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. Citação da 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.

Worklet do compositor

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

Embora a especificação do worklet do compositor tenha sido movida para o WICG e será iterada, é a 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 pintados novamente quando a página rola um pouco. Em vez disso, é possível reutilizar as camadas do frame anterior e executar o compositor novamente com a posição de rolagem atualizada. Isso acelera as coisas. Isso nos ajuda a alcançar 60 fps.

Worklet do compositor.

Como o nome sugere, o worklet do compositor permite que você se conecte ao compositor e influencie a forma como a camada de um elemento, que já foi pintada, é posicionada e disposta sobre as 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 na própria camada e em cada frame o 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 ainda vai demorar um pouco.

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 seu JavaScript para organizar os filhos de um nó na caixa dele.

É claro que executar uma implementação completa do JavaScript do layout flex-box do CSS é mais lento do que executar uma implementação nativa equivalente, mas é fácil imaginar um cenário em que atalhos podem gerar um ganho de desempenho. Imagine um site que consiste apenas em blocos, como o Windows 10 ou um layout de estilo 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 (especificação)

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. Vamos 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 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%) vai gerar 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 CSS personalizadas (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 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

Métricas de fonte é exatamente o que parece. 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. Os 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

O código da demonstração (demonstração ao vivo usando polyfill) está aberto.