Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Async css #3983

Open
andershol opened this issue Aug 31, 2018 · 61 comments
Open

Async css #3983

andershol opened this issue Aug 31, 2018 · 61 comments
Labels
addition/proposal New features or enhancements needs incubation Reach out to WHATWG Chat or WICG for help topic: link topic: style

Comments

@andershol
Copy link

andershol commented Aug 31, 2018

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)

@sashafirsov
Copy link

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.

@andershol
Copy link
Author

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.

@noamr
Copy link
Collaborator

noamr commented Aug 1, 2024

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

@LeaVerou
Copy link

LeaVerou commented Aug 1, 2024

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'">

@scottjehl
Copy link

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!

@rajsite
Copy link

rajsite commented Jan 9, 2025

<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.

@noamr
Copy link
Collaborator

noamr commented Jan 9, 2025

I'm happy to submit a proposal for blocking=none as per #3983 (comment), 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?

@scottjehl
Copy link

scottjehl commented Jan 10, 2025

@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!]

@noamr
Copy link
Collaborator

noamr commented Jan 10, 2025

@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>

@bramus
Copy link

bramus commented Jan 10, 2025

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.

@noamr
Copy link
Collaborator

noamr commented Jan 10, 2025

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.

@rik
Copy link

rik commented Jan 10, 2025

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

@noamr
Copy link
Collaborator

noamr commented Jan 10, 2025

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.

@bramus
Copy link

bramus commented Jan 10, 2025

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

@noamr
Copy link
Collaborator

noamr commented Jan 10, 2025

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.

@rik
Copy link

rik commented Jan 10, 2025

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.

@noamr
Copy link
Collaborator

noamr commented Jan 10, 2025

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
Copy link
Collaborator

noamr commented Jan 10, 2025

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?

@zcorpan
Copy link
Member

zcorpan commented Jan 10, 2025

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 zcorpan added the addition/proposal New features or enhancements label Jan 10, 2025
@scottjehl
Copy link

@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.

@tunetheweb
Copy link
Contributor

tunetheweb commented Jan 10, 2025

I think fonts with font-display: swap are the clearest use case. See google/fonts#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 #3983 (comment), 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...

@jasonwilliams
Copy link

jasonwilliams commented Jan 10, 2025

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).

@noamr
Copy link
Collaborator

noamr commented Jan 10, 2025

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"?

@scottjehl
Copy link

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.

@noamr
Copy link
Collaborator

noamr commented Jan 10, 2025

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.

@scottjehl
Copy link

@chrishtr Barry said as much already, but using the end of the page like that introduces the potential for a very late fetch, and more generally requires us to (ab)use source order to configure loading behavior, which isn't something we need to do for other asset types anymore. For example, we used to need to put scripts at the end of the page but defer has thankfully made that approach unnecessary. Centralizing the area where page resources are referenced is great for clarity, whereas asking teams to split their asset loading strategies by source order would be another in-the-know workaround even if it achieved the desired effect.

@chrishtr
Copy link
Contributor

chrishtr commented Jan 10, 2025

potential for a very late fetch

It won't be too late due to the preload scanner, and if the developer wants it to be even sooner they can add a preload to the head. Right?

(ab)use source order to configure loading behavior

Style sheets are special though, because they directly affect rendering of the content. (Scripts, on the other hand, often have nothing to do directly with rendering.) Therefore style sheets have a direct, intimate connection to the HTML. This is why I think developers just need to know that "style sheets load before anything after it in the HTML" (or "style sheets always apply to content after them in the HTML [but not always to content before, during loading]") and then act accordingly.

I hear you that centralizing things can be good for ergonomics for developers who understand what is going on, but I agree with @noamr that there is a corresponding risk for developers who are less versed in this; putting the sheet at the "end of the body" (a) already works (in Chromium and mostly Gecko so far) and (b) hopefully makes them think more about the implications in a more intuitive way.

@scottjehl
Copy link

It won't be too late due to the preload scanner

You are probably closer to the particulars on that part than me, but my understanding is you'd be relying on the amount of HTML that has streamed, so fetch timing by source-order location would correlate to the size of the HTML document and vary by network connectivity. To be fair, the fetch timing drawback of this approach is one of the lesser reasons that it feels like an improper fit. It's more that it's an unintuitive pattern to need to employ for loading behavior, and doesn't fit with how teams typically configure their assets. The expected convention for many other resource types now is to be able to declaratively control your resources' fetch behavior via attributes, independent of their location in source order.

As some of the use cases above described, a stylesheet fit for async loading typically doesn't contain styles that need to block rendering, so it's not a matter of finding the right dependent element to precede with a link. It's more literally like an asynchronous script use case: the stylesheet isn't critical for initial rendering and is expected to load in parallel and apply whenever it lands (as the print-media workaround has been popularly used to do for quite some time).

@chrishtr
Copy link
Contributor

You are probably closer to the particulars on that part than me, but my understanding is you'd be relying on the amount of HTML that has streamed, so fetch timing by source-order location would correlate to the size of the HTML document and vary by network connectivity.

Yes, the size of the HTML document will delay the load somewhat. But since the preload scanner is a separate parser that is optimized to find just resource links, the delay should be short. And for sites which don't want even that delay, they can add a preload in the head.

@pmeenan
Copy link
Contributor

pmeenan commented Jan 10, 2025

You are probably closer to the particulars on that part than me, but my understanding is you'd be relying on the amount of HTML that has streamed, so fetch timing by source-order location would correlate to the size of the HTML document and vary by network connectivity.

Yes, the size of the HTML document will delay the load somewhat. But since the preload scanner is a separate parser that is optimized to find just resource links, the delay should be short. And for sites which don't want even that delay, they can add a preload in the head.

Realistically, if the size of the HTML is enough to be measurable for the induced delay, there are going to be a LOT of resources in it and the discovery time is going to be a rounding error compared to basic ordering and prioritization.

@tunetheweb
Copy link
Contributor

As I said above, the loading delay can be worked around (with a link rel=preload) but I am more concerned with the execution delay. For the fonts use case, loading at the bottom of the page means you're using the fallback font for later.

Being able to load AND async execute in the <head> would allow that to happen much earlier if it loads earlier.

And the only way to work around that is with the <link... onload> hack.

FYI, I ran an HTTP Archive query and 17,534 sites (out of 16,257,087) use this hack:
https://docs.google.com/spreadsheets/d/1yNAgB-exdK105aMXj0eZZXiGL7Gbd1AsnEJoJoXtC5w/edit?gid=388083267#gid=388083267

To be honest that's less than I thought would be using it!

@zcorpan
Copy link
Member

zcorpan commented Jan 13, 2025

@chrishtr the important part is not if it's fetched early but when it is applied, which depends on the length of the HTML for the "stylesheet at the end of body" solution. It would be OK only for short documents, but not all documents are short.

@tunetheweb 0.1% of all pages in httparchive is a lot for a hack. I expect more pages would use async if it was available (easier to use, more discoverable, and doesn't conflict with CSP rules for scripting). The main question for me is whether the user experience would be net improved or regressed. As discussed, this feature is footgunny, and there's a risk of regressing the user experience (if more pages use it incorrectly than pages using it correctly). We could maybe mitigate this by making the name very clearly communicate that you don't want to use it generally (e.g. async="this-causes-a-flash-of-unstyled-content").

@scottjehl
Copy link

@zcorpan Thanks. I agree that's a lot of usage.

I can appreciate the concern for potential misuse of any feature. I do think there's precedent for opt-in standard features that can cause undesirable performance when used for common/default use cases (which is why it's great they're opt-in). For example, img[loading=lazy] is a great feature, yet it is not recommended for images that are likely to be visible at load. Perhaps more so than loading=lazy, loading a critical stylesheet asynchronously causes a noticeable degradation in rendering, which I'd hope/expect to discourage improper use on its own.

@jasonwilliams
Copy link

jasonwilliams commented Jan 13, 2025

I'd prefer to remain consistent with scripts and have async for attribute. The cost of an explicit name outweighs the familiarity of what we already have in my opinion.

There might be some help that tooling can provide.

Whilst I don't have a static solution to deter mis-use, I do think the UA can track if the viewport was styled (on startup, before arbitary time) by an async link, then warn the user of bad practice in the console.
I'm not sure how expensive that check is but it can immediately be skipped if no async-link is used so those not using the feature aren't affected.

I think you could also lint against "main.css" having an async attribute, something I've seen when searching for the print-styles hack.

@noamr
Copy link
Collaborator

noamr commented Jan 13, 2025

I wonder if instead of enabling this we could seek alternative solutions to the issue of loading stylesheets for non-critical/below-the-fold UI that don't cause this UX degradation and somehow mitigate the (ergonomic?) shortcomings of the load-early/apply-on-time technique?

@jasonwilliams I think trying to treat styles & scripts in the same way is not the right way to go about this. Those beasts are different. See this comment.

@scottjehl
Copy link

There are concerns that people might use this feature incorrectly. To assess whether that concern is great enough to make this feature less intuitive by design, maybe it'd help to see examples for other opt-in features that were popularly mistaken for a default in this way. My inclination is to expect that authors will be as capable of understanding this opt-in attribute as they already are of many other non-default attributes (many of which come with behaviors that may or may not fit a use case). Maybe there's precedent I'm not aware of, and it'd help here.

Throughout the thread, folks seem to agree on the utility of this feature. There's clearly a great deal of usage for the workaround on the web, and the number of +1s here show large support for the proposal.

@noamr
Copy link
Collaborator

noamr commented Jan 14, 2025

I wonder if we could do something like the following:

<link rel=stylesheet href="widget.css" for="widget" >

Where this would load the style, but would only apply it when an element with an ID of widget (in this example) is seen, blocking rendering at that point. This way, if configured correctly, there will be no FoUC, and you can still declare this style in one place, the head of you so choose.

@tunetheweb
Copy link
Contributor

tunetheweb commented Jan 14, 2025

How would that work for the fonts use case?

It would work for all the other use-cases (below-the-fold, non-critical etc). I think that for font-display: swap we might need something a bit more specific.

@noamr
Copy link
Collaborator

noamr commented Jan 14, 2025

How would that work for the fonts use case?

It would work for all the other use-cases (below-the-fold, non-critical etc). I think that for font-display: swap we might need something a bit more specific.

Oops, I edited your comment instead of replying by mistake :)

@noamr
Copy link
Collaborator

noamr commented Jan 14, 2025

@tunetheweb I think we could have two separate solutions here.

<!-- This would load (like preload), but would only apply & block parsing when an element with ID="some-id" is seen -->
<!-- If the target element is something like an async script, we can keep parsing but block the script execution on the stylesheet --> 
<link rel=stylesheet href="widget.css" for="some-id">

<!-- This would load the stylesheet asynchronously, but would only apply font descriptors and whatever they need -->
<link rel="font stylesheet" href="https://fonts.example.com/api?my-font">

We could then update the Google Fonts recommended <link> tag to use <link rel="font stylesheet">.

I wonder if there are more use cases.
@scottjehl are you familiar with other use cases, beyond:

  1. Font swapping
  2. Non-critical/below-the-fold CSS

@scottjehl
Copy link

I wonder if there are more use cases.

Sure thing, though I hope that introducing more use cases here won't cause a distraction - we already have a couple of common cases that we've all agreed are valid enough. Other use cases would include patterns where sections of HTML are included somewhere in the page and not used in the initial page render, but may be visible at a later time. I'm riffing here, but I could imagine a rather complex hidden-by-default dialog/popover/navigation section of a document that may be paired with its own stylesheet (a common situation for UI components). You may not want that stylesheet to block rendering of the "dialog" HTML nor any other HTML that follows it because it's meant to be hidden until it's revealed sometime long after page load. That's a nice case for an async stylesheet that doesn't disrupt the rest of the page, and serves to free up the critical path. There are surely many parallels with embed snippets (3rd party or not) that bring their own stylesheet: a help-chat widget that can layer in and appear whenever it happens to load, for example. Along those lines, Google fonts is an example of an embed code that causes undesirable blocking behavior that can be mitigated by going async. It's a popular example of a fairly common problem with embeds.

My experience is that asynchronous use cases happen often in complex UIs and even the two main use cases cited throughout the thread show that this pattern is frequently not tied to any particular element's rendering or even existence at load time. We mentioned why the font loading example isn't tied to the rendering of one particular element, but the critical CSS patterns we've cited aren't either. Nowadays, these patterns have less to do with older, finicky "above the fold" style isolation, and instead are about a simpler distinction of synchronously loading CSS that is necessary for rendering HTML that's going to be visible in the initial page load, and asynchronously loading the rest of the CSS (which might well be the CSS for the entire website, which if it's a SPA for example is quite elegant.). In all cases, this pattern is used in the aim of a faster initial page render, and is not used at the cost of any layout shifts of FOUC.

the for="some-id"> example

The for attribute suggestion introduced above, particularly paired with a new concept (for link elements at least) of using it like a label/input pairing relationship that targets one particular element, seems to me to be limited in scope and conceptually unexpected, but most importantly just tangential to the simple goal of this proposal, which is simply to load a stylesheet asynchronously. Specifically, render-blocking of any HTML is behavior we're intending to avoid with this pattern, so blocking for any particular element selector is a rather different concept than what this thread describes.

To back up and refocus...

The async attribute is a commonly known option in resource loading. The way it behaves with a script matches the behavior of this proposal 1-1. Before desiring (and designing) a different concept based on concerns that async could be misused, I think we should have consensus and precedent in this thread that async is indeed more problematic than another attribute that we may or may not want to use.

And on that particular topic, it's worth noting that script[async] itself is an improper fit for just tossing it on any-old-script: with scripts, defer is more versatile and useful for typical UI enhancement scripting, while async is useful for independent scripts that really can load whenever they load. That's less common, but often handy.

Developers understand the difference between default, defer, and async and it's great we have options. Sometimes, with scripts and indeed CSS, async is just what you need.

@noamr
Copy link
Collaborator

noamr commented Jan 14, 2025

I wonder if there are more use cases.

Sure thing, though I hope that introducing more use cases here won't cause a distraction - we already have a couple of common cases that we've all agreed are valid enough. Other use cases would include patterns where sections of HTML are included somewhere in the page and not used in the initial page render, but may be visible at a later time. I'm riffing here, but I could imagine a rather complex hidden-by-default dialog/popover/navigation section of a document that may be paired with its own stylesheet (a common situation for UI components). You may not want that stylesheet to block rendering of the "dialog" HTML nor any other HTML that follows it because it's meant to be hidden until it's revealed sometime long after page load. That's a nice case for an async stylesheet that doesn't disrupt the rest of the page, and serves to free up the critical path. There are surely many parallels with embed snippets (3rd party or not) that bring their own stylesheet: a help-chat widget that can layer in and appear whenever it happens to load, for example. Along those lines, Google fonts is an example of an embed code that causes undesirable blocking behavior that can be mitigated by going async. It's a popular example of a fairly common problem with embeds.

Thanks for the details!
I see this as the same use case, "non critical UI" or "below the fold".

If the dialog is opened before the CSS is loaded, that would cause an FoUC. As a user I encounter many websites with this pattern unfortunately... You click a dialog, and it's unstyled for a sec before its CSS loaded.

@scottjehl
Copy link

Those are the main use cases I have run into, yes. There were a few examples mentioned in there - if the dialog one isn't as compelling, the "help-chat" widget (substitute any non-essential overlay UI here) example seems to me common and reasonable.

@noamr
Copy link
Collaborator

noamr commented Jan 16, 2025

Having spoken with @scottjehl offline, I recognize that a valid use case for this is when the elements being styled asynchronously have some kind of default CSS that makes them hidden until the CSS is loaded, which would make them "styled into being visible". This is a legitimate use case because the resulting UX is not a FoUC.

I wonder if the web platform should enable this use case directly, or if that's enough to justify the simpler-but-footgunny "async" attribute.

@noamr noamr added the agenda+ To be discussed at a triage meeting label Jan 16, 2025
@hsivonen
Copy link
Member

<!-- This would load (like preload), but would only apply & block parsing when an element with ID="some-id" is seen -->
<!-- If the target element is something like an async script, we can keep parsing but block the script execution on the stylesheet --> 
<link rel=stylesheet href="widget.css" for="some-id">

How do you intend this to work when the <foo id="some-id"> comes from document.write? (Previously, we've thought it bad for the point at which document.write returns to depend on whether an external script is already in the cache or not.)

For markup coming from the network, it does not seems as problematic to allow arbitrary elements to block the parser, though it doesn't seem particularly great for performance to have to perform a "does the parser need to be blocked on ID match" on every element", but perhaps it can be optimized well enough by making documents carry a flag indicating whether this feature is in use at all, which would provide for a quick check for all the pages that don't use the proposed feature.

@itsdouges
Copy link

Lazy loading themes would be useful.

@noamr
Copy link
Collaborator

noamr commented Jan 18, 2025

Lazy loading themes would be useful.

Sure! How would you envision this working when the user changes theme before the theme's css is loaded?

@noamr
Copy link
Collaborator

noamr commented Jan 27, 2025

As per the WHATNOT discussion, created a WICG proposal: WICG/proposals#195

@past past removed the agenda+ To be discussed at a triage meeting label Jan 29, 2025
@scottjehl
Copy link

scottjehl commented Jan 29, 2025

A comment that expands on many of the use cases in this issue, as well as why the existing "print media" workaround already does exactly what we need, and only has a few drawbacks that a standard async attribute would address:

WICG/proposals#195 (comment)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
addition/proposal New features or enhancements needs incubation Reach out to WHATWG Chat or WICG for help topic: link topic: style
Development

No branches or pull requests