Codeskill

Learn to code, step by step

Performance of compiled CSS and mixin discipline

What happens after Sass compiles: CSS file size, duplicate rules, and mixin habits that quietly inflate your bundle. SCSS makes repetition easy. That is both the point and the risk.

Mixins duplicate; functions do not

A mixin inlines its body at every @include site. A function returns a value – the caller emits one property. Responsive mixins and utility generators are the usual culprits when compiled CSS grows faster than your component count.

// Expensive: inlines full block 200 times
@mixin flex-centre {
  display: flex;
  align-items: center;
  justify-content: center;
}

.card { @include flex-centre; }
.modal { @include flex-centre; }
// ... 198 more components

// Cheaper: one class, many elements
.flex-centre {
  display: flex;
  align-items: center;
  justify-content: center;
}

Use mixins for patterns that need parameterisation and appear in modest numbers. Use a single class or extend sparingly when the output is identical everywhere. We covered specificity risks of @extend in the intermediate tutorials – do not trade one problem for another.

Loop discipline

Generating utility classes from a spacing map is fine. Generating every margin permutation for twelve breakpoints is not, unless you have measured the need.

$space: ('0': 0, '1': 0.25rem, '2': 0.5rem, '3': 1rem);

@each $name, $value in $space {
  .m-#{$name} { margin: $value; }
  .p-#{$name} { padding: $value; }
}
// 8 classes - reasonable

// Avoid unless you truly need it:
@each $bp, $min in $breakpoints {
  @media (min-width: $min) {
    @each $name, $value in $space {
      .m-#{$bp}-#{$name} { margin: $value; }
    }
  }
}
// breakpoints × space keys × properties - adds up fast

Before adding a loop, estimate output: breakpoints × map keys × properties. If the number makes you wince, generate utilities on demand or limit the scale.

Measuring compiled output

Check size after compile, not from line count in SCSS. Gzip compresses repeated selectors well, but 400 KB of CSS still costs parse time in the browser.

  • ls -lh dist/main.css – raw size
  • gzip -c dist/main.css | wc -c – gzipped size (closer to transfer size)
  • Chrome DevTools Coverage tab – which rules never apply on a given page

Split entry points when products only need part of the system. A landing page should not ship admin table styles because one main.scss imports everything.

Purge and scope strategies

Utility-heavy SCSS pairs well with a purge step (Tailwind does this by default; you can do similar with PurgeCSS on your own class names). List safelist patterns for dynamically injected classes so purge does not strip them.

/* Compiled output you intend to keep */
.btn { /* ... */ }
.card { /* ... */ }

/* Generated utilities - candidate for purge if unused on page */
.m-0 { margin: 0; }
.m-1 { margin: 0.25rem; }
/* ... Hundreds more */

Component SCSS scoped to BEM-style classes rarely needs purging – the class names are tied to markup you control. Utility generators are where purge earns its keep.

Mixin audit checklist

  • Does this mixin emit more than three declarations? Could it be a class?
  • Is it included in more than ten places with identical arguments?
  • Does a loop generate classes nobody uses yet?
  • Are vendor prefixes still in mixins for properties browsers handle natively?

Run the audit when compiled CSS crosses a threshold you set (say 50 KB gzipped for a marketing page). Fix the worst offender, recompile, measure again.

Try it

  • Compile your project and record raw and gzipped CSS size.
  • Find one mixin included more than five times with identical output; replace with a shared class and recompile.
  • Count classes generated by one @each loop and decide if the scale is justified.
PreviousDesign-system scale (multi-brand, multi-product)