Montando seletores CSS eficiêntes

Artigo que discute sobre a forma como os navegadores reconhecem os seletores descendentes do CSS e sobre os aspectos que devem ser levados em conta para montar seletores eficiêntes.

Introdução

Recentemente, li o artigo How to write efficient css selectors (Como escrever seletores CSS eficiêntes) e aprendi que os seletores descendentes do CSS na verdade são avaliados da direita para a esquerda e não da esquerda para direita, como eu imaginava. Este é um pequeno detalhe, mas que faz toda a diferença na hora de montar seletores CSS de forma eficiênte para que o navegador renderize uma página com alta performance.

Neste artigo, vamos entender melhor como funciona esse processo de aplicação dos estilos pelas folhas de estilo e debater sobre a criação de seletores CSS eficiêntes.

Seletores Descendentes do CSS

Para refrescar a memória, os Seletores Descendentes do CSS são aqueles que especificam os estilos a serem aplicados a um elemento que é um filho não imediato de outro elemento. Por exemplo:

#nav a {
  color: #006600;
}

No exemplo acima, o seletor descendente "#nav a" diz para aplicar um estilo a todos os links (tag "a") descendentes do elemento que possui o id "nav".

O processo de renderização de páginas

A W3C, através da especificação do CSS 2.1, apresenta um procedimento para determinar o valor de uma propriedade de estilo de um elemento. Estes passos sugerem que os navegadores percorram os elementos do documento e, para cada elemento percorrido, analisem as folhas de estilos para determinar quais os valores de cada propriedade vão ter aquele elemento.

Supondo que os navegadores realmente percorrem os elementos para depois determinar os seus estilos, fica evidente o custo de um seletor composto por muitos seletores descendentes. Por exemplo:

/* Exemplo de seletor com alto custo */
#container #nav ul li a {
  ...
}

Considerando o exemplo acima, quando o navegador renderizar a página, ele primeiro vai percorrer os elementos e, quando encontrar um link (tag "a"), ele vai checar se o seletor do exemplo casa com o elemento encontrado. Ou seja, para todos os links do site, ele vai checar se aquele link é descendente de um elemento "li", que por sua vez precisa ser descendente de um elemnto "ul", que é descendente de um elemento com id "nav", que é descendente de um elemento com id "container". Portanto, há um custo enorme, já que, para saber se o seletor casa ou não, é preciso analizar todos os ancestrais daquele link, até chegar na raíz do documento html.

A contradição da performance vs estruturação do documento

Sabendo que um seletor CSS composto por muitos seletores descendentes possui alto custo e, portanto, baixa performance, fica óbvio que para se obter uma boa performance é necessário evitá-los. Porém, evitá-los implica em uma reestruturação da forma com que os seletores e o documento são criados.

Analizando aquele exemplo de seletor com alto custo, poderiamos reescrevê-lo para algo do tipo:

/* Exemplo de seletor de baixo custo */
.nav-link {
  ...
}

Note que este é apenas um seletor de classe, sem descendente. Porém, para utilizá-lo, seria necessário aplicar a classe "nav-link" em todos os links que desejarmos. Isso, a meu ver, é contraditório, pois estariamos "poluindo" nosso documento com muitas classes para uso muito específico, tornando o documento HTML maior e prejudicando a manutenção do mesmo. Outro pequeno inconveniente é que, para seguir esta abordagem, precisariamos colocar classes em praticamente todos os elementos da página para que o layout fosse feito em função do documento e não o documento em função dos estilos que ele precisa.

Um equilibrio, para este caso, seria montar um seletor assim:

/* Exemplo de seletor com custo moderado */
.container-nav-list a {
  ...
}

Neste caso, seria necessário aplicar a classe "container-nav-list" apenas no elemento "ul" desejado, e automaticamente os links descendentes daquela lista teriam um estilo próprio. O nome da classe pode ser montado com a combinação dos elementos que usamos no exemplo inicial, mas também pode ser algo mais genérico, para poder ser reaproveitada em outras listas.

Observe que, reestruturando o HTML, podemos extinguir completamente os seletores descendentes, mas isso, a meu ver, nem sempre é o ideal. E você, qual a sua opinião?

Observação: embora os seletores CSS sejam avaliados da direita para esquerda, os seletores do jQuery, por exemplo, não seguem a mesma lógica. Eles caminham da esquerda para direita e os seletores descendentes, neste caso, auxiliam o jQuery para percorrer apenas o trecho que interessa da árvore do documento.

5 comentários

Régis disse...

pego o conteúdo e escrevo o HTML semântico; depois vejo como ficou na tela; coloco os seletores conforme o outline; passo num validador; o SEO me ajuda no ARIA, preencho os titles e aria label; quando parto no SASS eu sempre começo estilizando pelos seletores e dentro as tags [a abordagem moderada que tu te refere]... a OO não me deixa ter a compulsão de criar um monte de classes e me guia pra o reuso

Rubens Takiguti Ribeiro (autor do blog) disse...

Régis, obrigado pelo comentário. É de grande valia.

Porém, toca num ponto um pouco nebuloso. De fato, utilizar as classes CSS com a mesma visão da OO te impede de sair criando muitas classes, mas, por outro lado, acaba te obrigando a colocar class em muitos elementos, dependendo do caso, gerando custo extra. Isso não chega a me incomodar num cenário de aplicação web gerada dinamicamente, com helpers, mas vejo como crítica num cenário web estático, especialmente para manutenções.

O grande ponto que quis discutir no artigo foi da necessidade de um elemento do documento (por exemplo, com a class "aviso") tenha um estilo diferente do padrão quando está dentro de outro elemento. Por exemplo, os elementos com class "aviso" são amarelos por padrão, mas numa determinada página, que tem o elemebto body com id "login", eles tem outra cor. Neste caso, acho extremamente conveniente aproveitar o seletor descendente (#login .aviso), porém, vai contra o princípio de criação de seletores eficientes. Na visão eficiênte, seria necessário criar uma outra classe para ser colocada nos elementos que já possuem a class "aviso", e aí teriam mais de uma class (por exemplo "aviso aviso-login"). Mas, neste caso, haveria um custo adicional de ajustar esta class. Custo que foi gerado exclusivamente em função desta "dificuldade" dos navegadores. E isso, de certa forma, é o que me incomoda.

Particularmente, sou contra um "aviso" ter outra cor numa página específica. Prefiro padrão para tudo. Porém, na prática, sempre existem clientes que querem estilos com peculiaridades para cada página e cuja decisão não pode ser ignorada.