DEV Community

Antonio Towami
Antonio Towami

Posted on

Targeting 100/100 PageSpeed: Our Critical CSS Inlining Strategy

Our stretch target is a 100 PageSpeed score on merchant storefronts. A score is not a guarantee—it varies by page, data, device, network, and Lighthouse version—but the target forces us to keep the render path small.

The Problem With One Big CSS File

A render-blocking styles.css can delay the first paint while the browser downloads, parses, and applies the rules needed for the page. For a marketplace with catalog pages, account dashboards, rich text editors, and storefronts, one shared file can grow quickly.

Our Three-Tier CSS Strategy

We split CSS into three categories based on when it's needed:

1. Inlined Route CSS

The shared layout inlines its base styles, and public routes inline a small route-specific stylesheet in the server-rendered response:

@templ.Raw("<style>" + assets.GetMainCSS("catalog_list_mobile.css") + "</style>")
@templ.Raw(helper.DesktopStyle(
    assets.GetMainCSS("catalog_list_desktop.css"),
))
Enter fullscreen mode Exit fullscreen mode

This removes an external request from the initial rendering path. These files are route styles, not automatically generated "above-the-fold" subsets, so we still inspect their size and contents.

2. File-Linked CSS (account pages)

Account pages change frequently and benefit less from duplicating CSS in every HTML response, so they use cacheable stylesheets:

<link rel="stylesheet" href="/storage/static/css/site/main_account_mobile.css">
Enter fullscreen mode Exit fullscreen mode

3. Lazy-Loaded Bundles (everything else)

Non-critical shared CSS — such as the footer, lightbox, and secondary catalog UI — loads asynchronously. Editor styles currently live in the linked account stylesheet:

<link rel="stylesheet" href="/storage/static/css/site/bundle_mobile.css"
      media="print" data-media="all"
      onload="this.media=this.dataset.media;this.onload=null">
<noscript>
  <link rel="stylesheet" href="/storage/static/css/site/bundle_mobile.css">
</noscript>
Enter fullscreen mode Exit fullscreen mode

The media="print" pattern keeps the stylesheet from blocking initial screen rendering; once loaded, the handler changes its target media to all. The noscript fallback matters, and content must remain usable before the bundle arrives. This technique is suitable only for genuinely non-critical CSS.

Mobile-First File Structure

scss/
├── mobile/       ← base styles and small-screen rules
├── desktop/      ← desktop overrides
└── common/       ← shared variables, mixins
Enter fullscreen mode Exit fullscreen mode

Mobile CSS is the default. A helper wraps compiled desktop CSS in the project's desktop media query before inlining it. The deferred desktop bundle also has a media target. This prevents desktop rules from applying on small screens, but it does not guarantee that mobile browsers never download those bytes.

Each page section compiles to separate files:

compiled-css/
├── main_catalog_list_mobile.css     ← inlined on catalog
├── main_catalog_list_desktop.css    ← inlined on catalog (desktop media)
├── main_account_mobile.css     ← linked on /account
├── main_account_desktop.css
├── bundle_mobile.css           ← lazy loaded
└── bundle_desktop.css
Enter fullscreen mode Exit fullscreen mode

The Templ Integration

Our Go templates read the compiled CSS files while rendering. In development, that happens on every render for live reload; in production, the first successful read is cached in memory. Simplified:

func GetMainCSS(filename string) string {
    if production {
        return loadOnce(filename)
    }
    return readCompiledCSS(filename)
}
Enter fullscreen mode Exit fullscreen mode

In the Templ layout:

@templ.Raw("<style>" + assets.GetMainCSS("catalog_list_mobile.css") + "</style>")
Enter fullscreen mode Exit fullscreen mode

templ.Raw is safe here only because the input is trusted build output. Never pass merchant or user-controlled CSS through this path without a deliberate sanitization and isolation model.

There is no client-side CSS-in-JS. The essential CSS arrives with the HTML, while shared non-critical styles can still be cached as external files.

The Results

This design improves the mechanics that affect first render, but we do not treat an unrecorded Lighthouse run as evidence. We compare before/after builds with the same page data and throttling, track stylesheet and HTML byte sizes, and use field Core Web Vitals when enough traffic is available.

Inlining also has costs: repeated HTML bytes cannot use the browser's CSS cache, a large inline block can delay HTML transfer, and many entry points complicate builds. We monitor inline stylesheet and HTML sizes, move non-critical rules to external files, and re-measure as pages evolve. Defining an enforceable size budget is a useful next step.


This article is based on lessons from building Towami, a multi-country marketplace and white-label storefront platform. Follow for more practical notes on Go, HTMX, infrastructure, and SaaS.

Top comments (0)