FluentCss documentation

Performance

FluentCss is designed so the common case is cheap. This page explains where the cost actually lives and the few habits that keep a large app fast.

Where the cost is

There are three distinct costs, and they behave very differently:

  1. 1. Building the class string. Every time a chain such as Css.Px(4).Py(2) is evaluated, the builder creates a UtilityClass per utility, builds each class name, collects them and joins the result. This runs on every render where the expression appears.
  2. 2. Adding to the registry. Each utility is looked up in ClassRegistry.DefaultRegistry. If it is already known, the call is a dictionary lookup and nothing else happens.
  3. 3. Generating the stylesheet. Only a new class appends a rule and invalidates the cached Css string. Once every class is known, the stylesheet is never rebuilt again.

The short version

The registry and the generated CSS settle down after warm-up. The only cost that keeps repeating on every render is building the class strings.

Precompute class strings

Define styles once in a static field or get-only property and use the resulting string in markup. The builder then runs once per process, and every render becomes a field read.

public static class Styles
{
    public static string Button { get; } =
        $"{Css.Px(6).Py(3)
             .Background(Color.Blue600)
             .Color(Color.White)
             .FontBold()
             .Hover(h => h.Background(Color.Blue700))}";
}

// Rendered as many times as you like, the builder only ran once.
<button class="@Styles.Button">Save</button>

This is the single most valuable habit. It keeps the per-utility object, string and list allocations, plus the registry lookup, out of the render path entirely. This documentation is built the same way: all of the class strings you see here are precomputed once.

Select between precomputed variants

When a style depends on state, build each variant once and choose between them. Do not rebuild the chain inside the selection.

private static readonly string NavLink =
    $"{Css.Px(3).Py(2).RoundedMd()
         .Color(Color.Slate600)
         .Hover(h => h.Background(Color.Slate100))}";

private static readonly string NavLinkActive =
    $"{Css.Px(3).Py(2).RoundedMd()
         .FontBold()
         .Background(Color.Blue100)
         .Color(Color.Blue700)}";

// Only the selection runs per render.
private string ClassFor(string href) =>
    IsActive(href) ? NavLinkActive : NavLink;

Keep builders out of loops

A chain written inside an @foreach or @for loop is evaluated for every iteration. When the set of variants is modest, build the strings once (for example into a static readonly array) and index into them.

private static readonly string[] Swatches =
    [.. Shades.Select(c => $"{Css.H(8).Grow()} {Css.Background(c)}")];

// ...
@for (var i = 0; i < Shades.Length; i++)
{
    <div class="@Swatches[i]"></div>
}

Keep the set of classes bounded

The registry is process wide and never evicts entries. Every distinct class is retained for the life of the process and, the first time it is seen, triggers a full rebuild of the stylesheet. Utilities that accept arbitrary integers (P(n), W(n), Z(n), Opacity(n), ...) can therefore grow memory and CPU without bound when the values come from user input or computed layout.

Keep values on a scale

Prefer the fixed Tailwind scale over arbitrary values. Treat any utility value derived from user input as a potential memory leak: the class, its CSS rule and its contribution to the stylesheet are never released.

What you do not need to worry about

  • • Calling the same utility many times. A repeated Add is a dictionary lookup. Do not cache or batch registrations by hand.
  • • The one-time startup burst. The first page that introduces many new classes pays for a stylesheet rebuild per new class, but only once per process and only for pages that have not been seen before.
  • • A handful of inline chains outside loops. A few builder calls per component are negligible next to Blazor's own rendering work.

Order of magnitude

For reference, on a desktop-class CPU registering 500 utilities takes on the order of a few tens of microseconds when the classes are new, and roughly half that when they are already known. Per utility that is well under a microsecond. The numbers only start to matter when a builder runs on every render of every row of a large list, which is exactly what precomputing avoids.

Checklist

  • ✓Precompute class strings as static members.
  • ✓Choose between precomputed variants instead of rebuilding.
  • ✓Move builder chains out of @foreach and @for loops.
  • ✓Keep utility values on a bounded scale.

Next, learn how to style with utility classes.