Deferred stylesheets - take 3

A recent blink-dev intent and ensuing discussion got me thinking about the current ways we load styles.

This was heavily discussed in the past (#1, #2), but those discussions seemed focused on one case, where I think we can benefit from a more holistic discussion.

AFAIU, we have a few different cases for styles loaded today:

  1. Critical CSS - required for the page’s initial rendering. Should block rendering.
  2. Non-critical CSS - needed for the current page’s hidden parts. May or may not need to block the rendering of those hidden parts.
  3. Non-matching CSS - not required for current page unless conditions change. (e.g. print)

(1) is implemented through regular style links today, and currently (at least in Chromium), blocks the rendering, but not the HTML’s parsing. One side effect of that is that async scripts defined after the style (with no blocking scripts in the way) can still execute before the stylesheet finished downloading, enabling interesting loading patterns. The intent above wants to change that, in order to reduce code complexity, but at the expense of those patterns.

(2) can be implemented in multiple ways. One such way is to have the style links be embedded in the body, right above the content which they impact. Another is to use script-driven CSS loading patterns. Those 2 approaches have different characteristics. The former blocks the parser (at least in Chromium[1]), meaning that async scripts defined after it will not run before it finished loading. On the other hand, it also means that it will block rendering of everything below it. The latter approach doesn’t impact rendering at all, until the style finished loading, so can result in FOUC, but on the other hand, might be faster, as parsing is not paused.

Seems like it would be good to document:

  • The use-cases for running async scripts while styles are loading
    • If it’s just for timing out blocking styles, maybe we can come up with a better API
  • The use-cases for non-critical styles that shouldn’t block rendering at all
    • AFAICT, that’s the use case for deferred stylesheets beyond the current in-body behavior, but it’s not clear to me what it is…

[1] Browsers disagree on many things related to style loading. It would be good to try and coalesce behaviors here, but that may be a tangent.

Thanks for starting this discussion. My use of non-blocking styles include:

  • Content below-the-fold that cannot be seen by the user initially.
    • This should block if the user scrolls to the content before the stylesheet is loaded however.
  • Content that is not visible to the user until after an interaction. For example content that is inside of a closed summary/detail. Or content that is for another “page” that will be displayed via SPA routing.
  • Similar to above, content that requires JavaScript to have loaded, for example design system components. I might do something like opacity: 0; transition opacity; in the blocking main stylesheet and then set opacity: 1 in the non-blocking sheet.
  • Less common, but occasionally I have divided stylesheets into critical, such as page structure and sizing values (margin, opacity), and less critical such as colors, border radiuses and box shadows.

Those are helpful use-cases for “deferred styles” that don’t match the in-body style behavior. Thanks for that!

Trying to sum up responses from Twitter here:

  • Font loading for external font providers (like Google Fonts) requires loading CSS that then loads fonts.
    • Loading fonts late triggers layout shifts, which is bad (bad CLS scores). But blocking the entire rendering on styles that just fetch fonts is also bad (bad FCP scores).
    • The font-loading CSS’s desired blocking behavior depends on the defined font-display, but the browser doesn’t know that ahead of time.
  • Links in the body can be hard to maintain.
  • There are performance benefits to running scripts in parallel to style download regardless of the specific “timeout” functionality.
  • Blocking the parser is not great. FOUC is arguably worse…

Reflecting on all the above:

  • It seems like there are strong use-cases for deferred styles, which shouldn’t block rendering at all. Today’s patterns to do that work, but are clunky. Having a direct syntax for it would be helpful.
  • Font loading is broken.
  • Supporting deferred styles won’t help avoid the parser blocking behavior, as some styles need to avoid FOUC
  • What Chromium does today for in-head styles is to block rendering without parsing. That significantly complicates the rendering code.
    • Is there a third option?
  • We could provide opt-ins to stylesheets that indicate that their styles are not referred to by following scripts, and therefore their download shouldn’t block those scripts. That wouldn’t solve the complexity issues around continuing to parse without rendering. I’m also not sure that is something developers can easily predict (but maybe tools can help us, if it’s beneficial).