Maple
⌘K
DOCUMENTATIONv2.0.17

Runtime vs. Static CSS Delivery

At first glance, this sounds like a comparison runtime should lose. A browser can load a static stylesheet through its native CSS pipeline, while Maple has to run JavaScript before it can generate rules. If the stylesheet is tiny, stable, and scoped exactly to the current page, that instinct is is spot on.

The interesting question begins just outside that ideal case. Real interfaces often ship shared CSS, unused rules, dynamic classes, conflict handling, route boundaries, and build tooling. Those costs change the comparison from "JavaScript versus CSS" into a delivery question: when is it cheaper to ship Maple's small runtime and generate only the rules that appear in the DOM, and when is it cheaper to ship a pre-generated stylesheet?

How the Benchmark Is Set Up

The benchmark compares two delivery models for the same utility-class workload. In the runtime model, the page ships maple.js. Maple reads the class attributes it finds in the browser, resolves those utilities, and inserts the generated CSS rules into the CSSOM. In the static model, the page ships a normal stylesheet through <link rel="stylesheet">.

The static stylesheet is not produced by a different utility framework or a different compiler. The runner first lets Maple generate the CSS, then serializes Maple's own rules, minifies them, and loads that CSS in the static variant. That matters because the comparison is not about whether one syntax can generate better CSS than another. The DOM, classes, and generated rule semantics stay aligned. Only the delivery model changes.

Each run reports one timing metric: Styled Ready. After navigation, the fixture waits for the load event, drains two microtasks, forces a style and layout read, waits two animation frames, and records performance.now(). In plain language, it asks: how long did it take the browser to reach a settled styled frame?

The saved runs all use the same seed, 58761, five measured iterations, three network profiles, and three CPU profiles. The network profiles move from no throttling to a slow 500 Kbps download with 400ms latency. The CPU profiles move from no throttling to 6x slowdown. That gives each workload a nine-cell matrix instead of a single number.

A cell is called a tie when the runtime/static median difference is below max(25ms, average IQR). That rule is deliberately conservative. If two variants are only a handful of milliseconds apart, the report does not pretend that browser scheduling noise has revealed a grand truth.

The fixture sizes are small, medium, and large. They contain 2,500, 5,000, and 9,000 total class occurrences. The reuse ratio controls how many of those class names are unique. A reuse ratio of 10 means the same utilities repeat often. A reuse ratio of 2.5 means many more unique utilities appear, which grows the static stylesheet and also gives Maple more distinct runtime work to do.

You can run these benchmarks yourself. The complete setup is available in the examples/benchmarks directory of the Maple repository.

Understanding the Results

The first thing to keep in mind is that the default static runs are generous to static CSS. They contain exactly the CSS needed by the current page and no unused rules. That is a good lower bound because it shows the best case Maple has to beat. It is also cleaner than many production bundles, where CSS may include shared app styles, component-library rules, global resets, route-level styles for screens the user has not opened, or conservatively extracted utilities.

Maple's runtime was 13.19 KB gzipped in these runs. That number is not directly comparable to CSS bytes in a simplistic way. JavaScript has to be parsed and executed. CSS is render-blocking and has to be transferred, parsed, matched, and applied. Either asset can be cached. Either path can win. The useful comparison is the whole delivery path, not the file extension.

1. When static CSS is tiny and exact

The high-reuse run uses --reuse-ratio 10 without unused CSS. The same utility classes repeat often, so the generated stylesheet stays small: 7.08 KB for the small fixture, 11.69 KB for medium, and 18.88 KB for large. This is the scenario static CSS wants: a small file, no waste, and no runtime interpretation.

Static wins 23 of 27 cells. The fixture averages favor static by 92ms on small, 86ms on medium, and 125ms on large, for an overall lead of 101ms. The largest single lead is 321ms on the large fixture with a fast network and a slow CPU. The result is clear, and so is its scale: the cleanest static win is measured in tens to low hundreds of milliseconds.

This highlights an important nuance about file sizes. Even though the large static stylesheet is already bigger than Maple's runtime in gzipped bytes, static still wins most cells. Transfer size matters, but it is not the whole story; CPU cost, CSS parsing, JavaScript execution, and browser scheduling all shape the final timing.

Benchmark report for reuse ratio 10 without unused CSS, where static delivery wins by 101ms overall.
High reuse keeps the static CSS small. The matrix is mostly blue, but the overall gap is 101ms.

2. Static still leads, but the margin shrinks

The next run lowers reuse to 5, which raises the unique utility count. Static CSS grows to 11.85 KB, 21.01 KB, and 34.55 KB. Static still wins overall, but the lead falls from 101ms to 43ms.

This is where we see that performance is a complex system question, rather than a simple metric. Small still favors static by 63ms. Medium favors static by 47ms. Large favors static by only 19ms on average, even though static wins more matrix cells. The shape matters: fast-network cells continue to reward static CSS, while slower-network and larger-payload cells start giving Maple more room to catch up.

Benchmark report for reuse ratio 5 without unused CSS, where static delivery wins by 43ms overall.
Lower reuse grows the stylesheet. Static still wins overall, but the lead narrows to 43ms.

3. With more unique utilities, the ideal run becomes even

At --reuse-ratio 2.5, the fixtures contain many more unique classes. Static CSS grows to 21.09 KB, 37.72 KB, and 63.44 KB. The overall average says runtime wins by 2ms, which is best read as effectively even.

The details are more useful than the headline. Static wins 14 cells while runtime wins 11, yet the average is almost identical because Maple's wins show up where larger CSS payloads are more expensive. This is why a single aggregate number can be misleading. The benchmark is telling us that network profile, device profile, and stylesheet size are interacting, not that one delivery model has become universally better.

Benchmark report for reuse ratio 2.5 without unused CSS, where runtime and static delivery are effectively even.
With more unique utilities, the ideal static run becomes effectively even: runtime leads by 2ms overall.

Up to this point, we've tested against a perfectly optimized static baseline. But while that baseline is easy to achieve in a lab, it rarely survives in production. Most real-world applications accumulate shared styles or unused CSS that the current page doesn't actively need. The next three runs add that realistic 'ballast' to the static side to see how the performance shifts.

4. Add unused CSS, and medium plus large flip

The --add-unused-css mode appends synthetic unused rules to the static stylesheet. It targets roughly 20 KB for small, 55 KB for medium, and 110 KB for large. This does not model every production app, but it models a common failure mode: CSS that is downloaded and parsed even though the current page cannot use it.

With --reuse-ratio 2.5 --add-unused-css, the small fixture still favors static by 18ms. That is a good reminder that Maple is not automatically faster just because unused CSS exists somewhere. But medium flips to runtime by 160ms, large flips to runtime by 397ms, and the overall run favors runtime by 180ms.

Benchmark report for reuse ratio 2.5 with unused CSS, where runtime delivery wins by 180ms overall.
Once unused CSS is added, medium and large fixtures turn green and the overall run moves to runtime by 180ms.

5. With moderate reuse, unused CSS dominates the result

The next ballast run uses --reuse-ratio 5. The static payloads are 24.82 KB, 56.99 KB, and 110.77 KB. Runtime wins the overall average by 246ms, with 16 runtime cell wins against 8 static cell wins.

The large fixture provides the clearest signal: runtime leads by 506ms. When pages are burdened with a large stylesheet full of unused rules, the cost of transferring and parsing that dead weight far exceeds the cost of running Maple to generate exactly what the DOM needs.

Benchmark report for reuse ratio 5 with unused CSS, where runtime delivery wins by 246ms overall.
With moderate reuse and unused CSS, the large fixture becomes the clearest runtime win.

6. The strongest Maple run keeps high reuse but adds ballast

The final run is the most useful comparison against the first one. It goes back to --reuse-ratio 10, the high-reuse case where ideal static CSS beat Maple by 101ms. The only big change is that static now carries unused CSS: 19.89 KB, 58.99 KB, and 112.88 KB.

That change flips the result. Small is basically even, with runtime ahead by 3ms. Medium favors runtime by 249ms. Large favors runtime by 539ms. Overall, runtime wins by 264ms. This is the cleanest illustration of Maple's performance argument: the runtime does have startup cost, but it does not pay for CSS rules that never appear in the DOM.

If we look at specific network conditions, the penalty of unused CSS becomes even starker. On a fast device with a slow network in the large fixture, Maple leads by a staggering 1.53 seconds. When bytes are expensive to transfer, skipping the download of unused rules completely outweighs the cost of generating CSS on the client.

Benchmark report for reuse ratio 10 with unused CSS, where runtime delivery wins by 264ms overall.
The same high-reuse shape that favored static flips once the static side carries unused CSS.

7. Caching favors static delivery again

The final scenario revisits the high-reuse case with unused CSS (--reuse-ratio 10 --add-unused-css), but this time with caching enabled. The static payloads remain identical, but they are served directly from the browser cache, completely removing the network transfer penalty for the unused rules.

This change shifts the advantage back to static CSS. Small favors static by 45ms, medium by 73ms, and large by 131ms. Overall, static delivery reclaims the lead by 83ms. When the browser does not have to download the unused CSS, the cost of parsing it is often lower than the cost of Maple executing its runtime and injecting rules dynamically.

This caching caveat hits differently depending on your architecture. In a Multi-Page Application (MPA) where every click is a full reload, a heavily cached static stylesheet (even with unused rules) is faster on subsequent visits, whereas a runtime would have to re-execute on every page. However, in a Single-Page Application (SPA), the environment stays alive. Maple's incremental, lazy-evaluation model perfectly matches the SPA lifecycle—it gives a fast initial load by generating only what is needed, and subsequent client-side navigations remain cheap because it only does incremental work for new classes.

Benchmark report for reuse ratio 10 with unused CSS and caching enabled, where static delivery wins by 83ms overall.
With caching enabled, the network penalty for unused CSS disappears, and static delivery takes an 83ms lead overall.

What actually causes the flip

Maple's work scales mainly with the unique utilities it encounters in the live DOM. Reusing the same class a thousand times does not require a thousand new CSS rules. Static CSS also scales with unique generated rules, but it pays that cost through a stylesheet that participates in the render-blocking CSS path. When that stylesheet also contains unused rules, the browser pays for styling information the current page cannot benefit from.

Network and device profiles decide how visible those costs are. Slow networks make transferred bytes more expensive. Slower devices make JavaScript execution, CSS parsing, style recalculation, and layout more expensive. Class reuse controls how much CSS the static path needs. Unused CSS controls how much of that work is wasted. Those variables are connected, so a single "runtime versus static" number is less useful than understanding the shape of your app.

About Tailwind comparisons

When comparing Maple to build-time tools like Tailwind, it helps to look at the entire architecture. Tailwind shifts work into build-time extraction and static asset delivery. That can be excellent when your build pipeline reliably emits tiny per-route CSS. But if the app also needs safelists for dynamic class names, shared CSS that spans many routes, or a runtime conflict helper such as tailwind-merge, those pieces belong in the accounting too.

The broader point is architectural. Maple's 13.19 KB runtime isn't just generating CSS; it's interpreting utilities, resolving class-order conflicts, supporting dynamic class values, and inserting CSS incrementally. When evaluating your stack, it's most helpful to weigh Maple's runtime against the combined footprint of both the CSS and JavaScript assets required by other approaches.

What to Take Away

The conclusion is: Maple does not make CSS itself faster, and it does not erase the cost of JavaScript. It changes where styling cost lives. Instead of shipping a stylesheet that must be complete before the page can finish styling, Maple ships a small runtime and creates rules only when class names actually appear.

If your first screen has a tiny, exact, stable stylesheet and your build system already produces it with little effort, static CSS may be faster. The high-reuse ideal run shows that clearly. If your app carries shared CSS, unused route styles, dynamic classes, or runtime conflict-management code, Maple can become faster on a cold start because it avoids the heavy network transfer the static path requires up front. The ballast runs show that just as clearly.

At the end of the day, choosing Maple is about looking at the whole picture rather than chasing a single benchmark. We always recommend testing on your actual users' devices and network conditions. When making a decision, it helps to consider the entire styling lifecycle: from cache hits and startup times to build complexity and your team's daily maintenance costs.

In many modern applications, the raw speed difference between runtime and static CSS often comes down to a few imperceptible milliseconds. When performance is essentially a tie, the real winner is the architecture that makes your life easier. For many teams, Maple's zero-build setup, natural dynamic classes, and automatic conflict resolution provide a developer experience that goes far beyond what a lab test can measure.

  • Choose static CSS when JavaScript must be optional, startup JavaScript is already over budget, or your CSS is consistently tiny and precisely scoped.
  • Choose Maple when you value no build step, no extraction rules, no unused utility CSS, natural dynamic classes, automatic conflict resolution, and portability across stacks.
  • Treat small benchmark differences as signals, not verdicts. A 20ms lab result can disappear under different caching, routing, browser, device, or network conditions.
ESC

Start typing to search across the documentation.