What to do when CSS blocks page parsing?

Recently, I conducted an audit of one site and came across a pattern preload/polyfillthat I have already seen with several clients. Today, the use of this previously popular pattern is not recommended. However, it is useful to consider it in order to illustrate the importance of careful use of the mechanism of preloading materials by web browsers. It is also interesting because it allows you to demonstrate a real example of how the order of elements in a document can affect performance (this is what is discussed in this wonderful Harry Roberts article). The material, the translation of which we publish today, is devoted to the analysis of situations in which improper and untimely handling of CSS resources affects the performance of web pages.





About loadCSS


I am a big fan of the Filament Group - they release an incredible amount of great projects. In addition, they constantly create invaluable tools and share them for the sake of improving the web. One of these tools is loadCSS , which for a long time was the very tool that I recommended everyone use to load non-critical CSS resources.

Although this has now changed (and the Filament Group has published an excellent article about what its employees prefer to use these days), I still, when I audit sites, often see how they loadCSSuse it in production.

One of the patterns that I have come across is the patternpreload/polyfill. Using this approach, any files with styles are loaded in preload mode (the attribute of the relcorresponding links is set to preload). After that, when they are ready to use, apply their events onloadto connect them to the page.

<link rel="preload" href="path/to/mystylesheet.css" as="style" onload="this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="path/to/mystylesheet.css"></noscript>

Since not all browsers support the design <a href="https://caniuse.com/#feat=link-rel-preload"><link rel="preload"></a>, the project loadCSSgives developers a convenient polyfill, which is added to the page after the description of the relevant links:

<link rel="preload" href="path/to/mystylesheet.css" as="style" onload="this.rel='stylesheet'">
<noscript>
    <link rel="stylesheet" href="path/to/mystylesheet.css">
</noscript>
<script>
/*! loadCSS rel=preload polyfill. [c]2017 Filament Group, Inc. MIT License */
(function(){ ... }());
</script>

Disorder in network priorities


I have never been an ardent fan of this pattern. Preloading is a rude tool. Materials downloaded using attribute links rel="preload"successfully fight with other materials for network resources. Usage preloadassumes that stylesheets that load asynchronously due to the fact that they do not play a critical role in the output of a page receive a very high priority from browsers.

The following image, taken from WebPageTest, demonstrates this problem very well. In lines 3-6, you can see asynchronous loading of CSS files using a pattern preload. But, while developers consider these files not so important that downloading them would block rendering, usepreload means that they will be loaded before the browser receives the remaining resources.


CSS files that use the preload pattern when loaded will arrive in the browser earlier than other resources, even though they are not resources that are critical to the initial rendering of a page

HTML parser lock


The problems associated with the priority of resource loading are already enough to avoid using the pattern in most situations preload. But in this case, the situation is aggravated by the presence of another stylesheet, which is loaded in the usual way.

<link rel="stylesheet" href="path/to/main.css" />
<link rel="preload" href="path/to/mystylesheet.css" as="style" onload="this.rel='stylesheet'">
<noscript>
    <link rel="stylesheet" href="path/to/mystylesheet.css">
</noscript>
<script>
/*! loadCSS rel=preload polyfill. [c]2017 Filament Group, Inc. MIT License */
(function(){ ... }());
</script>

There are the same problems when using preloadleads to the fact that not the most important files get high priority. But itโ€™s just as important, and perhaps less obvious, what effect this has on the browserโ€™s ability to parsing the page.

Again, this has already been written in detail , so I recommend reading that material in order to better understand what is happening. Here I will talk about this briefly.

Typically, loading styles blocks page rendering. The browser needs to query and parse styles in order to be able to display the page. However, this does not prevent the browser from parsing the rest of the HTML code.

Scripts, on the other hand, block the parser if they are not marked as deferor async.

Since the browser has to assume that the script may be manipulating either the content of the page itself or the styles applied to it, it needs to be careful about when this script is run. If the browser knows that some CSS code is loading, it will wait for the arrival of this CSS code, and after that it will run the script. And since the browser cannot continue parsing the document until the script is executed, this means that the styles no longer just block rendering. They prevent the browser from parsing HTML.

This behavior is true for both external scripts and scripts embedded in the page. If CSS loads, inline scripts do not run until this CSS arrives in the browser.

Problem study


The most understandable way to visualize this problem is to use the Chrome developer tools (I really like the level to which these tools have grown).

Among the Chrome tools there is a tab Performancewith which you can record the page load profile. I recommend artificially slowing down the network connection so that the problem manifests itself brighter.

In this case, I tested using the Fast 3G network setup. If you look closely at what happens to the main thread, you can understand that the request to load the CSS file occurs at the very beginning of HTML parsing (about 1.7 seconds after the page starts loading).


A small rectangle below the HTML parsing block represents a request to receive a CSS file.

During the next period of time, which is approximately a second, the main thread is inactive. Here you can see small islands of activity. This is a triggering of events indicating the completion of the loading of styles; it is the sending by the preloading mechanism of resources of other requests. But the browser completely stops parsing HTML.


If you look at the big picture, it turns out that after the start of loading CSS, the main thread is idle for more than 1.1 seconds.

So, 2.8 seconds have passed, the style is loaded, the browser processes it. Only then we see the processing of the built-in script, and after that the browser finally returns to HTML parsing.


CSS arrives in about 2.8 seconds, after which we see that the browser continues parsing HTML

Firefox is a nice exception


The above behavior is common for Chrome, Edge, and Safari. Firefox is a nice exception to the list of popular browsers.

All other browsers suspend HTML parsing, but use a proactive parser (a means of preloading materials) to view the code for links to external resources and to fulfill requests for loading these resources. Firefox, however, goes a step further in this matter: it speculatively builds a DOM tree, even though it expects the script to execute.

Unless the script manipulates the DOM, which leads to the need to discard the results of speculative parsing, this approach allows Firefox to take advantage. Of course, if the browser has to drop the speculatively constructed DOM tree, it means that he did nothing useful by building this tree.

This is an interesting approach. I was terribly curious to know how effective it is. Now, however, there is no information about this in the Firefox Performance Profiler. There you canโ€™t find out whether the speculative parser worked, whether it is necessary to redo the work done by it, and if it still needs to be redone, how this will affect performance.

I talked with those who are responsible for the Firefox developer tools, and I can say that they have interesting ideas on how to present such information in the profiler in the future. I hope they succeed.

Solution to the problem


In the case of the client, which I mentioned at the very beginning, the first step in solving this problem looked extremely simple: get rid of the pattern preload/polyfill. Preloading non-critical CSS is pointless. Here you need to switch to using, instead of rel="preload", an attribute media="print". This is exactly what the experts from the Filament Group recommend. This approach, in addition, allows you to completely get rid of polyfill.

<link rel="stylesheet" href="/path/to/my.css" media="print" onload="this.media='all'">

This already puts us in a better position: now network priorities are much better aligned with the real importance of the downloads. And we also get rid of the built-in script.

In this case, there is still another built-in script located in the document header, below the line that initiates the request to load CSS. If you move this script so that it is in front of the line loading the CSS, this will eliminate the parser lock. If you analyze the page again using the Chrome Developer Tools, the difference will be completely obvious.


Before making changes to the page code, the HTML parser stopped at line 1939, having encountered a built-in script, and stayed here for about a second. After optimization, he was able to get to line 5281

Previously, the parser stopped at line 1939 and waited for CSS to load, but now it reaches line 5281. There, at the end of the page, there is another built-in script that stops the parser again.

This is a quick solution to the problem. This is not the option that represents the final solution to the problem. Changing the order of the elements and getting rid of the patternpreload/polyfillis just the first step. You can best solve this problem by embedding critical CSS code in a page, rather than loading it from an external file. Patternpreload/polyfillDesigned to be used in addition to inline CSS. This allows us to completely ignore the problems associated with scripts and ensure that the browser, after executing the first request, has all the styles necessary for rendering the page.

But for now, it should be noted that we can achieve a good performance boost by making very small changes to the project regarding the way styles are loaded and the order of elements in the DOM.

Summary


  • If you use a loadCSSpattern preload/polyfill, go to the style loading pattern print.
  • If you have external styles that are loaded in the usual way (that is, using regular links to files of these styles), move all the built-in scripts that can be moved above the links to load styles.
  • Embed critical styles in the page in order to ensure the fastest possible start of rendering.

Dear readers! Have you encountered problems slowing down page rendering due to CSS?

PS
RUVDS congratulates all IT professionals on March 8!
This year, we decided not to give tulips and not to make a selection of geek gifts. We took a different path and created the IT is female page to show the presence of female specialists in IT.


All Articles