Manipulando CSS não usado em Sass para melhorar o desempenho

Publicados: 2022-03-10
Resumo rápido ↬ Você sabe o impacto que o CSS não utilizado tem no desempenho? Spoiler: É muito! Neste artigo, exploraremos uma solução orientada a Sass para lidar com CSS não utilizado, evitando a necessidade de dependências complicadas do Node.js envolvendo navegadores headless e emulação DOM.

No desenvolvimento de front-end moderno, os desenvolvedores devem ter como objetivo escrever CSS que seja escalável e sustentável. Caso contrário, eles correm o risco de perder o controle sobre as especificidades, como a especificidade da cascata e do seletor, à medida que a base de código cresce e mais desenvolvedores contribuem.

Uma maneira de conseguir isso é através do uso de metodologias como CSS Orientado a Objetos (OOCSS), que em vez de organizar CSS em torno do contexto da página, incentiva a separação da estrutura (sistemas de grade, espaçamento, larguras, etc.) da decoração (fontes, marca, cores, etc.).

Portanto, nomes de classes CSS, como:

  • .blog-right-column
  • .latest_topics_list
  • .job-vacancy-ad

São substituídos por alternativas mais reutilizáveis, que aplicam os mesmos estilos CSS, mas não estão vinculadas a nenhum contexto específico:

  • .col-md-4
  • .list-group
  • .card

Essa abordagem é comumente implementada com a ajuda de uma estrutura Sass, como Bootstrap, Foundation, ou cada vez mais frequentemente, uma estrutura sob medida que pode ser moldada para melhor se adequar ao projeto.

Mais depois do salto! Continue lendo abaixo ↓

Então agora estamos usando classes CSS escolhidas a dedo de uma estrutura de padrões, componentes de interface do usuário e classes de utilitários. O exemplo abaixo ilustra um sistema de grade comum construído usando Bootstrap, que empilha verticalmente e, quando o ponto de interrupção md é atingido, alterna para um layout de 3 colunas.

 <div class="container"> <div class="row"> <div class="col-12 col-md-4">Column 1</div> <div class="col-12 col-md-4">Column 2</div> <div class="col-12 col-md-4">Column 3</div> </div> </div>

Classes geradas programaticamente, como .col-12 e .col-md-4 são usadas aqui para criar esse padrão. Mas e quanto a .col-1 a .col-11 , .col-lg-4 , .col-md-6 ou .col-sm-12 ? Estes são todos exemplos de classes que serão incluídas na folha de estilo CSS compilada, baixada e analisada pelo navegador, apesar de não estar em uso.

Neste artigo, começaremos explorando o impacto que o CSS não utilizado pode ter nas velocidades de carregamento da página. Em seguida, abordaremos algumas soluções existentes para removê-las das folhas de estilo, seguindo com minha própria solução orientada a Sass.

Medindo o impacto de classes CSS não utilizadas

Embora eu adore Sheffield United, os poderosos blades, o CSS do site deles é empacotado em um único arquivo minificado de 568kb, que chega a 105kb mesmo quando compactado em gzip. Isso parece muito.

Este é o site do Sheffield United, meu time de futebol local (isso é futebol para você nas colônias). (Visualização grande)

Vamos ver quanto desse CSS é realmente usado por em sua página inicial? Uma rápida pesquisa no Google revela muitas ferramentas online para o trabalho, mas prefiro usar a ferramenta de cobertura no Chrome, que pode ser executada diretamente do DevTools do Chrome. Vamos dar uma volta.

A maneira mais rápida de acessar a ferramenta de cobertura nas Ferramentas do desenvolvedor é usar o atalho de teclado Control+Shift+P ou Command+Shift+P (Mac) para abrir o menu de comandos. Nele, digite coverage e selecione a opção 'Mostrar Cobertura'. (Visualização grande)

Os resultados mostram que apenas 30kb de CSS da folha de estilo de 568kb são usados ​​pela página inicial, com os 538kb restantes relacionados aos estilos necessários para o restante do site. Isso significa que 94,8% do CSS não é usado.

Você pode ver horários como esses para qualquer ativo no Chrome em Ferramentas do desenvolvedor via rede -> Clique no seu ativo -> guia Tempo. (Visualização grande)

O CSS faz parte do caminho crítico de renderização de uma página da Web, que envolve todas as diferentes etapas que um navegador deve concluir antes de iniciar a renderização da página. Isso torna o CSS um recurso de bloqueio de renderização.

Então, com isso em mente, ao carregar o site do Sheffield United usando uma boa conexão 3G, leva 1,15s antes que o CSS seja baixado e a renderização da página possa começar. Isto é um problema.

O Google também reconheceu isso. Ao executar uma auditoria do Lighthouse, on-line ou por meio do navegador, qualquer possível economia de tempo de carregamento e tamanho de arquivo que possa ser feita pela remoção de CSS não utilizado é destacada.

No Chrome (e no Chromium Edge), você pode corrigir as auditorias do Google Lighthouse clicando na guia Auditoria nas ferramentas do desenvolvedor. (Visualização grande)

Soluções existentes

O objetivo é determinar quais classes CSS não são necessárias e removê-las da folha de estilo. Estão disponíveis soluções existentes que tentam automatizar este processo. Eles geralmente podem ser usados ​​por meio de um script de compilação Node.js ou por meio de executores de tarefas, como o Gulp. Esses incluem:

  • UNCSS
  • Purificar CSS
  • Limpar CSS

Estes geralmente funcionam de maneira semelhante:

  1. No bulld, o site é acessado por meio de um navegador headless (ex.: marionetista) ou emulação DOM (ex.: jsdom).
  2. Com base nos elementos HTML da página, qualquer CSS não utilizado é identificado.
  3. Isso é removido da folha de estilo, deixando apenas o necessário.

Embora essas ferramentas automatizadas sejam perfeitamente válidas e eu tenha usado muitas delas em vários projetos comerciais com sucesso, encontrei algumas desvantagens ao longo do caminho que valem a pena compartilhar:

  • Se os nomes de classe contiverem caracteres especiais como '@' ou '/', eles podem não ser reconhecidos sem escrever algum código personalizado. Eu uso o BEM-IT de Harry Roberts, que envolve a estruturação de nomes de classes com sufixos responsivos como: u-width-6/12@lg , então já bati esse problema antes.
  • Se o site usa implantação automatizada, pode retardar o processo de compilação, especialmente se você tiver muitas páginas e muito CSS.
  • O conhecimento sobre essas ferramentas precisa ser compartilhado por toda a equipe, caso contrário pode haver confusão e frustração quando o CSS está misteriosamente ausente nas folhas de estilo de produção.
  • Se o seu site tiver muitos scripts de terceiros em execução, às vezes quando abertos em um navegador headless, eles não funcionam bem e podem causar erros no processo de filtragem. Portanto, normalmente você precisa escrever um código personalizado para excluir scripts de terceiros quando um navegador headless é detectado, o que, dependendo da sua configuração, pode ser complicado.
  • Geralmente, esses tipos de ferramentas são complicados e introduzem muitas dependências extras no processo de compilação. Como é o caso de todas as dependências de terceiros, isso significa confiar no código de outra pessoa.

Com esses pontos em mente, me fiz uma pergunta:

Usando apenas Sass, é possível lidar melhor com o Sass que compilamos para que qualquer CSS não utilizado possa ser excluído, sem recorrer apenas a excluir grosseiramente as classes de origem no Sass?

Alerta de spoiler: a resposta é sim. Aqui está o que eu inventei.

Solução Orientada para Sass

A solução precisa fornecer uma maneira rápida e fácil de escolher o que Sass deve ser compilado, sendo simples o suficiente para não adicionar mais complexidade ao processo de desenvolvimento ou impedir que os desenvolvedores aproveitem coisas como CSS gerado programaticamente Aulas.

Para começar, há um repositório com scripts de compilação e alguns estilos de amostra que você pode clonar aqui.

Dica: Se você ficar preso, sempre poderá fazer referência cruzada com a versão concluída no branch master.

cd no repositório, execute npm install e, em seguida, npm run build para compilar qualquer Sass em CSS conforme necessário. Isso deve criar um arquivo css de 55kb no diretório dist.

Se você abrir /dist/index.html em seu navegador da Web, deverá ver um componente bastante padrão, que ao clicar se expande para revelar algum conteúdo. Você também pode ver isso aqui, onde as condições reais da rede serão aplicadas, para que você possa executar seus próprios testes.

Usaremos esse componente expansor de interface do usuário como nosso objeto de teste ao desenvolver a solução orientada a Sass para lidar com CSS não utilizado. (Visualização grande)

Filtrando no Nível Parcial

Em uma configuração típica do SCSS, você provavelmente terá um único arquivo de manifesto (por exemplo: main.scss no repositório), ou um por página (por exemplo: index.scss , products.scss , contact.scss ) onde os parciais do framework são importados. Seguindo os princípios OOCSS, essas importações podem ser algo assim:

Exemplo 1

 /* Undecorated design patterns */ @import 'objects/box'; @import 'objects/container'; @import 'objects/layout'; /* UI components */ @import 'components/button'; @import 'components/expander'; @import 'components/typography'; /* Highly specific helper classes */ @import 'utilities/alignments'; @import 'utilities/widths';

Se algum desses parciais não estiver em uso, a maneira natural de filtrar esse CSS não utilizado seria simplesmente desabilitar a importação, o que impediria que fosse compilado.

Por exemplo, se estiver usando apenas o componente expansor, o manifesto normalmente se pareceria com o abaixo:

Exemplo 2

 /* Undecorated design patterns */ // @import 'objects/box'; // @import 'objects/container'; // @import 'objects/layout'; /* UI components */ // @import 'components/button'; @import 'components/expander'; // @import 'components/typography'; /* Highly specific helper classes */ // @import 'utilities/alignments'; // @import 'utilities/widths';

No entanto, de acordo com o OOCSS, estamos separando a decoração da estrutura para permitir a reutilização máxima, portanto, é possível que o expansor exija CSS de outros objetos, componentes ou classes de utilitário para renderizar corretamente. A menos que o desenvolvedor esteja ciente desses relacionamentos inspecionando o HTML, ele pode não saber importar esses parciais, portanto, nem todas as classes necessárias seriam compiladas.

No repositório, se você observar o HTML do expansor em dist/index.html , esse parece ser o caso. Ele usa estilos dos objetos de caixa e layout, o componente de tipografia e utilitários de largura e alinhamento.

dist/index.html

 <div class="c-expander"> <div class="o-box o-box--spacing-small c-expander__trigger c-expander__header" tabindex="0"> <div class="o-layout o-layout--fit u-flex-middle"> <div class="o-layout__item u-width-grow"> <h2 class="c-type-echo">Toggle Expander</h2> </div> <div class="o-layout__item u-width-shrink"> <div class="c-expander__header-icon"></div> </div> </div> </div> <div class="c-expander__content"> <div class="o-box o-box--spacing-small"> Lorum ipsum <p class="u-align-center"> <button class="c-expander__trigger c-button">Close</button> </p> </div> </div> </div>

Vamos resolver esse problema que está esperando para acontecer oficializando esses relacionamentos dentro do próprio Sass, então uma vez que um componente é importado, todas as dependências também serão importadas automaticamente. Dessa forma, o desenvolvedor não tem mais a sobrecarga extra de ter que auditar o HTML para saber o que mais eles precisam importar.

Mapa de importações programáticas

Para que esse sistema de dependência funcione, em vez de simplesmente comentar em instruções @import no arquivo manifest, a lógica Sass precisará ditar se as parciais serão compiladas ou não.

Em src/scss/settings , crie uma nova parcial chamada _imports.scss , @import -a em settings/_core.scss e crie o seguinte mapa SCSS:

src/scss/settings/_core.scss

 @import 'breakpoints'; @import 'spacing'; @import 'imports';

src/scss/settings/_imports.scss

 $imports: ( object: ( 'box', 'container', 'layout' ), component: ( 'button', 'expander', 'typography' ), utility: ( 'alignments', 'widths' ) );

Este mapa terá a mesma função que o manifesto de importação no exemplo 1.

Exemplo 4

 $imports: ( object: ( //'box', //'container', //'layout' ), component: ( //'button', 'expander', //'typography' ), utility: ( //'alignments', //'widths' ) );

Ele deve se comportar como um conjunto padrão de @imports , pois se certos parciais forem comentados (como o acima), esse código não deve ser compilado na compilação.

Mas como queremos importar dependências automaticamente, também devemos poder ignorar este mapa nas circunstâncias corretas.

Render Mixin

Vamos começar a adicionar alguma lógica Sass. Crie _render.scss em src/scss/tools e adicione seu @import a tools/_core.scss .

No arquivo, crie um mixin vazio chamado render() .

src/scss/tools/_render.scss

 @mixin render() { }

No mixin, precisamos escrever Sass que faz o seguinte:

  • render()
    “Ei, $imports , tempo bom, não é? Diga, você tem o objeto container em seu mapa?"
  • $importações
    false
  • render()
    “É uma pena, parece que não será compilado então. Que tal o componente do botão?”
  • $importações
    true
  • render()
    "Agradável! Esse é o botão que está sendo compilado então. Diga oi para a esposa por mim.”

No Sass, isso se traduz no seguinte:

src/scss/tools/_render.scss

 @mixin render($name, $layer) { @if(index(map-get($imports, $layer), $name)) { @content; } }

Basicamente, verifique se a parcial está incluída na variável $imports e, em caso afirmativo, renderize-a usando a diretiva @content do Sass, que nos permite passar um bloco de conteúdo para o mixin.

Usaríamos assim:

Exemplo 5

 @include render('button', 'component') { .c-button { // styles et al } // any other class declarations }

Antes de usar este mixin, há uma pequena melhoria que podemos fazer nele. O nome da camada (objeto, componente, utilitário etc.) é algo que podemos prever com segurança, então temos a oportunidade de simplificar um pouco as coisas.

Antes da declaração do mixin de renderização, crie uma variável chamada $layer e remova a variável com nome idêntico dos parâmetros do mixins. Igual a:

src/scss/tools/_render.scss

 $layer: null !default; @mixin render($name) { @if(index(map-get($imports, $layer), $name)) { @content; } }

Agora, nas parciais _core.scss onde os objetos, componentes e utilitários @imports estão localizados, redeclare essas variáveis ​​para os seguintes valores; representando o tipo de classes CSS sendo importadas.

src/scss/objects/_core.scss

 $layer: 'object'; @import 'box'; @import 'container'; @import 'layout';

src/scss/components/_core.scss

 $layer: 'component'; @import 'button'; @import 'expander'; @import 'typography';

src/scss/utilities/_core.scss

 $layer: 'utility'; @import 'alignments'; @import 'widths';

Dessa forma, quando usamos o mixin render() , tudo o que precisamos fazer é declarar o nome parcial.

Enrole o mixin render() em torno de cada declaração de objeto, componente e classe de utilidade, conforme abaixo. Isso lhe dará um uso de mixin de renderização por parcial.

Por exemplo:

src/scss/objects/_layout.scss

 @include render('button') { .c-button { // styles et al } // any other class declarations }

src/scss/components/_button.scss

 @include render('button') { .c-button { // styles et al } // any other class declarations }

Nota: Para utilities/_widths.scss , envolver a função render() em torno de toda a parcial causará erro na compilação, já que em Sass você não pode aninhar declarações de mixin dentro de chamadas de mixin. Em vez disso, apenas envolva o mixin render() em torno das chamadas create-widths() , como abaixo:

 @include render('widths') { // GENERATE STANDARD WIDTHS //--------------------------------------------------------------------- // Example: .u-width-1/3 @include create-widths($utility-widths-sets); // GENERATE RESPONSIVE WIDTHS //--------------------------------------------------------------------- // Create responsive variants using settings.breakpoints // Changes width when breakpoint is hit // Example: .u-width-1/3@md @each $bp-name, $bp-value in $mq-breakpoints { @include mq(#{$bp-name}) { @include create-widths($utility-widths-sets, \@, #{$bp-name}); } } // End render }

Com isso, na compilação, apenas os parciais referenciados em $imports serão compilados.

Misture e combine quais componentes são comentados em $imports e execute npm run build no terminal para experimentar.

Mapa de dependências

Agora estamos importando parciais programaticamente, podemos começar a implementar a lógica de dependência.

Em src/scss/settings , crie uma nova parcial chamada _dependencies.scss , @import em settings/_core.scss , mas certifique-se de que esteja depois de _imports.scss . Em seguida, crie o seguinte mapa SCSS:

src/scss/settings/_dependencies.scss

 $dependencies: ( expander: ( object: ( 'box', 'layout' ), component: ( 'button', 'typography' ), utility: ( 'alignments', 'widths' ) ) );

Aqui, declaramos dependências para o componente expansor, pois ele exige que os estilos de outros parciais sejam renderizados corretamente, como visto em dist/index.html.

Usando esta lista, podemos escrever a lógica que significaria que essas dependências sempre seriam compiladas junto com seus componentes dependentes, não importando o estado da variável $imports .

Abaixo de $dependencies , crie um mixin chamado dependency-setup() . Aqui, faremos as seguintes ações:

1. Percorra o mapa de dependências.

 @mixin dependency-setup() { @each $componentKey, $componentValue in $dependencies { } }

2. Se o componente puder ser encontrado em $imports , percorra sua lista de dependências.

 @mixin dependency-setup() { $components: map-get($imports, component); @each $componentKey, $componentValue in $dependencies { @if(index($components, $componentKey)) { @each $layerKey, $layerValue in $componentValue { } } } }

3. Se a dependência não estiver em $imports , adicione-a.

 @mixin dependency-setup() { $components: map-get($imports, component); @each $componentKey, $componentValue in $dependencies { @if(index($components, $componentKey)) { @each $layerKey, $layerValue in $componentValue { @each $partKey, $partValue in $layerValue { @if not index(map-get($imports, $layerKey), $partKey) { $imports: map-merge($imports, ( $layerKey: append(map-get($imports, $layerKey), '#{$partKey}') )) !global; } } } } } }

Incluir o sinalizador !global diz ao Sass para procurar a variável $imports no escopo global, em vez do escopo local do mixin.

4. Depois é só chamar o mixin.

 @mixin dependency-setup() { ... } @include dependency-setup();

Então, o que temos agora é um sistema de importação parcial aprimorado, onde se um componente é importado, um desenvolvedor não precisa importar manualmente cada uma de suas várias parciais de dependência também.

Configure a variável $imports para que apenas o componente expansor seja importado e execute npm run build . Você deve ver no CSS compilado as classes expansoras junto com todas as suas dependências.

No entanto, isso não traz nada de novo para a tabela em termos de filtragem de CSS não utilizado, já que a mesma quantidade de Sass ainda está sendo importada, programática ou não. Vamos melhorar isso.

Importação de dependência aprimorada

Um componente pode exigir apenas uma única classe de uma dependência, então continuar e importar todas as classes dessa dependência apenas leva ao mesmo inchaço desnecessário que estamos tentando evitar.

Podemos refinar o sistema para permitir uma filtragem mais granular em uma base de classe por classe, para garantir que os componentes sejam compilados apenas com as classes de dependência necessárias.

Com a maioria dos padrões de design, decorados ou não, existe uma quantidade mínima de classes que precisam estar presentes na folha de estilo para que o padrão seja exibido corretamente.

Para nomes de classe usando uma convenção de nomenclatura estabelecida, como BEM, normalmente as classes nomeadas “Block” e “Element” são necessárias no mínimo, com “Modifiers” sendo tipicamente opcionais.

Observação: as classes de utilidade normalmente não seguiriam a rota BEM, pois são isoladas por natureza devido ao seu foco estreito.

Por exemplo, dê uma olhada neste objeto de mídia, que é provavelmente o exemplo mais conhecido de CSS orientado a objetos:

 <div class="o-media o-media--spacing-small"> <div class="o-media__image"> <img src="url" alt="Image"> </div> <div class="o-media__text"> Oh! </div> </div>

Se um componente tiver esse conjunto como dependência, faz sentido sempre compilar .o-media , .o-media__image e .o-media__text , pois essa é a quantidade mínima de CSS necessária para fazer o padrão funcionar. No entanto, com .o-media--spacing-small sendo um modificador opcional, ele só deve ser compilado se dissermos explicitamente, pois seu uso pode não ser consistente em todas as instâncias de objetos de mídia.

Modificaremos a estrutura do mapa $dependencies para nos permitir importar essas classes opcionais, incluindo uma maneira de importar apenas o bloco e o elemento caso nenhum modificador seja necessário.

Para começar, verifique o HTML do expansor em dist/index.html e anote as classes de dependência em uso. Registre-os no mapa $dependencies , conforme abaixo:

src/scss/settings/_dependencies.scss

 $dependencies: ( expander: ( object: ( box: ( 'o-box--spacing-small' ), layout: ( 'o-layout--fit' ) ), component: ( button: true, typography: ( 'c-type-echo', ) ), utility: ( alignments: ( 'u-flex-middle', 'u-align-center' ), widths: ( 'u-width-grow', 'u-width-shrink' ) ) ) );

Onde um valor é definido como true, vamos traduzir isso em “Somente classes de bloco e nível de elemento de compilação, sem modificadores!”.

A próxima etapa envolve a criação de uma variável whitelist para armazenar essas classes e quaisquer outras classes (não dependentes) que desejamos importar manualmente. Em /src/scss/settings/imports.scss , após $imports , crie uma nova lista Sass chamada $global-filter .

src/scss/settings/_imports.scss

 $global-filter: ();

A premissa básica por trás do $global-filter é que quaisquer classes armazenadas aqui serão compiladas na compilação, desde que a parcial a que pertencem seja importada via $imports .

Esses nomes de classe podem ser adicionados programaticamente se forem uma dependência de componente ou podem ser adicionados manualmente quando a variável é declarada, como no exemplo abaixo:

Exemplo de filtro global

 $global-filter: ( 'o-box--spacing-regular@md', 'u-align-center', 'u-width-6/12@lg' );

Em seguida, precisamos adicionar um pouco mais de lógica ao mixin @dependency-setup , para que quaisquer classes referenciadas em $dependencies sejam automaticamente adicionadas à nossa lista de permissões $global-filter .

Abaixo deste bloco:

src/scss/settings/_dependencies.scss

 @if not index(map-get($imports, $layerKey), $partKey) { }

...adicione o seguinte trecho.

src/scss/settings/_dependencies.scss

 @each $class in $partValue { $global-filter: append($global-filter, '#{$class}', 'comma') !global; }

Isso percorre todas as classes de dependência e as adiciona à lista de permissões $global-filter .

Neste ponto, se você adicionar uma instrução @debug abaixo do mixin dependency-setup() para imprimir o conteúdo de $global-filter no terminal:

 @debug $global-filter;

... você deve ver algo assim na compilação:

 DEBUG: "o-box--spacing-small", "o-layout--fit", "c-box--rounded", "true", "true", "u-flex-middle", "u-align-center", "u-width-grow", "u-width-shrink"

Agora que temos uma lista branca de classe, precisamos aplicar isso em todos os diferentes parciais de objetos, componentes e utilitários.

Crie uma nova parcial chamada _filter.scss em src/scss/tools e adicione um @import ao arquivo _core.scss da camada de ferramentas.

Nesta nova parcial, criaremos um mixin chamado filter() . Usaremos isso para aplicar a lógica, o que significa que as classes só serão compiladas se incluídas na variável $global-filter .

Começando simples, crie um mixin que aceite um único parâmetro — a $class que o filtro controla. Em seguida, se a $class estiver incluída na whitelist $global-filter , permita que ela seja compilada.

src/scss/tools/_filter.scss

 @mixin filter($class) { @if(index($global-filter, $class)) { @content; } }

Em uma parcial, envolveríamos o mixin em torno de uma classe opcional, assim:

 @include filter('o-myobject--modifier') { .o-myobject--modifier { color: yellow; } }

Isso significa que a .o-myobject--modifier só seria compilada se estivesse incluída em $global-filter , que pode ser definida diretamente ou indiretamente por meio do que está definido em $dependencies .

Percorra o repositório e aplique o mixin filter() a todas as classes modificadoras opcionais nas camadas de objetos e componentes. Ao lidar com o componente de tipografia ou a camada de utilitários, como cada classe é independente da próxima, faria sentido torná-las todas opcionais, para que possamos habilitar as classes conforme precisamos delas.

Aqui estão alguns exemplos:

src/scss/objects/_layout.scss

 @include filter('o-layout__item--fit-height') { .o-layout__item--fit-height { align-self: stretch; } }

src/scss/utilities/_alignments.scss

 // Changes alignment when breakpoint is hit // Example: .u-align-left@md @each $bp-name, $bp-value in $mq-breakpoints { @include mq(#{$bp-name}) { @include filter('u-align-left@#{$bp-name}') { .u-align-left\@#{$bp-name} { text-align: left !important; } } @include filter('u-align-center@#{$bp-name}') { .u-align-center\@#{$bp-name} { text-align: center !important; } } @include filter('u-align-right@#{$bp-name}') { .u-align-right\@#{$bp-name} { text-align: right !important; } } } }

Nota: Ao adicionar os nomes de classe do sufixo responsivo ao mixin filter() , você não precisa escapar do símbolo '@' com um '\'.

Durante este processo, ao aplicar o mixin filter() aos parciais, você pode (ou não) ter notado algumas coisas.

Turmas Agrupadas

Algumas classes na base de código são agrupadas e compartilham os mesmos estilos, por exemplo:

src/scss/objects/_box.scss

 .o-box--spacing-disable-left, .o-box--spacing-horizontal { padding-left: 0; }

Como o filtro aceita apenas uma única classe, ele não leva em conta a possibilidade de que um bloco de declaração de estilo possa ser para mais de uma classe.

Para explicar isso, expandiremos o mixin filter() para que, além de uma única classe, seja possível aceitar uma lista de argumentos Sass contendo muitas classes. Igual a:

src/scss/objects/_box.scss

 @include filter('o-box--spacing-disable-left', 'o-box--spacing-horizontal') { .o-box--spacing-disable-left, .o-box--spacing-horizontal { padding-left: 0; } }

Portanto, precisamos informar ao mixin filter() que, se uma dessas classes estiver no $global-filter , você poderá compilar as classes.

Isso envolverá lógica adicional para verificar o argumento $class do mixin, respondendo com um loop se um arglist for passado para verificar se cada item está na variável $global-filter .

src/scss/tools/_filter.scss

 @mixin filter($class...) { @if(type-of($class) == 'arglist') { @each $item in $class { @if(index($global-filter, $item)) { @content; } } } @else if(index($global-filter, $class)) { @content; } }

Depois é só voltar aos parciais a seguir para aplicar corretamente o mixin filter() :

  • objects/_box.scss
  • objects/_layout.scss
  • utilities/_alignments.scss

Neste ponto, volte para $imports e ative apenas o componente expansor. Na folha de estilo compilada, além dos estilos das camadas genéricas e de elementos, você deve ver apenas o seguinte:

  • As classes de bloco e elemento pertencentes ao componente expansor, mas não seu modificador.
  • As classes de bloco e elemento pertencentes às dependências do expansor.
  • Quaisquer classes modificadoras pertencentes às dependências do expansor que são explicitamente declaradas na variável $dependencies .

Teoricamente, se você decidiu incluir mais classes na folha de estilo compilada, como o modificador de componentes do expansor, é apenas uma questão de adicioná-lo à variável $global-filter no ponto de declaração ou anexá-lo em algum outro ponto na base de código (desde que seja antes do ponto em que o próprio modificador é declarado).

Ativando tudo

Portanto, agora temos um sistema bastante completo, que permite importar objetos, componentes e utilitários para as classes individuais dentro dessas parciais.

Durante o desenvolvimento, por qualquer motivo, você pode querer habilitar tudo de uma vez. Para permitir isso, criaremos uma nova variável chamada $enable-all-classes e, em seguida, adicionaremos alguma lógica adicional para que, se isso for definido como true, tudo seja compilado, independentemente do estado das $imports e $global-filter variáveis ​​de $global-filter .

Primeiro, declare a variável em nosso arquivo de manifesto principal:

src/scss/main.scss

 $enable-all-classes: false; @import 'settings/core'; @import 'tools/core'; @import 'generic/core'; @import 'elements/core'; @import 'objects/core'; @import 'components/core'; @import 'utilities/core';

Em seguida, só precisamos fazer algumas pequenas edições em nossos mixins filter() e render() para adicionar alguma lógica de substituição para quando a variável $enable-all-classes estiver definida como true.

Primeiro, o mixin filter() . Antes de qualquer verificação existente, adicionaremos uma instrução @if para ver se $enable-all-classes está definido como true e, em caso afirmativo, renderizar o @content , sem perguntas.

src/scss/tools/_filter.scss

 @mixin filter($class...) { @if($enable-all-classes) { @content; } @else if(type-of($class) == 'arglist') { @each $item in $class { @if(index($global-filter, $item)) { @content; } } } @else if(index($global-filter, $class)) { @content; } }

Em seguida, no mixin render() , precisamos apenas fazer uma verificação para ver se a variável $enable-all-classes é verdadeira e, em caso afirmativo, pule quaisquer outras verificações.

src/scss/tools/_render.scss

 $layer: null !default; @mixin render($name) { @if($enable-all-classes or index(map-get($imports, $layer), $name)) { @content; } }

Então, agora, se você definir a variável $enable-all-classes como true e reconstruir, todas as classes opcionais serão compiladas, economizando bastante tempo no processo.

Comparações

Para ver que tipo de ganhos esta técnica está nos dando, vamos fazer algumas comparações e ver quais são as diferenças de tamanho de arquivo.

Para garantir que a comparação seja justa, devemos adicionar os objetos box e container em $imports e, em seguida, adicionar o modificador o-box--spacing-regular ao $global-filter , assim:

src/scss/settings/_imports.scss

 $imports: ( object: ( 'box', 'container' // 'layout' ), component: ( // 'button', 'expander' // 'typography' ), utility: ( // 'alignments', // 'widths' ) ); $global-filter: ( 'o-box--spacing-regular' );

Isso garante que os estilos dos elementos pai do expansor sejam compilados como seriam se não houvesse filtragem.

Folhas de estilo originais vs filtradas

Vamos comparar a folha de estilo original com todas as classes compiladas, com a folha de estilo filtrada onde apenas o CSS exigido pelo componente expansor foi compilado.

Padrão
Folha de estilo Tamanho (kb) Tamanho (gzip)
Original 54,6kb 6,98kb
Filtrado 15,34kb (72% menor) 4,91kb (29% menor)
  • Original: https://webdevluke.github.io/handlingunusedcss/dist/index2.html
  • Filtrado: https://webdevluke.github.io/handlingunusedcss/dist/index.html

Você pode pensar que a economia percentual do gzip significa que isso não vale o esforço, pois não há muita diferença entre as folhas de estilo originais e filtradas.

Vale ressaltar que a compactação gzip funciona melhor com arquivos maiores e mais repetitivos. Como a folha de estilo filtrada é a única prova de conceito e contém apenas CSS para o componente expansor, não há tanto para compactar quanto em um projeto da vida real.

Se fôssemos aumentar cada folha de estilo por um fator de 10 para tamanhos mais típicos do tamanho do pacote CSS de um site, a diferença nos tamanhos dos arquivos gzip seria muito mais impressionante.

10x Tamanho
Folha de estilo Tamanho (kb) Tamanho (gzip)
Originais (10x) 892,07kb 75,70kb
Filtrado (10x) 209,45kb (77% menor) 19,47kb (74% menor)

Folha de estilo filtrada vs UNCSS

Aqui está uma comparação entre a folha de estilo filtrada e uma folha de estilo que foi executada pela ferramenta UNCSS.

Filtrado vs UNCSS
Folha de estilo Tamanho (kb) Tamanho (gzip)
Filtrado 15,34kb 4,91kb
UNCSS 12,89kb (16% menor) 4,25kb (13% menor)

A ferramenta UNCSS ganha aqui marginalmente, pois está filtrando CSS nos diretórios genérico e de elementos.

É possível que em um site real, com uma variedade maior de elementos HTML em uso, a diferença entre os 2 métodos seja insignificante.

Empacotando

Então nós vimos como – usando apenas Sass – você pode ganhar mais controle sobre quais classes CSS estão sendo compiladas na compilação. Isso reduz a quantidade de CSS não utilizado na folha de estilo final e acelera o caminho crítico de renderização.

No início do artigo, listei algumas desvantagens de soluções existentes, como o UNCSS. É justo criticar essa solução orientada a Sass da mesma maneira, então todos os fatos estão na mesa antes de você decidir qual abordagem é melhor para você:

Prós

  • Não são necessárias dependências adicionais, portanto, você não precisa confiar no código de outra pessoa.
  • Menos tempo de compilação necessário do que as alternativas baseadas em Node.js, pois você não precisa executar navegadores headless para auditar seu código. Isso é especialmente útil com integração contínua, pois é menos provável que você veja uma fila de compilações.
  • Resulta em tamanho de arquivo semelhante quando comparado a ferramentas automatizadas.
  • Pronto para uso, você tem controle total sobre qual código está sendo filtrado, independentemente de como essas classes CSS são usadas em seu código. Com alternativas baseadas em Node.js, muitas vezes você precisa manter uma lista branca separada para que as classes CSS pertencentes ao HTML injetado dinamicamente não sejam filtradas.

Contras

  • A solução orientada a Sass é definitivamente mais prática, no sentido de que você precisa se manter atualizado sobre as variáveis $imports e $global-filter . Além da configuração inicial, as alternativas do Node.js que analisamos são amplamente automatizadas.
  • Se você adicionar classes CSS ao $global-filter e depois removê-las do seu HTML, você precisa se lembrar de atualizar a variável, caso contrário você estará compilando CSS desnecessário. Com grandes projetos sendo trabalhados por vários desenvolvedores ao mesmo tempo, isso pode não ser fácil de gerenciar, a menos que você planeje adequadamente.
  • Eu não recomendaria aparafusar este sistema em qualquer base de código CSS existente, pois você teria que gastar um pouco de tempo juntando dependências e aplicando o mixin render() a muitas classes. É um sistema muito mais fácil de implementar com novas compilações, onde você não tem código existente para lidar.

Espero que você tenha achado isso tão interessante de ler quanto eu achei interessante de montar. Se você tiver alguma sugestão, ideia para melhorar essa abordagem ou quiser apontar alguma falha fatal que eu perdi completamente, não deixe de postar nos comentários abaixo.