It's important to understand that [...] is a gradual process. For better user experience, the rendering engine will try to display contents on the screen as soon as possible. It will not wait until all HTML is parsed before starting to build and layout the render tree. Parts of the content will be parsed and displayed, while the process continues with the rest of the contents that keeps coming from the network.
— Tali Garsiel, How browsers work
Let's demonstrate that fact by loading a page with lots of text content. Page download is throttled by the server and takes a long time.
Try to scroll down while the page is being downloaded.
The display keeps updating, but DOMContentLoaded event won't trigger until the download and parsing of the HTML is completed.
π External styles block rendering, but not document parsing
As we have seen in the previous example, the DOM content tree is gradually builded as the HTML is downloaded. What if we have an stylesheet link on that page?
An external stylesheet by itself does not block all of the process. The fetching of the stylesheet starts, but the HTML parsing continues. The DOMContentLoaded event will trigger regardless the style state.
However, although the DOMContentLoaded event fires, the page won't be displayed to the user until the fetching of the style is done.
This means an stylesheet resource doesn't block the document parsing, but will block the rendering. From JavaScript land It feels strange to have no visual feedback when DOMContentLoaded has already fired. There seems to be no way to force the browser into the first paint.
If an style is added dynamically from JavaScript, It won't block the rendering. Even if that is done before the first render.
While there are pending styles to download and parse, the window `load` will not fire. No matter how late they have been added to the DOM.
Syles imported with @import statements from the external styles are considered part of the same loading process.
Timeline: It shows that an external style link does not block the document parsing nor the DOMContentLoaded event. However the first paint won't happen until the download and processing of the style resource ends.
Quirks
Blink; When the link tag is located in the head, then as claimed the document parsing is not blocked. However if the link tag is located in the body, It blocks the document parsing (the hard way). Test it in this secondary example. Gecko and WebKit based browsers don't observe this difference between body and head.
We can achieve the gradual rendering effect seen on the first example while displaying already styled content. It is possible by inlining the contents of the stylesheet.
Although inlining styles is a limited option (prevents caching, is difficult to scale),
for the shake of the example It is cool to see how the browser can build the DOM and render styled contents as the HTML is dowloaded.
The best experience comes when only critical styles are inlined, as we get the gradual rendering effect of styled content.
However It forces the loading of the non-critical styles on a deferred stage. The media attribute trick is a well known option for the deferred load. Although It feels hackish and relies on JavaScript.
Another possiblity would be to put the non-critical styles in a link rel="preload" tag. That link wouldn't block document parsing nor rendering. Finally place the regular link to the same style at the very end of the body. Rendering can't be blocked if something has been already painted.
This would be a sweet spot solution, except for the quirk commented on the external styles example. Blink blocks the document parsing when it finds a style link in the body. Thus although the visual exeperience is good, the delay of DOMContentLoaded makes this solution non-generalizable.
On a more general note, be aware of abusing preloading. On HTTP/1.1 you can easily hit the number of concurrent connections to the same domain (6 in Chrome). Aside from that, remember that you are hand-picking high priority resources. That implicitly de-prioritizes other resources.
Timeline: It shows an early paint. Contents are displayed as they arrive, styled with the critical inlined styles. Meanwhile non-critical styles are being preloaded and won't block anything. Once the non-critical styles are downloaded the styles are applied (reflow). This sequence of events is taken from a WebKit based browser.
π Scripts block document parsing, but not rendering
When an script is found in the HTML, the browser pauses the document parsing until the script is executed.
If the script is external, It must be fetched and then executed. Document parsing won't continue until the script ends execution.
Givent that an script does not necessarily block the rendering, the location of the script in the document is important. If the script is located at the end of the body, some rendering is possible
Timeline: It shows that an external script blocks document parsing. Interactive state won't be reached until the script is fetched and executed. However if enough document content has been already parsed, paint still happens.
π Scripts execution requires to finish the ongoing styles loading
As we have seen previously, and external stylesheet will not block the document parsing. However if the external style is followed by an script, things change.
An script not only pauses the document parsing, but also requires to finish the ongoing styles loading. Only then will start its execution.
So document parsing becomes blocked when it gets to an script. In turn the script execution is blocked by the loading styles.
Note we emphasize the fact that only ongoing loading styles will block the script. If the order is inverted and the script is placed before the style, It will get executed immediately. See that in this secondary example (timeline will be similar to the second case).
This rule only applies to initial styles. That is to stylesheet links that are in the initial HTML response. Stylesheet links added dynamically do not block successive scripts, even if they are added to the document before those scripts.
When the initial stylesheets contain @import statements, those are considered part of the same style loading. Thus importing styles from styles is a serious concern, as it pospones script execution and rendering. Check that in this other secondary example.
Timeline: The external style by itself would not block the document parsing, as seen previously. However the script gets blocked by the style loading, and in turn the script blocks the document parsing. Thus DOM interactive is bounded by the style loading plus the script execution.
Even though deferred scripts don't block document parsing and get downloaded in parallel (with low priority), they share some key behaviours with the "normal" synchronous script tags:
They are executed in the order they were added to the DOM tree.
Before execution they must wait for ongoing styles loading (if any) to end.
They are all executed at the same time, before DOMContentLoaded. Therefore all deferred scripts need to be completely downloaded before any of them can execute.
Roughtly we can think of deferred scripts as synchronous scripts whose execution is pushed towards the end of the document parsing process, with the benefit of prefetching them.
As a side note, scripts of type=module behave like deferred scripts. They keep order, execute once the document is interative, wait for styles, etc. The most notable difference is that they probably get higher download priority. Also they won't have access to document.currentScript.
Timeline: It shows two deferred scripts that start to download as the document parses. The document parsing ends and It reaches the interactive state. The first deferred script takes 5s to download and the second takes 2s. They need to wait for the style download to end. Then both get executed in order. Finally DOMContentLoaded is fired. The rendering is hold by the style download, not the scripts. Without the external style, rendering would take place almost immediately.
Timeline: It shows that four external resources start loading (one style, three async scripts) while the document is being received and parsed. The 2nd async script is the first to end its download. At this point none of the other external resources, nor the document itself have been completely received yet. Execution of this 2nd async script starts and takes a while — around 2seconds, a very looong task! While this script is executing, three other resources have finished arriving (the style, the document itself, and the 3rd async script). They can't be processed while the 2nd script is executing. When the execution of the 2nd script ends, we are unblocked to continue processing stuff. The style is processed. The rest of the document is parsed and reaches the interactive state. And the 3rd async script gets executed. Finally some rendering happens. Much later, the 1st async script ends its download and executes. Only then the load event fires.
We consider dynamic scripts those that are created —and added to the DOM— using JavaScript. This can be done at any time, there is no restriction.
There are two types of dynamic scripts: async (the default), or sync. There is no straight way to create a dynamic script that acts as defer.
The insertion of a dynamic script does not block document parsing. Even if they are created in sync mode and are added before the document is interactive state.
Dynamic sync scripts block each other. They execute in the order they were added to document (the insertion position is irrelevant).
Dynamic async scripts behave exactly as non-dynamic async scripts: they almost block nothing, and get executed as soon as they are ready.
In fact any dynamic script may execute in the middle of critical processes like document parsing. It is important to keep your eyes open.
Finally, they do block the window load event. This is true for every dynamic script (async or sync), disregarding when they are added to the document. If they are added before load has fired, the event will be holded until the pending scripts execute.
Timeline: Shows that all async scripts (initials or dynamically added) block nothing and get executed when they are ready. Deferred scripts hold the DOMContentLoaded event, and must wait other deferred scripts to be downloaded before executing. Dynamic sync scripts are executed in the order they where added to the document; in the example the dyn-sync-1 script takes a long time and holds the execution of dyn-sync-2. Finally domComplete won't happen until all pending scripts execute, disregarding how late they where added. In the example dyn-async-2 would have been the last resource, but we added a new script dyn-async-3 in its onload callback, thus posposing the load event.
It is possible to overcome that limitation by mocking the inlined script as an external async script, using the src attribute to contain the code as a data URL. The downside is that the code must be URL encoded. It is not easy to manage.
Note that this method would not be useful if we use defer, given that deferred scripts also await for the ongoing styles to finish loading.
Timeline: An style starts to load very early. It blocks the load event, the rendering, and would block inline scripts too. The inline script of the example mocks an external async script behaviour, thus It can execute immediately without waiting for the style to load. Compare it with the regular inline script timeline.