Houdini: desmistificando o CSS

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

Aí sim para 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 e as transformaram em rascunhos de especificação que, por sua vez, deram à luz um conjunto de rascunhos de especificação de suporte e de nível mais baixo.

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

As especificações

Worklets (spec)

Os Worklets em si não são muito úteis. Eles são um conceito introduzido para tornar possível muitos dos rascunhos posteriores. Se você pensou em "Web Workers" ao ler "worklet", não está errado. Há muita sobreposição conceitual. Por que algo novo se já temos workers?

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 próximos. Provavelmente, não é realista supor que alguns desses fragmentos de código precisarão ser executados every. single. frame. Algumas têm que por definição. Com 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 do worklet. Eles são leves e de curta duração.

API CSS Paint (spec)

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

Objeto de composição

A API descrita aqui está obsoleta. O worklet de composição foi reformulado e agora é proposto como um "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 seja iterada, é essa especificação que mais me empolga. Algumas operações são terceirizadas para a placa de vídeo do computador pelo mecanismo de CSS, embora isso dependa da placa de vídeo e do dispositivo, em geral.

O navegador geralmente usa a árvore do DOM e, com base em critérios específicos, decide atribuir uma camada a algumas ramificações e subárvores. Essas subárvores se pintam sobre ela (talvez usando um worklet de pintura no futuro). Como etapa final, todas essas camadas individuais, agora pintadas, são empilhadas e posicionadas umas sobre as outras, respeitando os índices z, as 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 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 atingir 60 fps.

Objeto de composição.

Como o nome sugere, o worklet do compositor permite conectar 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, você pode informar ao navegador que quer se conectar ao processo de composição para um determinado nó do DOM e pode 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 sua camada manipulando a transformação das camadas e alterar os atributos (como opacity), o que permite fazer coisas chiques a uma incrível taxa de 60 QPS.

Confira uma implementação completa da 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 (link em inglês) para o worklet do compositor para testar o recurso, obviamente, com um impacto muito maior no desempenho.

Objeto de layout (spec)

O primeiro rascunho de especificação real foi proposto. A implementação é bem tranquila.

Novamente, a especificação para isso está praticamente vazia, mas o conceito é intrigante: criar seu próprio layout. O worklet de layout precisa permitir que você realize display: layout('myLayout') e execute 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. O posicionamento absoluto e fixo não é usado, nem z-index, e os elementos nunca se sobrepõem ou têm qualquer tipo de borda ou estouro. Conseguir pular todas essas verificações na reformulação do 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 digitado (spec)

O CSSOM tipado (CSS Object Model ou Cascading Style Sheets Object Model, em inglês) resolve um problema que provavelmente todos já encontramos e acabamos de aprender a enfrentar. 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 só para que o navegador analise essa string e a converta de volta em um número para o mecanismo CSS. Isso fica ainda mais complicado quando você manipula transformações com JavaScript. Não há mais! O CSS está prestes a começar a digitar.

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

Em vez de strings, você trabalhará na StylePropertyMap de um elemento, em que cada atributo CSS tem sua própria chave e 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. A definição de height: calc(5px + 5%) resultaria em um LengthValue{px: 5, percent: 5}. Algumas propriedades, como box-sizing, aceitam determinadas palavras-chave e, portanto, têm um tipo de valor KeywordValue. A validade desses atributos poderia 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

(spec)

Você conhece as propriedades personalizadas do CSS (ou o alias não oficial "Variáveis CSS")? Estes 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 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. 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 terá que realizar esses desejos.

Mas não é só isso.

Há ainda mais especificações na lista de rascunhos do Houdini, mas o futuro delas é um pouco incerto e não são muito mais do que marcadores de posição para ideias. Exemplos incluem comportamentos personalizados de estouro, API de extensão de sintaxe CSS, extensão do comportamento de rolagem nativo e coisas igualmente ambiciosas que permitem na plataforma da Web coisas 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).