Skip to content

Instantly share code, notes, and snippets.

@xiaochengh
Last active November 13, 2021 00:14
Show Gist options
  • Save xiaochengh/c29c6246ead0cbb72708f10baa89bde4 to your computer and use it in GitHub Desktop.
Save xiaochengh/c29c6246ead0cbb72708f10baa89bde4 to your computer and use it in GitHub Desktop.
(Obsolete) Feature proposal: `renderblocking` attribute on <link> and <script> elements

This proposal is obsolete. Please refer to the new version.

Explainer

All current browsers have a render-blocking mechanism: after navigation, the user agent will not render any pixel to the screen before all stylesheets and synchronous scripts in <head> are loaded and evaluated (or a UA-defined timeout is reached)1. This prevents a Flash of Unstyled Contents (FOUC) and ensures critical scripts (like framework code) are executed, so that the page is usable after the first rendering cycle.

This proposal extends the above idea and proposes a new attribute renderblocking that can be added to <link> and <script> elements in <head> to support more use cases:

  • Block rendering on a critical web font to prevent layout shifting or a flash of invisible/unstyled text (see here for more details). It also works in the preloading of critical resources of other types, like images, json, etc.
<link renderblocking rel="preload" href="critical-font.woff2" as="font" type="font/woff2" crossorigin>
  • Block rendering on an async script, so that the script doesn’t block parsing but is guaranteed to be evaluated before rendering starts. This is useful for, e.g., SPA loader and A/B test framework scripts, which can be loaded asynchronously but need control over what is displayed on the first render.
<script renderblocking async src="async-script.js"></script>
  • Block rendering on script-inserted stylesheets or scripts. This prevents a FOUC when, e.g., the page uses a loader script to load the actual business stylesheets:
<script>
let link = document.createElement('link');
link.rel = 'stylesheet';
link.href = 'business-style.css';
link.setAttribute('renderblocking', '');
document.head.appendChild(link);
</script>
  • Besides, while rendering is blocked, requestAnimationFrame() callbacks should not be fired2. So we can add a requestAnimationFrame() in <head> to perform some tasks right before the first rendering cycle to, e.g., ensure a better-looking first paint:
<script>
requestAnimationFrame(() => {
  let status = getResourceLoadingStatus();
  putProgressBarOnPendingResources(status);
});
</script>

Prerequisites

This proposal is blocked on #3355.

In particular, this proposal requires the notion of “render-blocking resource”, so that after a navigation, the UA can finish loading all render-blocking resources first and then start rendering. This notion hasn’t been specified yet, but all browsers are treating external stylesheets as render-blocking resources (with subtle differences) to avoid a FOUC.

Before resolving the issue, this proposal assumes the following model:

  • After a navigation, the browsing context will first enter a “render-blocking period”, during which it has no rendering opportunities; it may have rendering opportunities only after the render-blocking period ends
  • During the render-blocking period, the UA collects certain “render-blocking resources” from the document, adds them into a set of pending render-blocking resources, and fetches them
  • When a render-blocking resource finishes loading (either successfully or exceeds a UA-defined timeout), it is removed from the set of pending render-blocking resources
    • For scripts, they are removed after evaluation instead of loading finishes
  • The render-blocking period ends when the UA cannot find any render-blocking resource, or the set of pending render-blocking resources becomes empty

Currently, all browsers treat external stylesheets and non-deferred scripts as render-blocking resources, so the above assumption seems reasonable.

Proposal Details

This proposal introduces a new boolean attribute renderblocking on the <link> and <script> elements. The attribute is effective only when all of the following requirements are satisfied:

  • The element is a descendant of the document <head> element
  • The element is inserted into the document before the parser inserts the <body> element
    • When on a <link> element, the link type must be stylesheet or preload
    • When on a <script> element, the defer attribute must not be set
  • The other attributes together allow the UA to fetch the resource
    • For example, the element is not disabled, media query must evaluate to true, ...

Whenever an element with an effective and true renderblocking attribute is inserted into the document, UA should make it render-blocking by adding it into the set of render-blocking resources, and clear it when the resources is loaded or the script is loaded and evaluated, or a UA-defined timeout is exceeded.

Note: The attribute works for both parser-inserted and script-inserted elements as long as it’s inserted before parser inserts <body>. This means that if an async script tries to insert a render-blocking stylesheet or script, since async scripts don’t block parser, the inserted element may or may not be actually render-blocking. This is due to the racy nature of async scripts, which this proposal has no intent to change.

Interactions with other specs

  • [HTML] In the update the rendering step, animation frame callbacks will not be run when rendering is blocked, because the document has no rendering opportunities.
  • [Priority Hints] If a resource is render-blocking, its importance will be overridden to high, so that rendering won’t be blocked on low-priority resources.

Possible extensions

Explicit timeout

We may extend the syntax into, for example, renderblocking=500ms. This overrides the UA-defined timeout, and makes the resource render-blocking for at most 500ms.

Pros:

  • This may be useful for certain images (like progressive JPEG), so that they can be rendered at a lower quality before fully loaded.
  • This gives developers control over the UX if the connection is slow. For example, if my webfont really is taking several seconds for a user with a slow connection, forcing them to stare at an unrendered page while the font downloads can be a really bad tradeoff. Having a timeout would solve this issue.
  • Power developers (like AMP) may know how complicated their client-side rendering is to fine tune that timeout. Maybe they can afford more leeway for network latency if their first frame renders quickly enough. Cons:
  • A good timeout value depends on the connection speed, which varies user by user. Developers may not have a good strategy to set this timeout, which means this extension doesn’t make things better. The user agent is in a better position to understand these kinds of tradeoffs.

Integration with the loading milestones proposal

Prerequisite: yoavweiss@’s proposal.

The renderblocking attribute is essentially the before-first-paint milestone in Yoav’s proposal, so we may want a syntax that may be extended into the broader proposal. For example, blocking=rendering/DCL/load/false and etc. This is still at a very early stage and not fixed yet.

renderblocking=false

We may want to allow a renderblocking=false syntax to explicit make a resource non-render-blocking. This allows us to make a non-critical stylesheet not block rendering, which developers currently rely on various workarounds.

beforefirstrender event

Add a beforefirstrender event that fires after all render-blocking resources have finished loading, and before the first rendering lifecycle update.

It gives the developer a chance to deterministically control the first frame based on which non-critical resources were fetched or timed out, how much DOM parsing was finished etc. This hook may be used to abort or modify a transition. For example, if they'd rather show a loading screen UI instead.

The event is not included by the proposal, since it’s equivalent to the first requestAnimationFrame().

Further Reading

Eliminate layout shifting for critical web fonts

When the page has a critical web font served via a channel that is fast in most circumstances (e.g. an AMP page using a font from Google Fonts), it is reasonable for developers to aim at a UX similar to locally installed fonts. In particular, both of the goals below should be achieved:

  1. The page layout is stable. There should not be any flash of invisible text or layout shifting due to swapping from a local fallback font to the web font.
  2. The web font should eventually be used to render the page.

However, all current solutions are either unreliable, hacky or cumbersome:

  • font-display: optional guarantees stable layout, but comes at the cost that the critical web font might not be used for rendering. If the web font doesn’t finish loading when needed, it will never be used and the page ends up in a fallback font.
  • Preloading an optional web font guarantees stable layout and makes the font more likely to be picked up by rendering, but still doesn’t guarantee the font being used.
  • Preload an optional web font and block rendering with other means/hacks, like using Javascript or adding an empty external stylesheet (as in crbug.com/1231827). This has a much higher chance to guarantee both objectives. However, it seems pretty bad to recommend hacks to web developers.
  • Adjust fallback font metrics with @font-face descriptors, so that the layout shift is minimized when swapping fonts. While achieving both goals, it’s not easy for web developers to adopt, as there are many adjustment parameters to figure out. It would be better rolled out by web font providers.

Both goals can be accomplished by preloading the web font with renderblocking.


Footnotes

[1] The exact behaviors differ slightly.
[2] Current browsers have different behaviors, and this proposal would like to standardize it. See test case.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment