Intentionally blocking rendering with JavaScript
You nearly always want to put <script> tags in the <head> and mark them as non-blocking using either async or defer. However, there’s an interesting use-case for actually wanting to block paint.
Imagine you have a UI component which relies on client-side JavaScript in order to render the ‘correct’ layout or markup (hear me out until the end, container queries won’t always cut it).
You really only have three choices:
- Accept a ‘flash’ of the unstyled component, before the JS loads and the styles can take effect.
- Hide the component until the script reveals it (which may also cause layout shift, depending on what you’re trying to accomplish).
- Use render-blocking JS to try and ‘beat paint’ and get everything in place before the user sees anything amiss.
Traditional inline scripts (and external scripts without async or defer) are parser-blocking. This means that the HTML parser is blocked from reading everything below until the script has been parsed and evaluated, and so nothing below can be painted.
However, the browser may still choose to paint the chunks it has received up until that point. There’s no guarantee that even if you do something like this, you’ll avoid the ‘flash’:
<my-component>Needs to measure layout</my-component>
<script>
class MyComponent extends HTMLElement {
// ...
}
customElements.define("my-component", MyComponent);
</script>HTML
If you move the blocking script to be above the component, then you won’t be able to read the children (which haven’t been parsed yet):
<script>
class MyComponent extends HTMLElement {
constructor() {
super();
const inner = this.querySelector('.inner');
// Too early.
console.log('constructor', { inner });
}
connectedCallback() {
const inner = this.querySelector('.inner');
// Still too early, as the element is connected before the children
// have been parsed and attached to the DOM.
console.log('connectedCallback', { inner });
}
}
customElements.define("my-component", MyComponent);
</script>
<my-component>
<span class="inner">Thing</span>
</my-component>HTML
The version below would work, since we delay registration of the component until after the parser has finished:
<script>
class MyComponent extends HTMLElement {
constructor() {
super();
const inner = this.querySelector('.inner');
// Works.
console.log('constructor', { inner });
}
connectedCallback() {
const inner = this.querySelector('.inner');
// Also works.
console.log('connectedCallback', { inner });
}
}
</script>
<my-component>
<span class="inner">Thing</span>
</my-component>
<script>
customElements.define("my-component", MyComponent);
</script>HTML
But you still can’t guarantee that my-component will never be rendered in its default state.
blocking="render"
There’s a neat, fairly well supported attribute for script tags that does exactly what we want: blocking="render".
Allows putting ‘blocking=render’ as an attribute and value to a
<script>,<style>or stylesheet<link>to make it explicitly render-blocking. The main usage is to avoid a flash of unstyled content or user interactions with an unmature page caused by, e.g., script-inserted scripts/stylesheets, client-side A/B testing and etc.
Now we can write this and be absolutely sure that nothing will be painted until the script has loaded:
<my-component>
<span class="inner">Thing</span>
</my-component>
<script blocking="render">
class MyComponent extends HTMLElement {
// ...
}
customElements.define("my-component", MyComponent);
</script>HTML
It also works with type="module" (no need to pollute the global namespace):
<script type="module" blocking="render">
class MyComponent extends HTMLElement {
// ...
}
customElements.define("my-component", MyComponent);
</script>HTML
And external scripts:
<script type="module" blocking="render" src="/js/my-component.js"></script>HTML
The neat thing is that the parser is still free to continue, even if rendering is blocked, making this a significant step-up over traditional blocking scripts.
But why
Blocking rendering or parsing is rarely the right option. It’s nearly always better to show something as fast as possible and progressively enhance it later.
That said, there are valid use-cases. A/B testing is one cited by the Chrome team and Harry Roberts, where an external script might show drastically different variations of a page.
However, it’s also perfect for small components which unavoidably depend on layout to make sense.
A practical example
Here’s a real one: the priority-plus navigation pattern.
The most important navigation elements remain visible, while the remaining items continually ‘shift’ to an offscreen popover/modal/drawer.
Given a variable number of navigation elements of unknown size, it’s not possible to know how many will ‘fit’ until they’re actually on the page. Since overflowing navigation elements need to change where they’re rendered in the DOM (for a11y as well as behaviour/styling), it’s not something you can solve with CSS-hackery alone.
This is the perfect use-case for an inline, blocking script where:
- The navigation pre-exists in the DOM as a single list.
- The render-blocking script measures the space and moves the overflowing navigation elements into the overflow.
- Rendering is unblocked.
No flash, and no (visible) layout shift.
Conclusion
Sometimes an inline render-blocking script is a small price to pay for avoiding aggressive layout shifts.
It does need to be small, though, and ought to be inline. The second you introduce a network call you’re adding a significant overhead.