Skip to content

Instantly share code, notes, and snippets.

@xiaochengh
Last active May 23, 2022 18:48
Show Gist options
  • Save xiaochengh/9404abbecdc3b45c0e9f3d5e99fbc65d to your computer and use it in GitHub Desktop.
Save xiaochengh/9404abbecdc3b45c0e9f3d5e99fbc65d to your computer and use it in GitHub Desktop.

Updates

  • May 2022: The attribute has been removed from <link rel="preload"> and <link rel="modulepreload">. We will pursue a CSS-based solution for the web font use cases. See

Explainer

All current browsers already 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.

In this proposal, we extend the above idea and propose a new attribute blocking that can be added to <link>, <script> and <style> elements in <head>. For now, we would only allow one value blocking="render" to support the most demanding use cases, but we would also like to keep the syntax open for future extensions.

Use cases

  • 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('blocking', 'render');
document.head.appendChild(link);
</script>
  • 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 client-side A/B test framework scripts, which can be loaded asynchronously but need control over what is displayed on the first render.
<script blocking="render" async src="async-script.js"></script>
  • Remove the attribute to manually unblock rendering. This allows us to achieve different tradeoffs, for example, a complete elimination of layout shifts if the connection is fast, or a faster first paint with some layout shifts if the connection is slow.
<script id="async-script" blocking="render" async src="async-script.js"></script>
<script>
  setTimeout(() => document.getElementById('async-script').blocking = '', 1500);
</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>

Technical Details

See pull request #7474 for the exact details.

The contents below might be out-of-date and are just for record-keeping purposes.

The "render-blocking" mechanism

This proposal is blocked on #3355.

In particular, this proposal requires an explicit definition of the render-blocking mechanism, so that after a navigation, the UA will not start rendering before finishing loading all the render-blocking resources. 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.

To standardize the behavior, we propose the following changes to the HTML Standard:

  1. In the create and initialize a Document object steps, add a new step at the end that sets up two new flags: Let awaitingParserInsertedBody be false, and renderBlockingCount be 0.
  2. In page load processing model for HTML files:
    • After creating and initializing document, change its awaitingParserInsertedBody value to true.
    • After the associated HTML Parser inserts a body HTML element to document, change the value of awaitingParserInsertedBody to false 3.
  3. In the definition of rendering opportunities, add a criterion that a browsing context has a rendering opportunity only if both are true on its active document:
    • The value of awaitingParserInsertedBody is false
    • The value of renderBlockingCount is 0

Then #3355 can be resolved by the stylesheet link fetching algorithm, which essentially makes parser-inserted stylesheets in <head> render-blocking.

Note 1: This setup allows the user agent to increase renderBlockingCount when starting to fetch a render-blocking resource, and decrease it when the fetching finishes. As a result, there is rendering opportunity only after all render-blocking resources have finished loading. The exact behavior will be specified in detail in section the processing algorithms.

Note 2: By this setup, the render-blocking mechanism is limited to HTML documents only. Other types of documents are unaffected.

Note 3: requestAnimationFrame() callbacks will not be run when rendering is blocked. This is because in the update the rendering step, animation frame callbacks are run only when the browsing context has a rendering opportunity.

The blocking attribute

This proposal introduces a new attribute blocking on the <link> and <script> elements. The attribute value is a set of space-separated tokens, where the only supported token is render, explicitly marking the resource as render-blocking. The user agent should ignore all tokens that are unsupported.

The attribute will instruct the processing algorithm of the resource to modify the value of renderBlockingCount of the document, and therefore achieve render-blocking.

Note 1: The syntax is designed for forward compatibility, as future extensions may add new tokens.

Note 2: The attribute works for both parser-inserted and script-inserted elements as long as they are inserted before the HTML parser inserts <body>. The implications include, for example, 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.

The processing algorithms

The basic idea is that, on fetching a resource marked with blocking="render", we decide whether this resource should be treated as render-blocking; if so, increment the renderBlockingCount value, and later decrement it when the fetching finishes. This allows us to reuse the existing algorithms to check, e.g., if the element is disabled, if a preload has a correct as attribute and etc.

<link rel=stylesheet>

In the stylesheet linked resource fetch setup steps, add a new step at the end before returning true:

If el's media attribute's value matches the environment, and el's node document's awaitingParserInsertedBody value is true, and el is parser-inserted or the blocking attribute contains token render, then

  • We say el contributes a render-blocking resource.
  • Increment el's node document's renderBlockingCount value by 1.

Note: We intentionally make parser-inserted stylesheets render-blocking by default even without the blocking attribute. This is compatible with what current browsers already do to avoid a FOUC.

In the process the linked resource steps for stylesheet steps, add a new step at the end:

If el contributes a render-blocking resource, then:

  1. Assert: el's node document's renderBlockingCount value is greater than 0.
  2. Decrement el's node document's renderBlockingCount value by 1.

<link rel=preload>

In the fetch and process the linked resource steps, add before step 9 (the fetch request step):

If el's blocking attribute contains token render, and el's node document's awaitingParserInsertedBody value is true, then:

  • We say el contributes a render-blocking resource.
  • Increment el's node document's renderBlockingCount value by 1.

The fetch request should use a processResponseDone algorithm of the following steps:

If el contributes a render-blocking resource, then:

  1. Assert: el's node document's renderBlockingCount value is greater than 0.
  2. Decrement el's node document's renderBlockingCount value by 1.

<script>

(WIP)

Interactions with other specs

  • [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

This section includes ideas that we discussed but chose to not include in the minimum viable product for various reasons. They may be added into later versions.

Other operations to block/unblock on

Note: this part shares a lot of common ideas with the before-foo milestones in yoavweiss@’s proposal.

As mentioned before, the syntax of the attribute is intentionally left open for extensions. We may introduce a negation syntax, so that each resource can selectively block and unblock certain operations (or “milestones” as termed by Yoav’s proposal) in the page’s lifetime. For examples:

  • Async CSS:
<link blocking="!render !parse" rel="stylesheet" src="async-sheet.css">
  • A script so unimportant that we would like it not to block anything: not only parser and rendering, but even the document load event:
<script blocking="!render !parse !load" src="minor.js"></script>

The list of milestones to block or unblock may include:

  • render: the first rendering cycle
  • parse: parsing of subsequent DOM contents
  • domcontentloaded: the DOMContentLoaded event
  • load: the document load event

(We considered using blocking="none" to unblock everything. It does not seem to be a good idea due to forward-compatibility reasons.)

A side-product is that async/defer scripts may be easier to explain. Async script is equivalent to blocking="!parse", while defer script implies blocking="!parse documentcontentloaded" but still has some other implications.

We chose to propose render as the MVP because it has the most demanding use cases and keeps the initial proposal simple. With the other milestones, there are other complications that we need to consider. For example, the interaction between milestones, browser’s internal operations hooked to the load event, and etc.

Explicit timeout

We may want an additional attribute to set an explicit timeout for the blocking period, e.g., blockingtime="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 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.

Alternative criteria to stop render-blocking

For certain types of resources, we may want it to block rendering first, but unblock as soon as certain parts of the resource are available. For example, if we want to eliminate layout shifting for an image, we can unblock rendering when the metadata (which includes dimensions of the image) is available, or when a certain proportion of a progressive JPEG is loaded.

Prevent 3rd-party abuse

As 3rd-party scripts can set element attributes in general, they may abuse this new feature by marking too many resources render-blocking, and harm the page load experience. To avoid that, we may introduce a JavaScript API that cancels any explicit render-blocking added to an element via the blocking attribute.

document.unblockRendering(element);

We may also provide APIs for how much render-blocking time each resource contributed, so that developers can identify ill-performing 3rd-party scripts more easily.

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. a 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 blocking="render".


Footnotes

[1] The exact behaviors differ slightly.
[2] Current browsers have different behaviors, and this proposal would like to standardize it. See test case.
[3] An HTML parser will always insert a "body" HTML element at some point, according to the spec.
[4] We only block rendering on the synchronous parts of the script. We don’t need to block on asynchronous code like event listeners, Promise.then() and etc. This implies that in the case of a module script with top-level awaits, we’ll block rendering only up to the first await, because await is equivalent to a Promise.then().

This questionnaire has moved.

For your convenience, a copy of the questionnaire's questions is quoted here in Markdown, so you can easily include your answers in an explainer.

  1. What information might this feature expose to Web sites or other parties, and for what purposes is that exposure necessary?

None

  1. Do features in your specification expose the minimum amount of information necessary to enable their intended uses?

It does not expose any information.

  1. How do the features in your specification deal with personal information, personally-identifiable information (PII), or information derived from them?

It does not deal with such information.

  1. How do the features in your specification deal with sensitive information?

It does not deal with sensitive information.

  1. Do the features in your specification introduce new state for an origin that persists across browsing sessions?

No.

  1. Do the features in your specification expose information about the underlying platform to origins?

No.

  1. Does this specification allow an origin to send data to the underlying platform?

No.

  1. Do features in this specification enable access to device sensors?

No.

  1. Do features in this specification enable new script execution/loading mechanisms?

No.

  1. Do features in this specification allow an origin to access other devices?

No.

  1. Do features in this specification allow an origin some measure of control over a user agent's native UI?

No.

  1. What temporary identifiers do the features in this specification create or expose to the web?

None.

  1. How does this specification distinguish between behavior in first-party and third-party contexts?

No distinction. Third-party scripts can add blocking=render to any element to block rendering, and hence, block rendering for a long time (until UA's internal timeout) if abused. We do not think of this as a major issue, or at least not a new issue, but just the general and already existing risk of including 3rd-party scripts.

  1. How do the features in this specification work in the context of a browser’s Private Browsing or Incognito mode?

No difference. This is just a rendering feature.

  1. Does this specification have both "Security Considerations" and "Privacy Considerations" sections?

No.

  1. Do features in your specification enable origins to downgrade default security protections?

No.

  1. How does your feature handle non-"fully active" documents?

It does not have an effect on such documents.

  1. What should this questionnaire have asked?

None.

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