A standard method for loading style sheets asynchronously


#1

Since <link rel="stylesheet"> blocks rendering, it sometimes makes sense to defer loading a portion of the CSS code of a particular page in order to improve rendering performance (especially on mobile). This can be achieved by splitting the CSS code into “critical CSS” which is inlined in the <head> and non-critical CSS which is loaded asynchronously.

There is no standard way of performing this async load (afaik) and multiple non-optimal approaches have emerged:

  1. Scott Jehl’s JavaScript function dynamically injects a <link> to the page but temporarily sets its .media property to a “random” non-matching value and only sets it to "all" after the async load has finished.

  2. Another approach is to dynamically inject the <link> from within a requestAnimationFrame callback in order to make sure that the style sheet is loaded only after the first paint has occurred.

Both methods have their drawbacks and I’m not sure what the level of browser support is for them. Maybe we should have a standard method for this…


#2

I think the lazyload attribute achieves this: https://dvcs.w3.org/hg/webperf/raw-file/tip/specs/ResourcePriorities/Overview.html#attr-lazyload


#3

I was wondering why the spec doesn’t recycle defer from <script>, which already exists. I’m unable to pinpoint the part of the discussion, where the name change appeared.


#4

@Boldewyn I’m wondering the same thing. The lazyload attribute seems to be centered around changing the download order in scenarios of “network resource contention” and although the Resource Priorities spec does state that <link lazyload> does not block rendering, I would be happier if async and defer functionality was copied from <script> instead.

Btw <link rel="import" async> is already specced, so again, it makes more sense to have <link rel="stylesheet" async> I think.


#6

Update: We now have <link rel=preload> for this :tada:.


#7

In my team at Nikkei we’ve had some problems with this approach. Specifically, using this trick which was conceived by Scott Jehl:

<link rel='preload' href='/style.css' onload='this.rel="stylesheet"'>

Ideally, using this syntax, you would want a stylesheet that is in cache to be parsed and applied before rendering any of the markup that follows the link tag, as if it were declaratively written as a conventional <link rel='stylesheet'>. If the stylesheet is not in cache, we should proceed to render the following HTML, and then perform any necessary relayout when the stylesheet is loaded.

However, we find that the stylesheet is not guaranteed to be parsed before render if in cache, and regardless of the cache state, the moment at which the load event fires and the rel changes to ‘stylesheet’ can happen in the middle of the initial render, causing the browser to start the render again, and the stylesheet therefore being essentially render-blocking.

We’re experimenting with ideas such as:

<link re='preload' href='/style.css' onload='requestIdleCallback(() => this.rel="stylesheet")'>
<link re='preload' href='/style.css' onload='requestAnimationFrame(() => this.rel="stylesheet")'>

Does anyone else have any experience with this?


#8

Would it help if the onload handler waited for the DOMContentLoaded event before applying the stylesheet? I’m not sure if it’s a guarantee that the initial render has completed, but if it is, then that seems like a good time to apply async stylesheets (and if not, then maybe an event type for “initial render” could be proposed for this use case).


#9

Ah, so something like

<link re='preload' href='/style.css' onload='document.readyState == "loading" ? document.addEventListener("DOMContentLoaded", () => this.rel="stylesheet") : this.rel="stylesheet";'>

We could give that a go, but as with the other solutions, the main problem is ensuring that the stylesheet will apply immediately if it is available in cache. The method above will wait until DOMContentLoaded even if the stylesheet is available before we hit the opening <body> tag.


#10

A bit on the technical edge (*) but how about using the Resource Timing API’s transferSize to determine if the resource came from the cache ?

Roughly:

  1. Retrieve the performance entry for style.css via getEntriesByName (*)
  2. Check if the resource came from the cache via its transferSize attribute. Flip the rel to stylesheet accordingly.

Note: I’m not entirely sure if it’s guaranteed that there will be a corresponding entry in the performance timeline by the time the onload event fires. Alternative: register a performance observer listening for style.css’s performance entry.

*: transferSize and related fields are available in Firefox (info) and it just landed in Chrome 54.

However, I’m wondering if instead of “apply immediately if it is available in cache” it wouldn’t be better to aim for “apply immediately if it quickly became available”. Thoughts?


#11

Had a chat with @KenjiBaheux and we came up with something like this (very rough):

<script>
var beforeFirstPaint = true;
requestAnimationFrame(() => {
  beforeFirstPaint = false;
});
function onScriptLoad(elPreload) {
  if (beforeFirstPaint) {
    elPreload.rel = 'stylesheet';
  } else {
    document.addEventListener('DOMContentLoaded', () => elPreload.rel = 'stylesheet');
  }
}
</script>
<link rel='preload' href='style.css' onload='onScriptLoad(this)'>

#12

Why is that the ideal?

Does “cached” really matter here, or is it about timing? It’s possible for the network to respond quicker than the cache.


#13

Ah, OK, fair point. So I guess I should have said if the stylesheet is available before we start the first layout task, then apply it before layout begins, otherwise delay until DOMContentLoaded to avoid restarting an in-progress layout.

Does that rationale make any sense?


#14

I’m still not sure what the goal is in terms of visual result.

Why is FOUC sometimes good & sometimes bad here? Is it because a quick FOUC is bad but a longer FOUC is less bad?


#15

My actual problem with this pattern <link rel='preload' href='/style.css' onload='requestAnimationFrame(() => this.rel="stylesheet")"'> is that, if (all) stylesheets are cached. The link[preload] transformation causes an unnecessary cycle of style and layout recalculations.

I think this is also the main problem @triblondon has. He only wants to circumvent this by delaying it further so that the user has a faster first impression.

In general: The style calculations should wait until the developer has a chance to do the rel transform. Or we need some new semantics. An async attribute would be just nice.


#16

I’m still not sure what the goal is in terms of visual result. Why is FOUC sometimes good & sometimes bad here?

FOUC is always bad. But if that’s happening, you are using the async loading technique wrong. The idea is to take out of the main stylesheets, those parts that have a strong impact on the page layout. This includes the code that is positioning and sizing main elements, setting fonts and colors etc. We place these in a separate style tag in the head, inlined, so it’s guaranteed to be available before the first paint.

Usually, that still leaves a ton of style rules for all sorts of things that either only happen later (e.g. dialog boxes, items being expanded, menus being opened etc) or have only a minor visual impact on the overall design, or are styling content that wil only be loaded later (e.g. user avatars, profile info etc). These styles are the ones that we want to load async and we want this loading to start ASAP so it gets to us as quickly as possible (minimize latency), but we don’t want to block rendering for it because we already have all the critical CSS available anyway.