html icon indicating copy to clipboard operation
html copied to clipboard

Async css

Open andershol opened this issue 7 years ago • 4 comments

Allow css files to be marked as async meaning that they will not block rendering. The syntax could be a new attribute on the link element or a new value for "rel" attribute (that already have link types such as "dns-prefetch", "preconnect", "prefetch", and "preload" that also seems to serve a technical, how-to-load purpose), say "<link rel='stylesheet async' type='text/css' href='theme.css'>".

This functionality is similar to the async attribute on the script element, and the font-display descriptor for the @font-face CSS at-rule.

A web search will find quite a few articles about how to do this in a more or less hackish way, so there seems to be a demand for it. It seems that the browsers could rather easily add this in a much more reliable way as the browser e.g. knows if the resource is already in the cache and can control the priority of requests.

(I did read the contributing guidelines, but as I read them, they only talk about submitting pull requests and not about submitting issues)

andershol avatar Aug 31 '18 16:08 andershol

While the JS-based packaging does not have an issue with loading CSS on demand of JS module( lazy or in run time), IMO having coherent phased load behavior over all resources including script, images, fonts, css, etc has sense to be unified and be a part of HTML standard. So I would extend this feature request to "unify the load behavior for web page resources" instead of just CSS lazy load.

Also current separation of synchronous/async/delayed behavior does not reflect the need for modern web app. In complex apps the order of load is phased and each phase accounts dependency graph. Extending loading parameters with "depend on" and "load order" is a good addition to proposal.

All seems to be easy polyfilled for backward compatibility.

sashafirsov avatar Aug 31 '18 16:08 sashafirsov

There are two related but orthogonal issues here:

  1. What should the browser do before a resource is downloaded? For fonts this might be wait for the font to download before showing anything, show invisible text, or show a fallback font some of which can be selected by font-display (note it does not seem to be possible using font-display to opt-out of the initial wait), for scripts it might be to wait for the script to load before proceeding or just to execute it when it is loaded, for images there is "lowsrc" and one might imagine an option to select how the image should animate on to the image when it is loaded. In short, it depends on the resource type what possibilities it makes sense to choose between.
  2. What priority should the resource have in the load order? This could tie in with the priority and dependency system that both http2 (section 5.3) and quic (section 3.2) seems to already have. It would probably make sense to give page authors a way to indicate a desired load-order instead of relying on the browsers heuristic.

As an example of why they are orthogonal, consider that it currently seems, that marking a script "async" will give it the lowest priority. But even though a script might not be needed for the first render, it might be desirable to have it load before, say, large decorative images on the page.

This issue is about (1) for the stylesheet resource type.

andershol avatar Sep 06 '18 17:09 andershol

Perhaps we can add a semantic that <link rel=stylesheet blocking="none"> would make parser-inserted scripts not render-blocking by default

noamr avatar Aug 01 '24 15:08 noamr

Maybe we can revive this issue?

Async CSS is pretty essential. It's the reason libraries like Font Awesome have to resort to <script> for their embed code. Tons of questions online about CSS async loading, all resorting to JS in one way or another, with the prevailing solution being this:

<link rel="stylesheet" href="/path/to/my.css" media="print" onload="this.media='all'">

LeaVerou avatar Aug 01 '24 17:08 LeaVerou

I still frequently find reasons for loading CSS asynchronously in my work. As @LeaVerou mentioned, the "print media hack" is commonly referenced as a workaround, but its reliance on JavaScript to apply CSS is a major downside.

The addition of an async attribute for the link element would allow developers to load CSS in a non-render blocking manner without relying on JavaScript. By default, I think this should cause a stylesheet to load at a low fetch priority, but the already standard fetch-priority attribute gives us control to change that when desired.

I'd love to volunteer my time to consult on specification or implementation work if that's helpful in any way. Thank you!

scottjehl avatar Jan 09 '25 21:01 scottjehl

<link rel="stylesheet" href="/path/to/my.css" media="print" onload="this.media='all'">

The addition of an async attribute for the link element would allow developers to load CSS in a non-render blocking manner without relying on JavaScript.

CSP settings avoiding inline JS also complicate this common workaround. Another win for a native implementation.

rajsite avatar Jan 09 '25 23:01 rajsite

I'm happy to submit a proposal for blocking=none as per https://github.com/whatwg/html/issues/3983#issuecomment-2263294010, but one q:

Why has putting those link tags at the beginning body not become a practice for this? It seems simple enough and potentially solves the problem. Is that a matter of popularizing it via DevRel/education? Or is it a material issue with this alternative?

noamr avatar Jan 09 '25 23:01 noamr

@noamr I like how that sounds but I think async feels more intuitive.

As for the question of in-body links, my understanding is they are sync and block rendering for HTML that follows them. Useful for different contexts but not the same. [update: browser behavior varies a lot here!]

scottjehl avatar Jan 10 '25 00:01 scottjehl

@noamr I like how that sounds but I think an attribute that's already in use would be much more intuitive (async).

blocking is also already in use, you use if to put blocking=render on scripts, or on dynamically inserted link elements. But currently it can only be used to add render-blocking and not remove it.

As for the question of in-body links, my understanding is they are sync and block rendering for HTML that follows them. Useful for different contexts but not the same.

That's incorrect AFAICT. <link> elements inside the body are always async as per spec and don't have the script-like semantics of blocking parsing.

We should be clear here with the use of the word sync - fetching of external resources can either block parsing or block rendering.

  • Only classic scripts block parsing.
  • Only elements in the head can block rendering (script, link).

So the following should just work:

<head>
   <title>...</title>
  <link rel=stylesheet href="blocking.css">
  <!-- stuff -->
</head>
<body>
  <link rel=stylesheet href="non-blocking.css">
  <!-- stuff -->
</body>

noamr avatar Jan 10 '25 09:01 noamr

That's incorrect AFAICT. <link> elements inside the body are always async as per spec and don't have the script-like semantics of blocking parsing.

These too are blocking. See https://codepen.io/bramus/pen/NPKYWrX/23a03a80e29b92fa80a41701959d1dba for a demo.

UPDATE: In Chrome and Safari. Not in Firefox.

bramus avatar Jan 10 '25 10:01 bramus

That's incorrect AFAICT. <link> elements inside the body are always async as per spec and don't have the script-like semantics of blocking parsing.

These too are blocking. See https://codepen.io/bramus/pen/NPKYWrX/23a03a80e29b92fa80a41701959d1dba for a demo.

UPDATE: In Chrome and Safari. Not in Firefox.

Yea I believe this is not per spec. It's a browser heuristic, not sure I like it :) Thanks for the clarification @bramus, I got this wrong indeed and was looking too much at the spec.

noamr avatar Jan 10 '25 10:01 noamr

Yeah, browsers all behave differently. 8 years ago, Jake wrote about it.

rik avatar Jan 10 '25 10:01 rik

Anyway, putting the <link> after the elements that don't rely on it and before the ones who do, with a corresponding preload if you must, sounds like an OK-ish practice?

I'm afraid that <link rel=stylesheet blocking=none> (or async or what not) would lead people in the direction of creating hard to detect FoUCs.

noamr avatar Jan 10 '25 10:01 noamr

Yeah, browsers all behave differently. 8 years ago, Jake wrote about it.

It was also covered by @csswizardry in https://csswizardry.com/2018/11/css-and-network-performance/#place-link-relstylesheet--in-body

bramus avatar Jan 10 '25 10:01 bramus

Perhaps we should have official chrome/MDN documentation about this.

  • Put it in the head if it must block rendering
  • Put it in the body before progressively-rendered elements, with preload if you must, with the Mozilla <script> hack if you wish.
  • If elements are dynamically loaded, dynamically load the link tag with them.

I still don't get the use case of the "preload it and apply when it's downloaded" a lot of people use, or for async. It makes sense for scripts, as some scripts are "value add" by themselves and nothing else depend on them, e.g. analytics, but CSS always comes with some HTML or script that relies on it, no?

Scripts and styles were not born equal, we should be careful with trying to apply the exact same semantics on them without weighing the consequences.

noamr avatar Jan 10 '25 10:01 noamr

Anyway, putting the after the elements that don't rely on it and before the ones who do, with a corresponding preload if you must, sounds like an OK-ish practice?

No, it is not: Safari and Firefox do not behave reliably and will block rendering of content before the <link>.

Even if all browsers behaved accordingly, it would still be great to have a spec and tests ensuring they keep behaving that way.

I still don't get the use case of the "preload it and apply when it's downloaded" a lot of people use, or for async. It makes sense for scripts, as some scripts are "value add" by themselves and nothing else depend on them, e.g. analytics, but CSS always comes with some HTML or script that relies on it, no?

The technique is used to avoid spending time rendering hidden content (whether it's "below the fold" or content that will only be displayed after some interaction) for the initial page load.

rik avatar Jan 10 '25 10:01 rik

Thanks for the context, I'd like to hear thoughts from @smaug---- / @zcorpan about this; Also from someone from Apple (@annevk?)

Also I see why this is more about being async than blocking=render, and that it would be good to have interop here.

noamr avatar Jan 10 '25 12:01 noamr

The technique is used to avoid spending time rendering hidden content (whether it's "below the fold" or content that will only be displayed after some interaction) for the initial page load.

Setting aside the notion that the current behavior should be spec'ed and interoperable, I'm still struggling with async being the right choice for this use case. Using it still risks FoUC, creating a race condition between the element appearing in the viewport / being opened for an interaction and the loading of the stylesheet.

The technique may make this FoUC not likely but still possible, which makes it racy/brittle. If a CSS is required for your element, it should probably block it. If it's required for a particular interaction, it should probably block the JS that enables this interaction or the buttons etc that can cause it. Anything else, and it's a race-to-FoUC.

Where async might be a reliable thing is for CSS that is a progressive enhancement, e.g. view transitions. But usually that CSS would also come with JS (in the view transitions case) or HTML in other cases.

Am I getting anything wrong here?

noamr avatar Jan 10 '25 13:01 noamr

Note that a parser-inserted <style>/<link rel=stylesheet> in body will still block (classic) <script>s. https://html.spec.whatwg.org/multipage/semantics.html#contributes-a-script-blocking-style-sheet

If it's desired to change that in addition to whether it blocks rendering, using async would make sense I think.

It's already possible to opt in to FOUC-style loading with some JS. If it's a common thing developers want (and they can somehow manage to avoid exposing users to FOUC), it seems nice to provide a declarative way to do so. However I am also a bit concerned about cargo-culting async attributes for all stylesheets and users get more undesirable FOUCs...

zcorpan avatar Jan 10 '25 13:01 zcorpan

@noamr

Thanks for chiming in. My mistake on the blocking attr! I'd forgotten about it being spec'd as it only works in chromium. I suppose I'm not opposed to blocking=false being the mechanism here if it's more semantically appropriate, but I do feel async is a more intuitive match for how this workaround already behaves.

Broadly, I don't think I disagree with many of your preferences about the use cases you mention, but I typically see this tool used to address different aims. My experience is that async CSS is typically used to mitigate/improve rendering performance in existing sites rather than as a building block for greenfield UI pattern development. As an aside, many CMSs in particular offer very little control over asset loading in the body as opposed to the head, so dev teams don't always have the option of that sort of composition in the body or the foot of the page even if it made sense for their needs.

In performance audits of large CMS driven sites, I find it's common to encounter render-blocking (and often third-party) links to CSS that isn't applicable to HTML in the initial page rendering. A popular way to get these out of the critical path and improve rendering time (and reduce SPOFs) is the print-media workaround, which relies on scripting and isn't available to many sites with CSP prohibiting onload attribute scripting. An example of a post that winds up landing on this pattern is https://csswizardry.com/2020/05/the-fastest-google-fonts/. In that case, developers are not afforded much control to improve google font's render-blocking request penalty, which delays FCP by about 1 second on 4G. There are other more ideal ways to load fonts of course, and in Google Fonts' case I've proposed this embed pattern, but still, standard async would pave a well-trodden path that folks have been using when they need it.

That's just one example. Another is when teams adopt a "critical" css loading strategy to move CSS loading into a tiered-priority groups: styles that are found to be applicable to the initially delivered HTML are deemed critical and loaded either inline or synchronously, while the rest is layered in async. Poor implementations of this approach certainly exist, but I've seen plenty of teams use it to great success.

Overall, it's a long-lived tool in performance teams' belts that should be available without JS hackery.

scottjehl avatar Jan 10 '25 15:01 scottjehl

I think fonts with font-display: swap are the clearest use case. See https://github.com/google/fonts/issues/2315 for example. You do want them to load ASAP, but don't consider them blocking for initial render if not available, as OK to use the fallback font.

And with Google Fonts being used on 60% of the web, there is potentially room for significant performance improvements by allow them to specify it as async by default. Plus as per @LeaVerou 's comment in https://github.com/whatwg/html/issues/3983#issuecomment-2263598366, it's similar for other font providers.

Of course you could just move the fonts CSS link to just before the </body> tag for a similar effect, but that could be discovered very late. Plus no font provider is ever gonna advise to put their stuff last.

P.S. We can talk about whether font-display: swap is a good thing, or was massively over pushed when it came out, as some of us (me!) hate the "font inflation" effect it gives, but that's a separate issue...

tunetheweb avatar Jan 10 '25 15:01 tunetheweb

Overall this looks like a benefit to be supported natively

I guess we don't want to turn this thread into bike shedding, but if an attribute is to be added async would make more sense. Developers are already familiar with it from the usage in script tags and blocking=none less so.

I was curious on use cases, but it does seem like fonts are the biggest one. For main-body CSS most sites don't split CSS into parts of the page which are viewed above or below the fold. There's some use case too for lazy loaded content, but I wonder if some of that overlaps with CSS Module Scripts which could dynamically load extra CSS along with additional content (although I guess this is specific to Shadow DOM stuff right now).

I'm afraid that (or async or what not) would lead people in the direction of creating hard to detect FoUCs.

I think this is a genuine concern, from the brief search I've done for usage of the the "print media" trick, it does look like some sites are doing it on their main CSS. I don't know if there's a genuine reason for this, or they're copy-pasting the idea from somewhere else without fully understanding the impact. I do wonder if there's tooling in future to catch this (like we have for detecting preloads which are not used within X seconds of window.load).

jasonwilliams avatar Jan 10 '25 16:01 jasonwilliams

Both the fonts and the non-critical-css use cases are:

  1. valid
  2. common
  3. racy in terms of FoUC, and thus footgunny

I think this tradeoff makes both of these use cases rather advanced, requiring care and expertise. When using font-display: swap, the effect is very explicit and contained to the particular CSS selector. But here it would be easy to get wrong and apply to too many things.

I would therefore hesitate before exposing this behavior declaratively in the web platform, especially behind a keyword that seems positive, easy to use and harmless like async (or blocking=none for that matter). I think changing the rel from preload to styesheet with script is less ugly from changing media from print and I think should work the same? Perhaps it's OK that these advanced/racy use cases rely on script?

Perhaps there are other use cases that are not in the category of "I know what I'm doing and I knowingly take the risk of FoUC"?

noamr avatar Jan 10 '25 17:01 noamr

The preload-to-stylesheet toggle you mention is another of many ways to produce a similar effect, but preload incurs a higher priority fetch that typically doesn't pair with the goals of async css. There are other alternatives to media toggling that offer the low priority fetch fwiw: for example, rel="alternate stylesheet" does an async low priority fetch.

The problem with all of these toggle hacks is that the mechanics of starting with an irrelevant setting and changing it back are not at all intuitive to anyone looking for a way to fetch a stylesheet without blocking the critical rendering path. Plus, they make CSS application reliant on scripting, and CSPs often block authors from using inline scripting for the onload handler anyway.

scottjehl avatar Jan 10 '25 17:01 scottjehl

The preload-to-stylesheet toggle you mention is another of many ways to produce a similar effect, but preload incurs a higher priority fetch that typically doesn't pair with the goals of async css. There are other alternatives to media toggling that offer the low priority fetch fwiw: for example, rel="alternate stylesheet" does an async low priority fetch.

The problem with all of these toggle hacks is that the mechanics of starting with an irrelevant setting and changing it back are not at all intuitive to anyone looking for a way to fetch a stylesheet without blocking the critical rendering path. Plus, they make CSS application reliant on scripting, and CSPs often block authors from using inline scripting for the onload handler anyway.

Understood, perhaps there can introduce something subtle where <link rel="stylesheet" fetchpriorty="low"> would be non-blocking? Or if that regresses browsers that don't implement this feature, do this only for <link rel="stylesheet preload"> or some such?

I think that this requires a tradeoff where things are perhaps not JS-bound but also not too enticing for non-experts, to avoid abuse resulting in increased FoUC.

noamr avatar Jan 10 '25 17:01 noamr

Personally, I'm not a big fan of mixing meaning like that and know @pmeenan spent some time separating preload (discovery) and fetchpriority (priority) semantics for Chrome when implementing the latter. Though that's not true for all browsers, which I presume is what @scottjehl was getting at when we said preload impacts priority?

tunetheweb avatar Jan 10 '25 17:01 tunetheweb

Yea I believe this is not per spec. It's a browser heuristic, not sure I like it :)

It is intentional and a good thing. I'd also like to see it encoded in the spec; see issue #1349. I hope WebKit ships this behavior like the other two browsers.

chrishtr avatar Jan 10 '25 18:01 chrishtr

Yea I believe this is not per spec. It's a browser heuristic, not sure I like it :)

It is intentional and a good thing. I'd also like to see it encoded in the spec; see issue #1349. I hope WebKit ships this behavior like the other two browsers.

Yea, I am convinced of it later in this thread :)

noamr avatar Jan 10 '25 18:01 noamr

Personally, I'm not a big fan of mixing meaning like that and know @pmeenan spent some time separating preload (discovery) and fetchpriority (priority) semantics for Chrome when implementing the latter. Though that's not true for all browsers, which I presume is what @scottjehl was getting at when we said preload impacts priority?

btw even today, putting the preload with a low fetch priority in the head, and then having the stylesheet itself at the bottom of the body is a solution that doesn't require scripting and sort-of does the right thing.

noamr avatar Jan 10 '25 18:01 noamr

Understood, perhaps there can introduce something subtle where would be non-blocking?

If that was the mechanics that folks feel is most intuitive to achieve an async CSS load, it'd be better than relying on scripted toggle hacks, but to me at least, it doesn't feel like what folks would look for intuitively. When I want to take a render-blocking script tag out of the critical path, I know I have defer and async at my disposal. Each option carries behavioral differences to be aware of, some of which can be undesirable and even cause a FOUC, but they're still useful and used to great effect.

Any of these features can be used to create a poor implementation and introduce layout shifts, but I do want to make sure we're not over-focusing on poor implementations of a helpful pattern. The request is to standardize a pattern that has been used for over a decade to improve rendering performance. I'd assume we all agree that introducing layout shifts is not a goal or worthy sacrifice in that pursuit, but an addressable artifact of poor implementation

scottjehl avatar Jan 10 '25 18:01 scottjehl