DEV Community

Cover image for We Tried to Break Our Own SCSS Compiler. Here Are the Numbers.
NuvoUI
NuvoUI

Posted on

We Tried to Break Our Own SCSS Compiler. Here Are the Numbers.

NuvoUI does something most SCSS frameworks avoid: its apply() mixin reads a list of utility items, with state and breakpoint modifiers, and turns them into CSS inside Dart Sass. No JavaScript, no PostCSS plugin, no scan of your HTML.

.card {
  @include apply(p(4), bg(surface), rounded(md), shadow(md) hover, p(6) lg);
}
Enter fullscreen mode Exit fullscreen mode

A fair question about that design: Sass is not a fast language, so what happens when a stylesheet calls apply() thousands of times? We tried to find out, starting in our own browser playground and then in Node so the numbers can be reproduced.

Setup

  • @nuvoui/core 2.0.1, loaded with @use "@nuvoui/core" as *
  • Dart Sass 1.103.1 in Node (the sass npm package, which is compiled to JavaScript)
  • Node 24.19.0 on an Apple M2 Pro, median of three runs
  • The playground runs the same package, version 1.105.1, in a Web Worker, so the page stays responsive while it compiles

Every compile also pays a fixed cost for loading the package and emitting its tokens. An empty stylesheet that only has the @use line takes 191 ms in Node. The per-rule figures below subtract it.

Test 1: 5,000 rules with the same six items

@for $i from 1 through 5000 {
  .stress-test-card-#{$i} {
    @include apply(p(4) md, m(2) lg, bg(red, 500) hover, text-color(white), rounded(md), flex);
  }
}
Enter fullscreen mode Exit fullscreen mode
Where Time Output
Playground (browser) 1,247 ms 90,051 lines
Node 2,049 ms 96,389 lines

This test looks like 30,000 string parses, but with as * nothing is parsed as text. Each item is a Sass function call that returns a small map, and apply() reads it directly. The values repeat, so after the first rule p(4) and bg(red, 500) resolve to their tokens from a cache. What this test really measures is output: 5,000 selectors, hover rules and two media queries per rule. About 60 microseconds per item, all in.

That is the common case. A real stylesheet repeats p(4) and bg(surface) far more often than it invents new values.

Test 2: every value unique

To take the cache out of the picture, every rule gets a padding and a margin no other rule uses:

@for $i from 1 through 2000 {
  .chaos-card-#{$i} {
    @include apply(p(#{$i}px) md, m(#{$i * 0.01}rem) lg, bg(red, 500) hover);
  }
}
Enter fullscreen mode Exit fullscreen mode
Rules Playground Node Node, per rule
2,000 1,834 ms 2,686 ms 1.25 ms
5,000 about 5,000 ms 6,420 ms 1.25 ms

Each rule holds two unique values, so a unique value costs about 0.6 ms, roughly 10 times a repeated item, because it is resolved and validated from scratch. The important number is the last column: the cost per rule is the same at 2,000 and at 5,000. Compile time grows in a straight line with the number of unique values.

That holds because one-off raw values are never stored, so there is no table that grows until every lookup slows down. The namespaced form (@use "@nuvoui/core" as nuvo with nuvo.apply()) is different: items arrive as text and are parsed, and parsed items go into size-capped caches. Our test suite guards that path: it compiles 4,000 namespaced rules of unique values and fails if the cost per call grows more than 1.4 times compared with a quarter of the rules. With the cap removed, that ratio measured between 1.7 and 3.5.

The playground figures were lower than Node's on the same machine. We report both and treat the Node figures as the reproducible ones.

What a typo costs

Speed is half of it. The other half is what happens when an item is wrong. A mistyped value in the playground:

gap: unknown value bg-red-500-hover-md-broken. Use a key (0, 1, 2, 3, 4, 5, 6, 8, 10, 12, 16, 20, 24, 32, 40, 48, 56, 64), a value with a CSS unit, calc(), var() or a keyword (normal, inherit, initial, unset, revert, revert-layer).
Enter fullscreen mode Exit fullscreen mode

NuvoUI fails the compile and names every valid option, instead of writing broken CSS that you find in the browser later.

What this means for a real project

  • A typical stylesheet has hundreds of unique raw values, not thousands. At about 0.6 ms each, that is well under a second.
  • The fixed cost is the package load, about 0.2 s per compiled entry. Compile one entry that imports your partials, not one stylesheet per component.
  • These are numbers for the JavaScript build of Dart Sass. We have not measured sass-embedded, which runs the native compiler.
  • Both tests are deliberately extreme. They show where the limits are, not what a normal build looks like.

Try it

Open the playground, paste either loop, and watch the timer. Or install it:

npm install @nuvoui/core sass
Enter fullscreen mode Exit fullscreen mode
@use "@nuvoui/core" as *;
Enter fullscreen mode Exit fullscreen mode

If you find a loop that breaks it, we want to see it.

Top comments (0)