CSS

20 fev, 2019

Houdini e os problemas do CSS atualmente – Parte 01

Publicidade

Talvez você nunca tenha ouvido falar no mágico Houdini, ou que ele tem a ver com CSS. Isso pode até não fazer sentido pra você agora, mas espero que depois de ler este pequeno (não tão pequeno assim) artigo sobre o que é Houdini e o futuro do CSS, você tenha um maior entendimento sobre todo esse cenário e como você pode contribuir para melhorar ainda mais a sua vida de desenvolvedor em um futuro próximo.

O que diabos é Houdini CSS?

Indo direto ao ponto, Houdini é o nome dado a uma força tarefa criada pela W3C para pensar e propor formas de criarmos nossas próprias propriedades em CSS, dando aos desenvolvedores acesso aos diferentes estágios do pipeline de renderização de uma página por parte do navegador.

Ou seja, a proposta dessa força tarefa é disponibilizar diferentes APIs para que nós possamos criar/controlar melhor as nossas propriedades ou para que possamos usufruir de polyfills/transpiladores que farão esse trabalho pra gente.

Um pouco confuso? Não vê sentido no que eu estou falando? Calma que eu vou explicar detalhadamente a razão de tudo isso e o porquê disso ser importante!

Então vamos falar dos problemas que temos hoje

Se você utiliza CSS com regularidade, acredito que talvez você já tenha enfrentado aquele velho problema de crossbrowser. Sabe quando você deseja utilizar uma determinada propriedade do CSS, mas sabe que ela não vai funcionar em todos os navegadores?

Vamos pegar, por exemplo, CSS Grid Layout, apesar de não ser nada novo e não ser hype, durante muito tempo os navegadores não deram quase nenhum suporte e, além disso, hoje em dia, se você precisa dar suporte a navegadores antigos com certeza vai precisar adotar uma estratégia pra contornar esse problema utilizando técnicas para minimizar isso, como Progressive Enhancement ou Graceful Degradation.

Outro ponto que deixa muita gente irritada é a lentidão em que o CSS evolui em comparação a JavaScript, por exemplo.

#praCegoVer — O workflow da criação de uma especificação em CSS

Como ilustra a imagem acima, a especificação é proposta, depois escreve-se essa especificação, há implementação dos vendors, repete-se o processo até que se chegue em um ponto seguro para adoção das especificações pelos navegadores e, somente depois de tudo isso, a gente usa a especificação.

Uma proposta para incluir uma nova especificação no CSS demora, literalmente, anos até que ela seja implementada nos navegadores em grande escala. Isso sem contar que ainda podemos enfrentar alguns problemas mesmo depois da adoção dos browsers, um exemplo que posso dar para ilustrar isso é a ocorrência de bugs ao utilizar algumas propriedades, o que é péssimo para nós, desenvolvedores.

Em resumo, temos dois problemas.

  • 1. Inconsistências de navegadores em relação às propriedades que as CSS nos oferecem.
  • 2. A longa demora para que novas propriedades cheguem até nós, desenvolvedores.

Temos atualmente ferramentas como pré-processadores e pós-processadores, que de certa forma nos ajudam a tentar contornar este problema. Além destes, temos Frameworks, como Bootstrap e Foundation, que também têm a proposta de facilitar as nossas vidas.

A questão aqui é que eles não resolvem, de fato, essas inconsistências – eles apenas as contornam.

Nem entrarei no mérito da questão de curva de aprendizado em relação a pré e pós-processadores, nem ao peso que os frameworks de CSS adicionam ao projeto entre outras discussões que a comunidade já teve por diversas vezes. Isso aí eu deixo a cargo de vocês.

O ponto é: CSS evolui lentamente em comparação ao JavaScript, e não há uma forma performática de se criar um polyfill ou transpilador para resolver as incompatibilidades que enfrentamos atualmente. Além disso, não há nenhuma maneira de se tocar no CSSOM, da mesma forma que fazemos com o DOM. De forma segura, o resultado disso é que atualmente não há muito o que nós, desenvolvedores, possamos fazer em relação a esse problema.

CSSOM? O que?

Não entendeu o que eu quis dizer acima? Calma, vou explicar não só o que é CSSOM, mas também qual é o caminho que o navegador faz até renderizar uma página.

Primeiro, vamos tentar entender o que o navegador faz para renderizar uma página pra gente.

#praCegoVer — Caminho de renderização de uma página por parte do navegador.

Decidi fazer de forma resumida o pipeline mostrado acima para facilitar o entendimento e a explicação de cada passo.

Aí faltaram os Worklets, mas falarei disso em um outro momento. Por enquanto trataremos desses quatro estágios.

Vamos falar separadamente de cada ponto – dessa forma podemos entender melhor o que acontece.

1. Parser:

O navegador recebe o documento (página) e começa a analisar o seu conteúdo para poder transformar em uma árvore de nó (segundo passo do pipeline). Ou seja, transformar em uma árvore DOM e CSSOM,. Isso significa que ele não faz nada além de interpretar o seu código e criar uma árvore estruturada a partir daquilo.

Um exemplo pode ser ilustrado abaixo:

Podemos ver acima uma marcação simples de HTML com uma lista, e nessa lista dois itens com um texto cada: “Olá Mundo” e “Teste”, respectivamente. Muito simples, certo?

Ao parsear este arquivo o navegador monta uma árvore, como você pode ver abaixo:

#praCegoVer — Representação do DOM a partir da imagem anterior.

O que você vê acima é basicamente a ilustração da árvore que o navegador monta resultando no DOM.

No exemplo mostrado acima temos uma estrutura bem simples, mas em qualquer página criada, a árvore montada pelo navegador é bem mais complexa, porém, acredito que, para facilitar o entendimento, esse exemplo tenha deixado as coisas mais claras na sua cabeça, certo?

Ok, temos o DOM, mas e o CSSOM? Pois é, no caso do CSSOM, a coisa é bem diferente, ou seja, a gente não trabalha com objetos em JavaScript, a gente trabalha com uma porção de textos concatenados, o que é ruim para performance. Um monte de string que a gente não tem acesso, da mesma maneira que o DOM – simples assim.

O que eu quero dizer com isso é que se você for fazer qualquer tipo de manipulação através de JavaScript, você vai passar valores do tipo string, o navegador vai fazer a conversão (se for necessário, como no caso de números) e só depois ele vai ser capaz de renderizar, e isso pode gerar problemas de performance e não é nada seguro.

Pelo menos não de uma forma segura o suficiente pra querer mexer, e por vários motivos, browsers trabalham com diferentes engines de renderização de CSS e não há APIs disponíveis no momento para tocar no CSSOM, ou pelo menos o pouco que é possível fazer nos limita de mais, sendo inviável trabalhar com o CSSOM diretamente.

#praCegoVer — Exemplo de um elemento sendo manipulado através de JavaScript

O exemplo acima mostra de forma simples, mais ou menos o que é possível fazer atualmente – nada além de atribuições e mudanças não muito complexas, sem contar que os valores são passados em strings e como mostrei acima isso não é nada bom para a performance.

É por essa razão que atualmente ficamos reféns das propriedades que os navegadores adotam em relação a CSS, sem que seja possível criar polyfills/transpiladores.

Tá, eu sei que eu misturei tudo aqui, mas é pra que você consiga entender o conceito de uma maneira mais abrangente, mas, em resumo:

O parser cria a árvore do CSSOM para que a partir disso o navegador consiga ler e construir a página que você deseja.

Se houver na sua folha de estilos alguma coisa que o navegador não conhece, ele simplesmente ignora e você não tem muito o que fazer senão procurar uma outra alternativa com o que é oferecido atualmente em relação a CSS.

Se estiver usando algum pseudo-elemento ou media queries mais específicos, se você encontrar algum problema, não há como mexer nisso, nem mesmo com JS.

2. CSSOM:

CSS Object Model é muito similar ao Document Object Model. É o responsável por processar os estilos da sua página/aplicação.

Isso significa que quem monta a árvore relacionada à folha de estilos é o CSSOM, e possibilita o controle do CSS através do JS.

Ou seja, com o CSSOM você tem APIs que facilitam manipulação, inclusão e até mesmo verificação de propriedades/valores na sua folha de estilos, mas NÃO inserção de novas propriedades. Isto é, se o CSSOM não entende o que é aquilo, você não tem nenhuma API exposta para tratar isso.

Em resumo, o CSSOM oferece um conjunto de APIs para que você possa manipular CSS através do JS, parecido com o DOM mas bem mais limitado como mostrado anteriormente.

Você conseguirá manipular as propriedades CSS que o navegador já conhece, mas não será possível enfiar coisas que ele não entende dentro do CSSOM.

E você acaba mexendo com um monte de strings concatenadas, bem diferente do DOM que possibilita alterações através de objetos JavaScript.

E qual o problema que temos com isso?

Bom, assim como no caso do DOM, é a questão de performance, ou seja, tocar no DOM ou no CSSOM com JavaScript é uma tarefa lenta e nada otimizada.

Porém, no caso do DOM, libs como React, funcionam muito bem, pois ao invés de injetar ou apagar novos elementos no DOM obrigando o navegador a reconstruir tudo, eles simplesmente renderizam os elementos uma única vez e depois conseguem alterar somente o pedaço que é necessário, sem a necessidade de reconstruir os elementos do zero.

Basicamente e bem a grosso modo, essas libs agem fazendo comparações entre os estados da sua aplicação que refletem nos elementos da sua página, que é mais rápido e eficiente.

E isso só é possível porque eles trabalham com objetos JavaScript. Pois é, como falei anteriormente, o DOM oferece a possibilidade de se trabalhar com objetos JavaScript, mas e no caso do CSSOM?

No caso do CSSOM as coisas são mais complexas de se trabalhar. Vamos lá. Lembra que eu falei que o CSSOM só trabalha com strings? Existe um motivo.

As strings existem porque quando mexemos no CSS através de JavaScript, o CSSOM injeta esses estilos diretamente no HTML, ou seja, inline. Vamos a um pequeno exemplo:

Como pode ser visto acima, eu setei um botão com uma função bastante simples: mudar a cor da tag <div> com o id “background”.

Se vocês inspecionarem o elemento, poderão notar que, quando o botão é clicado, a <div> com id=”background” tem a injeção de forma inline da propriedade background-color com a cor preta.

#praCegoVer — Tag div apenas com o id Background.

Após o clique no botão, vocês percebem uma mudança:

#praCegoVer — Tag div com o id Background e o style inline definindo a cor de fundo preta.

E se você se lembra das regras de cascateamento dos estilos, o inline sempre tem prioridade em relação a tag <style> e o arquivo em separado com as estilizações.

Essa é a razão pela qual a estilização dos elementos com JS é feita com strings, mas isso pode gerar problemas de performance em ambientes mais complexos.

Isso é bem ruim e é um dos problemas que temos atualmente. Perda de performance e dificuldade de manusearmos o CSS relacionado a nossa aplicação.

Por isso temos o Houdini, que é uma força tarefa que está pensando em resolver isso e muitas outras coisas no processo de renderização da página relacionadas a CSS, mas falarei mais dessas soluções adiante. Vamos em frente!

3. Layout:

Nesse ponto o navegador começa a analisar quais foram as definições passadas através da sua folha de estilos em relação ao posicionamento do elemento ou conjunto de elementos e começa a prepará-los para renderização.

Ele começa a fazer todos os cálculos necessários para que o elemento apareça da forma desejada e ele usa como base o viewport disponível.

Ou seja, se você definiu que o elemento terá um display Flex com comportamentos específicos, o navegador vai fazer todos os cálculos e posicionar o seu elemento com base no que foi passado.

Ocorre o mesmo com qualquer outro valor da propriedade display ou até mesmo position. Isso significa que ele vai ver o que foi definido e vai aplicar (Grid, Flex, Inline-block, Block, etc) na página/componente.

Neste ponto o navegador realiza muitas tarefas complexas em diversos contextos, lidando com diversos tipos de problemas, mas não vou detalhar aqui, pois meu objetivo é apresentar os problemas que isso causa para depois falar sobre quais sugestões o pessoal do Houdini oferece para resolver essa questão.

Quero mostrar de forma resumida como o navegador funciona a grosso modo. Sigamos em frente. Mas se quiser entender detalhadamente este ponto do processo, tem este artigo aqui,  que vai te ajudar a entender não somente este, mas todos os outros estágios do pipeline.

E não há muito o que ser feito por nós, desenvolvedores, pois não há nenhuma API para controlarmos este estágio. Neste caso, o processo é de responsabilidade completa do navegador.

Se houver alguma propriedade que ele não entende, ele vai simplesmente continuar ignorando; isto é, se ele encontrar alguma propriedade e não está preparado pra entender, o navegador vai simplesmente ignorar e seguir em frente – simples assim.

O que isso causa? Quais os problemas? Bom, vamos lá: digamos que tenha saído uma especificação atualizada com novas funcionalidades ou ainda uma propriedade completamente nova por parte da W3C.

No atual momento nós não temos nenhum acesso nem a este estágio e nem a outros pontos anteriores – isso significa que não poderemos utilizar nada novo em relação a CSS e não temos como criar qualquer tipo de polyfill/transpilador, porque não há nenhuma API disponível para trabalharmos. Não há nenhum acesso ao trabalho interno do navegador.

Neste caso teríamos que esperar uma atualização por parte dos engenheiros de browser que mantém os navegadores para resolver essa questão. Isto pode demorar (literalmente) meses ou até anos para que haja alguma solução efetiva – não preciso falar o quanto isso é ruim, certo? E nem das inconsistências que isso causa, né?

Mas e se houvesse uma forma de acessarmos este estágio e até criarmos nossas próprias propriedades de posicionamento de elementos? Pois é, falaremos sobre essas “soluções” mais adiante.

4. Paint:

O processo de paint funciona mais ou menos como o processo de Layout, mas no caso, o Paint analisa outros pontos específicos da página que você deseja renderizar, como:

  • Background color
  • Background image
  • Border
  • Elementos-filho
  • Outline

Ou seja, o processo de Paint precisa aguardar até que o processo de Layout seja finalizado para começar a renderizar aspectos mais específicos como fontes, cores, imagens, etc.

O processo de Paint não necessariamente renderiza toda a página toda vez que há alguma mudança de estado. Por exemplo, um clique que muda a cor e tamanho de fonte de um bloco.

Os navegadores são inteligentes o suficiente para processar essas pequenas mudanças sem necessariamente recarregar tudo – claro, cada um trabalhando da sua maneira.

Se fosse diferente, não teríamos a web tão dinâmica e interativa que temos hoje em dia.

Porém, é necessário alertar. Se a mudança for mais complexa e resultar em alterações em outros estágios do pipeline de renderização, obviamente isso afetará toda a página ou pelo menos uma parte da mesma terá de ser reconstruída, mas o que isso significa?

Bom, se houver alguma alteração de estado que mude o posicionamento de um elemento, por exemplo, o navegador será obrigado a voltar um estágio e rodar novamente o processo de Layout e depois o processo de Paint.

Outro exemplo, se houver algum estado que além de mexer no posicionamento (CSSOM), mexa também na árvore do DOM, adicionando ou tirando elementos, o navegador terá que parsear novamente a árvore e recomeçar tudo de novo (DOM e CSSOM).

Ou seja, quanto maior a mudança causada por uma interação do usuário com a página, mais complexo vai ser para o navegador processar tudo. Isso é ruim, de novo, para a performance e consequentemente para a experiência do usuário.

Cada um com sua responsabilidade, mas o mesmo acontece com o processo de Layout, lembra que eu falei que eles são similares? Pois é, basicamente muitas mudanças ou alterações causam um efeito negativo na aplicação.

Como eu falei antes, no caso do DOM, bibliotecas como React conseguem trabalhar bem com esse tipo de problema e resolvem de maneira bem eficiente isso, pois o DOM é altamente manipulável e você pode controlar tudo com objetos JavaScript, mas e no caso do CSSOM? Pois é, não há muito o que fazer. Basicamente você depende do processamento do navegador para lidar com a estilização da sua aplicação.

Mas e se você pudesse ter APIs que te possibilitem controlar e resolver esses problemas sem depender única e exclusivamente da engine do navegador? É exatamente isso que o pessoal do Houdini está desenvolvendo.

Isto é, alternativas para que nós, desenvolvedores, tenhamos o poder de acessar esses estágios de maneira mais efetiva e profunda.

E isso inclui o processo de Paint. Inclusive existem até pequenos exemplos no repositório do Google Labs (tem exemplo de praticamente todas as APIs) que mostram como estes funcionam.

O link é este aqui, porém, fiquem tranquilos, nos próximos artigos irei explicar de maneira mais detalhada cada um destes.

Concluindo

Nesse artigo eu quis mostrar quais problemas que temos atualmente por conta da falta de APIs que nos possibilitem um maior controle do pipeline de renderização e aplicação de estilos à nossa página.

Nos próximos artigos quero detalhar cada API e mostrar exemplos mais práticos e palpáveis. Dessa forma quero ajudar você a entender quais objetivos a W3C tem com essa iniciativa e como podemos ajudar e contribuir.

Qualquer dúvida só em contato pelo Twitter (@RaffaelDantass) ou deixe nos comentários.

Valeu!