Manejo de CSS no utilizado en Sass para mejorar el rendimiento

Publicado: 2022-03-10
Resumen rápido ↬ ¿Conoces el impacto que tiene el CSS no utilizado en el rendimiento? Spoiler: ¡Es mucho! En este artículo, exploraremos una solución orientada a Sass para lidiar con CSS no utilizado, evitando la necesidad de dependencias complicadas de Node.js que involucran navegadores sin interfaz y emulación DOM.

En el desarrollo front-end moderno, los desarrolladores deben apuntar a escribir CSS que sea escalable y mantenible. De lo contrario, corren el riesgo de perder el control sobre detalles como la cascada y la especificidad del selector a medida que crece la base de código y contribuyen más desarrolladores.

Una forma de lograr esto es mediante el uso de metodologías como CSS orientado a objetos (OOCSS), que en lugar de organizar CSS en torno al contexto de la página, fomenta la separación de la estructura (sistemas de cuadrícula, espaciado, anchos, etc.) de la decoración (fuentes, marca, colores, etc.).

Entonces, nombres de clase CSS como:

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

Se reemplazan con alternativas más reutilizables, que aplican los mismos estilos CSS, pero no están vinculadas a ningún contexto en particular:

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

Este enfoque se implementa comúnmente con la ayuda de un marco Sass como Bootstrap, Foundation o, cada vez más a menudo, un marco a medida que se puede moldear para adaptarse mejor al proyecto.

¡Más después del salto! Continúe leyendo a continuación ↓

Así que ahora estamos usando clases de CSS cuidadosamente seleccionadas de un marco de patrones, componentes de interfaz de usuario y clases de utilidad. El siguiente ejemplo ilustra un sistema de cuadrícula común creado con Bootstrap, que se apila verticalmente y luego, una vez que se alcanza el punto de interrupción md, cambia a un diseño de 3 columnas.

 <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>

Las clases generadas mediante programación, como .col-12 y .col-md-4 , se utilizan aquí para crear este patrón. Pero, ¿qué pasa con .col-1 a .col-11 , .col-lg-4 , .col-md-6 o .col-sm-12 ? Todos estos son ejemplos de clases que se incluirán en la hoja de estilo CSS compilada, descargada y analizada por el navegador, a pesar de no estar en uso.

En este artículo, comenzaremos explorando el impacto que el CSS no utilizado puede tener en la velocidad de carga de la página. Luego abordaremos alguna solución existente para eliminarla de las hojas de estilo, siguiendo con mi propia solución orientada a Sass.

Medir el impacto de las clases CSS no utilizadas

Si bien adoro a Sheffield United, los poderosos blades, el CSS de su sitio web está incluido en un solo archivo minificado de 568 kb, que llega a 105 kb incluso cuando está comprimido con gzip. Eso parece mucho.

Este es el sitio web de Sheffield United, mi equipo de fútbol local (eso es fútbol para ti en las colonias). (Vista previa grande)

¿Vamos a ver cuánto de este CSS se usa realmente en su página de inicio? Una búsqueda rápida en Google revela muchas herramientas en línea adecuadas para el trabajo, pero prefiero usar la herramienta de cobertura en Chrome, que se puede ejecutar directamente desde DevTools de Chrome. Vamos a darle un giro.

La forma más rápida de acceder a la herramienta de cobertura en Herramientas para desarrolladores es usar el método abreviado de teclado Control+Mayús+P o Comando+Mayús+P (Mac) para abrir el menú de comandos. En él, escribe coverage , y selecciona la opción 'Mostrar Cobertura'. (Vista previa grande)

Los resultados muestran que la página de inicio utiliza solo 30 kb de CSS de la hoja de estilos de 568 kb, y los 538 kb restantes se relacionan con los estilos requeridos para el resto del sitio web. Esto significa que la friolera de 94,8% del CSS no se utiliza.

Puede ver tiempos como estos para cualquier recurso en Chrome en Herramientas para desarrolladores a través de Red -> Haga clic en su recurso -> pestaña Tiempo. (Vista previa grande)

CSS es parte de la ruta de representación crítica de una página web, lo que implica todos los diferentes pasos que un navegador debe completar antes de que pueda comenzar a representar la página. Esto convierte a CSS en un activo que bloquea el renderizado.

Entonces, con esto en mente, al cargar el sitio web de Sheffield United usando una buena conexión 3G, toma 1.15 segundos antes de que se descargue el CSS y pueda comenzar la representación de la página. Esto es un problema.

Google también lo ha reconocido. Al ejecutar una auditoría de Lighthouse, en línea o a través de su navegador, se resaltan los posibles ahorros de tiempo de carga y de tamaño de archivo que se podrían lograr al eliminar el CSS no utilizado.

En Chrome (y Chromium Edge), puede corregir las auditorías de Google Lighthouse haciendo clic en la pestaña Auditoría en las herramientas para desarrolladores. (Vista previa grande)

Soluciones existentes

El objetivo es determinar qué clases de CSS no son necesarias y eliminarlas de la hoja de estilos. Existen soluciones disponibles que intentan automatizar este proceso. Por lo general, se pueden usar mediante un script de compilación de Node.js o mediante ejecutores de tareas como Gulp. Éstos incluyen:

  • UNCSS
  • PurificarCSS
  • PurgarCSS

Estos generalmente funcionan de manera similar:

  1. En bulld, se accede al sitio web a través de un navegador sin cabeza (p. ej., titiritero) o emulación DOM (p. ej., jsdom).
  2. Según los elementos HTML de la página, se identifica cualquier CSS no utilizado.
  3. Esto se elimina de la hoja de estilo, dejando solo lo que se necesita.

Si bien estas herramientas automatizadas son perfectamente válidas y he usado muchas de ellas con éxito en una serie de proyectos comerciales, me he encontrado con algunos inconvenientes en el camino que vale la pena compartir:

  • Si los nombres de las clases contienen caracteres especiales como '@' o '/', es posible que no se reconozcan sin escribir un código personalizado. Yo uso BEM-IT de Harry Roberts, que involucra la estructuración de nombres de clase con sufijos sensibles como: u-width-6/12@lg , por lo que he abordado este problema antes.
  • Si el sitio web utiliza la implementación automática, puede ralentizar el proceso de creación, especialmente si tiene muchas páginas y mucho CSS.
  • El conocimiento sobre estas herramientas debe compartirse con todo el equipo; de lo contrario, puede haber confusión y frustración cuando CSS está misteriosamente ausente en las hojas de estilo de producción.
  • Si su sitio web tiene muchos scripts de terceros ejecutándose, a veces cuando se abren en un navegador sin interfaz gráfica, estos no funcionan bien y pueden causar errores con el proceso de filtrado. Por lo general, debe escribir un código personalizado para excluir cualquier secuencia de comandos de terceros cuando se detecta un navegador sin interfaz, lo que, según su configuración, puede ser complicado.
  • En general, este tipo de herramientas son complicadas e introducen muchas dependencias adicionales en el proceso de compilación. Como es el caso con todas las dependencias de terceros, esto significa confiar en el código de otra persona.

Con estos puntos en mente, me planteé una pregunta:

Usando solo Sass, ¿es posible manejar mejor el Sass que compilamos para que se pueda excluir cualquier CSS no utilizado, sin recurrir a eliminar crudamente las clases de origen en el Sass por completo?

Alerta de spoiler: la respuesta es sí. Esto es lo que se me ocurrió.

Solución orientada a Sass

La solución debe proporcionar una manera rápida y fácil de elegir qué Sass debe compilarse, al mismo tiempo que es lo suficientemente simple como para no agregar más complejidad al proceso de desarrollo ni evitar que los desarrolladores aprovechen cosas como CSS generado mediante programación. clases

Para comenzar, hay un repositorio con scripts de compilación y algunos estilos de muestra que puede clonar desde aquí.

Sugerencia: si se atasca, siempre puede hacer una referencia cruzada con la versión completa en la rama maestra.

cd en el repositorio, ejecute npm install y luego npm run build para compilar cualquier Sass en CSS según sea necesario. Esto debería crear un archivo css de 55 kb en el directorio dist.

Si luego abre /dist/index.html en su navegador web, debería ver un componente bastante estándar, que al hacer clic se expande para revelar algo de contenido. También puede ver esto aquí, donde se aplicarán las condiciones reales de la red, para que pueda ejecutar sus propias pruebas.

Usaremos este componente de interfaz de usuario de expansión como nuestro sujeto de prueba al desarrollar la solución orientada a Sass para manejar CSS no utilizado. (Vista previa grande)

Filtrado a nivel de parciales

En una configuración típica de SCSS, es probable que tenga un solo archivo de manifiesto (p. ej., main.scss en el repositorio) o uno por página (p. ej., index.scss , products.scss , contact.scss ) donde los marcos parciales son importados. Siguiendo los principios de OOCSS, esas importaciones pueden verse así:

Ejemplo 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';

Si alguno de estos parciales no está en uso, entonces la forma natural de filtrar este CSS no utilizado sería deshabilitar la importación, lo que evitaría que se compile.

Por ejemplo, si solo usa el componente de expansión, el manifiesto normalmente se vería como el siguiente:

Ejemplo 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';

Sin embargo, según OOCSS, estamos separando la decoración de la estructura para permitir la máxima reutilización, por lo que es posible que el expansor requiera CSS de otros objetos, componentes o clases de utilidades para renderizarse correctamente. A menos que el desarrollador conozca estas relaciones al inspeccionar el HTML, es posible que no sepa importar estos parciales, por lo que no se compilarán todas las clases requeridas.

En el repositorio, si observa el HTML del expansor en dist/index.html , este parece ser el caso. Utiliza estilos de los objetos de cuadro y diseño, el componente de tipografía y las utilidades de ancho y alineación.

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>

Abordemos este problema que está por suceder haciendo que estas relaciones sean oficiales dentro del propio Sass, de modo que una vez que se importa un componente, las dependencias también se importarán automáticamente. De esta manera, el desarrollador ya no tiene la sobrecarga adicional de tener que auditar el HTML para saber qué más necesita importar.

Mapa de importaciones programáticas

Para que este sistema de dependencia funcione, en lugar de simplemente comentar en declaraciones @import en el archivo de manifiesto, la lógica de Sass deberá dictar si los parciales se compilarán o no.

En src/scss/settings , cree un nuevo parcial llamado _imports.scss , @import en settings/_core.scss y luego cree el siguiente 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 tendrá la misma función que el manifiesto de importación en el ejemplo 1.

Ejemplo 4

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

Debería comportarse como lo haría un conjunto estándar de @imports , en el sentido de que si se comentan ciertos parciales (como el anterior), entonces ese código no debería compilarse en la compilación.

Pero como queremos importar dependencias automáticamente, también deberíamos poder ignorar este mapa en las circunstancias adecuadas.

Mezcla de renderizado

Comencemos a agregar algo de lógica Sass. Cree _render.scss en src/scss/tools y luego agregue su @import a tools/_core.scss .

En el archivo, crea un mixin vacío llamado render() .

src/scss/tools/_render.scss

 @mixin render() { }

En el mixin, necesitamos escribir Sass que hace lo siguiente:

  • hacer()
    “Hola, $imports , hace buen tiempo, ¿no? Dime, ¿tienes el objeto contenedor en tu mapa?"
  • $importaciones
    false
  • hacer()
    “Es una pena, parece que no se compilará entonces. ¿Qué tal el componente del botón?
  • $importaciones
    true
  • hacer()
    "¡Agradable! Ese es el botón que se está compilando entonces. Saluda a la esposa de mi parte”.

En Sass, esto se traduce en lo siguiente:

src/scss/tools/_render.scss

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

Básicamente, verifique si el parcial está incluido en la variable $imports y, de ser así, renderícelo usando la directiva @content de Sass, que nos permite pasar un bloque de contenido al mixin.

Lo usaríamos así:

Ejemplo 5

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

Antes de usar este mixin, hay una pequeña mejora que podemos hacerle. El nombre de la capa (objeto, componente, utilidad, etc.) es algo que podemos predecir con seguridad, por lo que tenemos la oportunidad de simplificar un poco las cosas.

Antes de la declaración de mezcla de procesamiento, cree una variable llamada $layer y elimine la variable con el mismo nombre de los parámetros de mezcla. Al igual que:

src/scss/tools/_render.scss

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

Ahora, en los parciales _core.scss donde se encuentran los objetos, componentes y utilidades @imports , vuelva a declarar estas variables a los siguientes valores; que representa el tipo de clases CSS que se importan.

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';

De esta manera, cuando usamos el mixin render() , todo lo que tenemos que hacer es declarar el nombre parcial.

Envuelva el mixin render() alrededor de cada objeto, componente y declaración de clase de utilidad, como se indica a continuación. Esto le dará un uso de mezcla de renderizado por parcial.

Por ejemplo:

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 la función render() alrededor de todo el parcial generará un error en la compilación, ya que en Sass no puede anidar declaraciones de mezcla dentro de llamadas de mezcla. En su lugar, simplemente ajuste la mezcla render() alrededor de las llamadas create-widths() , como se muestra a continuación:

 @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 }

Con esto en su lugar, en la compilación, solo se compilarán los parciales a los que se hace referencia en $imports .

Mezcle y combine los componentes que se comentan en $imports y ejecute npm run build en la terminal para probarlo.

Mapa de Dependencias

Ahora que estamos importando parciales mediante programación, podemos comenzar a implementar la lógica de dependencia.

En src/scss/settings , cree un nuevo parcial llamado _dependencies.scss , @import en settings/_core.scss , pero asegúrese de que esté después de _imports.scss . Luego, en él, cree el siguiente mapa SCSS:

src/scss/settings/_dependencies.scss

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

Aquí, declaramos dependencias para el componente de expansión, ya que requiere estilos de otros parciales para renderizarse correctamente, como se ve en dist/index.html.

Usando esta lista, podemos escribir una lógica que significaría que estas dependencias siempre se compilarían junto con sus componentes dependientes, sin importar el estado de la variable $imports .

Debajo $dependencies , crea un mixin llamado dependency-setup() . Aquí, haremos las siguientes acciones:

1. Recorra el mapa de dependencias.

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

2. Si el componente se puede encontrar en $imports , recorra su lista de dependencias.

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

3. Si la dependencia no está en $imports , agréguela.

 @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 el indicador !global le indica a Sass que busque la variable $imports en el ámbito global, en lugar del ámbito local de la mezcla.

4. Entonces solo es cuestión de llamar al mixin.

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

Entonces, lo que tenemos ahora es un sistema mejorado de importación parcial, donde si se importa un componente, un desarrollador no tiene que importar manualmente cada una de sus diversas dependencias parciales también.

Configure la variable $imports para que solo se importe el componente de expansión y luego ejecute npm run build . Debería ver en el CSS compilado las clases de expansión junto con todas sus dependencias.

Sin embargo, esto realmente no trae nada nuevo a la mesa en términos de filtrado de CSS no utilizado, ya que todavía se importa la misma cantidad de Sass, mediante programación o no. Mejoremos en esto.

Importación de dependencia mejorada

Un componente puede requerir solo una sola clase de una dependencia, por lo que luego continuar e importar todas las clases de esa dependencia solo conduce a la misma hinchazón innecesaria que estamos tratando de evitar.

Podemos refinar el sistema para permitir un filtrado más granular clase por clase, para asegurarnos de que los componentes se compilan solo con las clases de dependencia que requieren.

Con la mayoría de los patrones de diseño, decorados o no, existe una cantidad mínima de clases que deben estar presentes en la hoja de estilo para que el patrón se muestre correctamente.

Para los nombres de clase que utilizan una convención de nomenclatura establecida, como BEM, normalmente se requieren como mínimo las clases con nombre "Bloque" y "Elemento", y los "Modificadores" suelen ser opcionales.

Nota: Las clases de utilidad normalmente no seguirían la ruta BEM, ya que son de naturaleza aislada debido a su enfoque limitado.

Por ejemplo, eche un vistazo a este objeto multimedia, que es probablemente el ejemplo más conocido 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>

Si un componente tiene este conjunto como dependencia, tiene sentido compilar siempre .o-media , .o-media__image y .o-media__text , ya que esa es la cantidad mínima de CSS requerida para que el patrón funcione. Sin embargo, .o-media--spacing-small es un modificador opcional, solo debe compilarse si lo decimos explícitamente, ya que su uso puede no ser consistente en todas las instancias de objetos de medios.

Modificaremos la estructura del mapa de $dependencies para permitirnos importar estas clases opcionales, mientras incluimos una forma de importar solo el bloque y el elemento en caso de que no se requieran modificadores.

Para comenzar, verifique el HTML del expansor en dist/index.html y tome nota de las clases de dependencia en uso. Regístrelos en el mapa $dependencies , como se muestra a continuación:

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' ) ) ) );

Cuando un valor se establece en verdadero, lo traduciremos a "Solo compilar clases de nivel de bloque y elemento, ¡sin modificadores!".

El siguiente paso consiste en crear una variable de lista blanca para almacenar estas clases y cualquier otra clase (que no sea de dependencia) que deseemos importar manualmente. En /src/scss/settings/imports.scss , después $imports , cree una nueva lista Sass llamada $global-filter .

src/scss/settings/_imports.scss

 $global-filter: ();

La premisa básica detrás $global-filter es que cualquier clase almacenada aquí se compilará en la compilación siempre que el parcial al que pertenecen se importe a través $imports .

Estos nombres de clase podrían agregarse mediante programación si son una dependencia de componente, o podrían agregarse manualmente cuando se declara la variable, como en el siguiente ejemplo:

Ejemplo de filtro global

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

A continuación, debemos agregar un poco más de lógica a la @dependency-setup , de modo que cualquier clase a la que se haga referencia en $dependencies se agregue automáticamente a nuestra lista blanca $global-filter .

Debajo de este bloque:

src/scss/settings/_dependencies.scss

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

... agregue el siguiente fragmento.

src/scss/settings/_dependencies.scss

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

Esto recorre las clases de dependencia y las agrega a la lista blanca $global-filter .

En este punto, si agrega una instrucción @debug debajo de la combinación dependency-setup() para imprimir el contenido de $global-filter en la terminal:

 @debug $global-filter;

... deberías ver algo como esto en la compilación:

 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"

Ahora que tenemos una lista blanca de clases, debemos aplicar esto en todos los diferentes parciales de objetos, componentes y utilidades.

Cree un nuevo parcial llamado _filter.scss en src/scss/tools y agregue un @import al archivo _core.scss de la capa de herramientas.

En este nuevo parcial, crearemos un mixin llamado filter() . Usaremos esto para aplicar la lógica, lo que significa que las clases solo se compilarán si se incluyen en la variable $global-filter .

Comenzando de manera simple, cree un mixin que acepte un solo parámetro: la $class que controla el filtro. A continuación, si $class está incluida en la lista blanca $global-filter , permita que se compile.

src/scss/tools/_filter.scss

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

En un parcial, envolveríamos el mixin alrededor de una clase opcional, así:

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

Esto significa que la clase .o-myobject--modifier solo se compilará si se incluye en $global-filter , que se puede establecer directamente o indirectamente a través de lo que se establece en $dependencies .

Vaya a través del repositorio y aplique la mezcla filter() a todas las clases de modificadores opcionales en las capas de objetos y componentes. Cuando se maneja el componente tipográfico o la capa de utilidades, como cada clase es independiente de la siguiente, tendría sentido hacerlas todas opcionales, de modo que podamos habilitar las clases cuando las necesitemos.

Aquí hay algunos ejemplos:

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: Al agregar los nombres de clase del sufijo receptivo a la mezcla filter() , no tiene que escapar del símbolo '@' con un '\'.

Durante este proceso, mientras aplicas la mezcla filter() a los parciales, es posible que hayas notado (o no) algunas cosas.

Clases agrupadas

Algunas clases en el código base están agrupadas y comparten los mismos estilos, por ejemplo:

src/scss/objects/_box.scss

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

Como el filtro solo acepta una sola clase, no tiene en cuenta la posibilidad de que un bloque de declaración de estilo pueda ser para más de una clase.

Para dar cuenta de esto, expandiremos la mezcla filter() para que, además de una sola clase, pueda aceptar una lista de argumentos de Sass que contenga muchas clases. Al igual que:

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; } }

Por lo tanto, debemos decirle a filter() mixin que si alguna de estas clases está en $global-filter , puede compilar las clases.

Esto implicará una lógica adicional para verificar el tipo de argumento $class de mixin, respondiendo con un bucle si se pasa una lista de argumentos para verificar si cada elemento está en la variable $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; } }

Entonces solo es cuestión de volver a los siguientes parciales para aplicar correctamente la mezcla filter() :

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

En este punto, regrese a $imports y habilite solo el componente de expansión. En la hoja de estilo compilada, además de los estilos de las capas genérica y de elementos, solo deberías ver lo siguiente:

  • Las clases de bloque y elemento que pertenecen al componente de expansión, pero no su modificador.
  • Las clases de bloques y elementos que pertenecen a las dependencias del expansor.
  • Cualquier clase de modificador que pertenezca a las dependencias del expansor que se declara explícitamente en la variable $dependencies .

Teóricamente, si decidió que desea incluir más clases en la hoja de estilo compilada, como el modificador de componentes de expansión, solo es cuestión de agregarlo a la variable $global-filter en el punto de declaración, o agregarlo en algún otro punto. en el código base (siempre que sea antes del punto donde se declara el modificador).

Habilitar todo

Así que ahora tenemos un sistema bastante completo, que le permite importar objetos, componentes y utilidades hasta las clases individuales dentro de estos parciales.

Durante el desarrollo, por cualquier motivo, es posible que desee habilitar todo de una sola vez. Para permitir esto, crearemos una nueva variable llamada $enable-all-classes , y luego agregaremos algo de lógica adicional, de modo que si se establece en verdadero, todo se compila sin importar el estado de las $imports y $global-filter variables.

Primero, declare la variable en nuestro archivo de manifiesto 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';

Luego, solo tenemos que hacer algunas ediciones menores en nuestros mixins filter() y render() para agregar algo de lógica de anulación para cuando la variable $enable-all-classes se establece en verdadero.

En primer lugar, la mezcla filter() . Antes de cualquier verificación existente, agregaremos una declaración @if para ver si $enable-all-classes está configurado como verdadero y, de ser así, renderice el @content , sin hacer preguntas.

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; } }

A continuación, en el mixin render() , solo tenemos que hacer una verificación para ver si la variable $enable-all-classes es verdadera y, de ser así, omitir cualquier verificación adicional.

src/scss/tools/_render.scss

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

Entonces, si tuviera que establecer la variable $enable-all-classes en verdadero y reconstruir, se compilarían todas las clases opcionales, ahorrándole bastante tiempo en el proceso.

comparaciones

Para ver qué tipo de ganancias nos brinda esta técnica, realicemos algunas comparaciones y veamos cuáles son las diferencias de tamaño de archivo.

Para asegurarnos de que la comparación sea justa, debemos agregar los objetos box y container en $imports , y luego agregar el modificador o-box--spacing-regular al $global-filter , así:

src/scss/settings/_imports.scss

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

Esto asegura que los estilos para los elementos principales del expansor se compilen como lo harían si no se estuviera filtrando.

Hojas de estilo originales vs filtradas

Comparemos la hoja de estilo original con todas las clases compiladas, con la hoja de estilo filtrada donde solo se ha compilado el CSS requerido por el componente de expansión.

Estándar
hoja de estilo Tamaño (kb) Tamaño (gzip)
Original 54.6kb 6.98kb
Filtrado 15.34kb (72% más pequeño) 4.91kb (29% más pequeño)
  • Original: https://webdevluke.github.io/handlingunusedcss/dist/index2.html
  • Filtrado: https://webdevluke.github.io/handlingunusedcss/dist/index.html

Puede pensar que el porcentaje de ahorro de gzip significa que no vale la pena el esfuerzo, ya que no hay mucha diferencia entre las hojas de estilo originales y las filtradas.

Vale la pena resaltar que la compresión gzip funciona mejor con archivos más grandes y repetitivos. Debido a que la hoja de estilo filtrada es la única prueba de concepto y solo contiene CSS para el componente de expansión, no hay tanto para comprimir como lo habría en un proyecto de la vida real.

Si tuviéramos que escalar cada hoja de estilo en un factor de 10 a tamaños más típicos del tamaño del paquete CSS de un sitio web, la diferencia en el tamaño de los archivos gzip sería mucho más impresionante.

10x tamaño
hoja de estilo Tamaño (kb) Tamaño (gzip)
originales (10x) 892.07kb 75.70kb
Filtrado (10x) 209.45kb (77% más pequeño) 19.47kb (74% más pequeño)

Hoja de estilo filtrada vs UNCSS

Aquí hay una comparación entre la hoja de estilo filtrada y una hoja de estilo que se ejecutó a través de la herramienta UNCSS.

Filtrado vs UNCSS
hoja de estilo Tamaño (kb) Tamaño (gzip)
Filtrado 15.34kb 4.91kb
UNCSS 12.89kb (16% más pequeño) 4.25kb (13% más pequeño)

La herramienta UNCSS gana aquí marginalmente, ya que filtra CSS en los directorios genérico y de elementos.

Es posible que en un sitio web real, con una mayor variedad de elementos HTML en uso, la diferencia entre los 2 métodos sea insignificante.

Terminando

Entonces, hemos visto cómo, usando solo Sass, puede obtener más control sobre qué clases de CSS se compilan en la compilación. Esto reduce la cantidad de CSS no utilizado en la hoja de estilo final y acelera la ruta de representación crítica.

Al comienzo del artículo, enumeré algunos inconvenientes de las soluciones existentes, como UNCSS. Es justo criticar esta solución orientada a Sass de la misma manera, por lo que todos los hechos están sobre la mesa antes de decidir qué enfoque es mejor para usted:

ventajas

  • No se requieren dependencias adicionales, por lo que no tiene que depender del código de otra persona.
  • Se requiere menos tiempo de compilación que las alternativas basadas en Node.js, ya que no tiene que ejecutar navegadores sin interfaz para auditar su código. Esto es especialmente útil con la integración continua, ya que es menos probable que vea una cola de compilaciones.
  • Da como resultado un tamaño de archivo similar en comparación con las herramientas automatizadas.
  • Fuera de la caja, tiene control total sobre qué código se filtra, independientemente de cómo se usen esas clases de CSS en su código. Con las alternativas basadas en Node.js, a menudo tiene que mantener una lista blanca separada para que las clases CSS que pertenecen al HTML inyectado dinámicamente no se filtren.

Contras

  • La solución orientada a Sass es definitivamente más práctica, en el sentido de que debe mantenerse al tanto de las variables $imports y $global-filter . Más allá de la configuración inicial, las alternativas de Node.js que hemos analizado están automatizadas en gran medida.
  • Si agrega clases de CSS a $global-filter y luego las elimina de su HTML, debe recordar actualizar la variable; de ​​lo contrario, estará compilando CSS que no necesita. Con proyectos grandes en los que trabajan varios desarrolladores al mismo tiempo, esto puede no ser fácil de administrar a menos que lo planifique adecuadamente.
  • No recomendaría atornillar este sistema en ningún código base CSS existente, ya que tendría que dedicar bastante tiempo a unir las dependencias y aplicar la render() a MUCHAS clases. Es un sistema mucho más fácil de implementar con nuevas compilaciones, donde no tiene código existente con el que lidiar.

Espero que hayas encontrado esto tan interesante de leer como yo lo encontré interesante de armar. Si tiene alguna sugerencia, idea para mejorar este enfoque o desea señalar algún defecto fatal que me he perdido por completo, asegúrese de publicarlo en los comentarios a continuación.