People usually don’t think about CSS as one of the main network performance bottlenecks. In this article, I’d like to share with you some tips I have learned how to significantly improve your page’s performance.
As you probably know, a browser can’t render the page if the Render Tree is not ready. And the Render Tree is the combination of DOM and CSSOM, hence anything that is blocking DOM or CSSOM construction – also blocks the page rendering.
Scripts can block DOM construction, but it’s easy to make them non-blocking with “async” and “defer” attributes. While it’s much more difficult to make your CSS asynchronous. As a result – your page will only render as quickly as your slowest stylesheet.
A good option would be to implement “Critical CSS” pattern: you define a minimum set of styles needed to render the first screen (everything above the fold) to a user and then asynchronously load all other non-critical styles once the page is rendered. All critical styles you inline into style tag in the head, thus eliminating extra network request.
<head> <style> /* inlined critical CSS */ </style> </head> <body> ...content goes here <script> loadNonCriticalCSS(); </script> </body> </html>
It’s an effective yet difficult strategy, especially if your website is highly dynamic or if you’re working with large or legacy codebase. It can be really hard to build and maintain styles when you use this approach.
Another possible optimization would be to split your huge CSS bundle into smaller chunks based on the screen size, so that a browser downloads styles for the current screen size with the highest priority, blocking Render Tree construction. Any CSS not needed for the current context will be downloaded with a low priority, not blocking the first render.
Let’s take a look at an example: here we have a single CSS bundle with all styles. A browser can’t render the page until it fully loads the bundle, parses it and builds CSS Object Model.
<link rel="stylesheet" href="all.css" />
Now, if we break it down to smaller CSS chunks, needed for specific screen sizes, we can declare media expressions using "media" attribute – and a browser will download only "base.css" plus what is necessary, depending on the current screen size ("large.css" in our example). Look at the "Priority" column in Chrome DevTools: everything that is not needed for the current screen size – loaded with the lowest priority and don’t block our Critical Path.
<link rel="stylesheet" href="base.css" media="all" /> <link rel="stylesheet" href="small.css" media="(max-width: 703px)" /> <link rel="stylesheet" href="medium.css" media="(min-width: 704px) and (max-width: 1031px)" /> <link rel="stylesheet" href="large.css" media="(min-width: 1032px)" /> <link rel="stylesheet" href="print.css" media="print" />
Avoid using CSS @import as it is really, really bad for performance. Because we can’t load all imported stylesheets in parallel with the main CSS file.
Look at these steps a browser has to do when you use @import:
<link rel="stylesheet" href="styles.css" />
Here’s where we’d like to be able to construct the Render Tree, but...
Now look at the waterfall, pay attention to the lack of parallelization:
If you simply add your imported stylesheet as a separate link tag...
<link rel="stylesheet" href="styles.css" /> <link rel="stylesheet" href="imported.css" />
...a browser can load them both in parallel, significantly improving performance:
Any synchronous script’s execution is blocked while a CSS file is being downloaded and processed. This is done on purpose in case if the script requests the page’s styles. And for that, a browser needs to construct CSSOM.
<link rel="stylesheet" href="slow-styles.css" /> <script> console.log(` I will not run until slow-styles.css is downloaded.` ); </script>
A browser will not execute script if there is any currently in-flight CSS.
If your stylesheet is slow – all scripts are not running and it has a dramatical impact on the page performance. Let’s take a look at an example:
<link rel="stylesheet" href="app.css" /> <script> var script = document.createElement('script'); script.src = "analytics.js"; document.getElementsByTagName('head').appendChild(script); </script>
We’ve completely lost any parallelization:
Now let’s try to swap stylesheet and script and look at the result:
<script> var script = document.createElement('script'); script.src = "analytics.js"; document.getElementsByTagName('head').appendChild(script); </script> <link rel="stylesheet" href="app.css" />
So we can definitely say: if your script has no dependency on CSS, place it above your stylesheets. It can make a huge impact on performance, depending on the page complexity.
However, in the latest Chrome (v75), I couldn’t reproduce this behavior: the CSS stylesheet is loaded always first, regardless of whether it goes after the script or before. In Safari (v12), on the contrary, I could reproduce and, as you see on screenshots, the order really matters for performance.
For SPA when we create a single giant stylesheet bundle, we encounter the following problems:
With HTTP 2 and a proper build configuration we can fix the issues with cache and loading too much CSS, splitting the bundle into smaller chunks, preferably a stylesheet per each component:
<head> <link rel="stylesheet" href="core.css" /> <link rel="stylesheet" href="nav.css" /> <link rel="stylesheet" href="article.css" /> <link rel="stylesheet" href="ads.css" /> <link rel="stylesheet" href="footer.css" /> </head>
Now we load only styles needed for the components on the page, plus caching becomes very effective. But still, we block the page rendering with our stylesheet requests :(
But here’s a trick: link rel="stylesheet" only blocks the rendering of a subsequent content, rather than the whole page (works in IE/Edge, Firefox and Chrome since v69). It means we can build our page like this:
<head> <link rel="stylesheet" href="core.css" /> </head> <body> <link rel="stylesheet" href="header.css" /> <header class="header"> <link rel="stylesheet" href="nav.css" /> <nav class="nav">...</nav> </header> <link rel="stylesheet" href="article.css" /> <main class="article">...</main> <link rel="stylesheet" href="footer.css" /> <footer class="footer">...</footer> </body>
The progressive rendering looks very effective and promising. I’m surprised it’s not widely known or used. If you decide to implement it in your website, please give me a notice, I’d like to look at it and hear from you about performance improvement.