Black Sheep Code
rss_feed

The behaviour of browsers and loading resources - HTML, images, and CSS

Published:
lightbulb

The code examples in this post are hosted in this repository.

Each code example contains a link the specific specific example, so you can run these yourself.

I run this code using the Live Sever VSCode plugin.

While accessing the application with your web browser - open your dev tools, navigate to the network tab, click 'disable cache' and set throttling to 3G. This allows us to see with more detail what's happening with browsers network requests.

HTML

Let's take an application that looks like this:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <style>
        // Just some styling so we can see the blocks individually
        .dom-50kB {
            width: 200px;
            height: 200px; 
            overflow: hidden;
            display: inline-block;
            margin: 1em;
            border: dashed 1px black;
        }
    </style>
</head>
<body id="body">
    <div class="dom-50kB">
        // 50kB of dom elements here
    </div>

    <div class="dom-50kB">
        // 50kB of dom elements here
    </div>

    <div class="dom-50kB">
        // 50kB of dom elements here
    </div>
</body>
</html>
Code Example: 1.1-html

Just plain HTML, and a little bit of CSS for purpose of demonstration.

Note that we see the individual blocks pop in one at a time - as the network response streams in.

This is important, and useful. HTML documents parse as they stream - we don't need to wait for the entire document to have loaded before we display it.

Images

Now, let's add some images to our application:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <style>
        .dom-50kB {
            width: 200px;
            height: 200px; 
            overflow: hidden;
            display: inline-block;
            margin: 1em;
            border: dashed 1px black;
        }
    </style>
</head>
<body id="body">
    <img src="https://place-hold.it/300?image-1"/>

    <div class="dom-50kB">
        // 50kB of dom elements here
    </div>
    
    <img src="https://place-hold.it/300?image-2"/>

    <div class="dom-50kB">
        // 50kB of dom elements here
    </div>

    <img src="https://place-hold.it/300?image-3"/>

    <div class="dom-50kB">
        // 50kB of dom elements here
    </div>

    <img src="https://place-hold.it/300?image-4"/>

</body>
</html>
Code Example: 2.1-images-only

We get similar behaviour, but observe the images start streaming as soon the DOM element has been encountered.

HTTP/1 Simulatenous request limits

What happens if we have something like this?

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>

</head>
<body id="body">
    <img src="https://place-hold.it/300?image-1"/>
    <img src="https://place-hold.it/300?image-2"/>
    <img src="https://place-hold.it/300?image-3"/>
    <img src="https://place-hold.it/300?image-4"/>
    <img src="https://place-hold.it/300?image-5"/>
    <img src="https://place-hold.it/300?image-6"/>
    <img src="https://place-hold.it/300?image-7"/>
    <img src="https://place-hold.it/300?image-8"/>
    <img src="https://place-hold.it/300?image-9"/>
    <img src="https://place-hold.it/300?image-10"/>
    <img src="https://place-hold.it/300?image-11"/>
    <img src="https://place-hold.it/300?image-12"/>
    <img src="https://place-hold.it/300?image-13"/>
    <img src="https://place-hold.it/300?image-14"/>
    <img src="https://place-hold.it/300?image-15"/>
    <img src="https://place-hold.it/300?image-16"/>
    <img src="https://place-hold.it/300?image-17"/>
    <img src="https://place-hold.it/300?image-18"/>
    <img src="https://place-hold.it/300?image-19"/>

</body>
</html>
Code Example: 2.2-lots-of-images

Our waterfall now looks like this:

A dev tools loading waterfall demonstrating a lot of stalled time for multiple images

The later images now have a good amount of stalled time.

This is because Live Server is using the HTTP/1 protocol - which limits the number of simultaneous connections per server at at time. See this Stack Overflow answer for a summary.

Also, see this discussion for the difference between 'stalled' and 'queuing'

Lazy loading images

It's nice that the loading of images won't otherwise block the rendering of the page, but we're still unnecessarily fetching data that the user might not need, if they don't scroll down the page.

We can load the images only when the the image appears in the viewport by using the loading="lazy" attribute.

Summarising where we are at

Browsers are sensibly designed. They'll display HTML and images as they receive it.

There is a bottleneck with http/1.1 - it's browser specific but for Chrome it's six connections per server simultaneously.

CSS

Let's add some CSS to the head of our document:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <style>
        .dom-50kB {
            width: 200px;
            height: 200px; 
            overflow: hidden;
            display: inline-block;
            margin: 1em;
            border: dashed 1px black;
        }

        // 👇
        // 50 kB of CSS here
    </style>
</head>
<body id="body">
    <div class="dom-50kB">
        // 50kB of dom elements here
    </div>

    <div class="dom-50kB">
        // 50kB of dom elements here
    </div>

    <div class="dom-50kB">
        // 50kB of dom elements here
    </div>
</body>
</html>
Code Example: 3.1-css-inline-in-head

Here the application behaves as we expect it might - the content streams in and because the CSS appears first, when the HTML content appears it is already styled.

If, on the other hand, we were to move that CSS to the bottom of the document:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <style>
        .dom-50kB {
            width: 200px;
            height: 200px; 
            overflow: hidden;
            display: inline-block;
            margin: 1em;
            border: dashed 1px black;
        }

    </style>
</head>
<body id="body">
    <div class="dom-50kB">
        // 50kB of dom elements here
    </div>

    <div class="dom-50kB">
        // 50kB of dom elements here
    </div>

    <div class="dom-50kB">
        // 50kB of dom elements here
    </div>

    <style>
        // 👇
        // 50 kB of CSS here
    </style>
</body>
</html>
Code Example: 3.2-css-inline-at-bottom

Then what we'll see the content stream in, initially unstyled, and when it reaches the CSS then the style pops in. This behaviour is known as a "flash of unstyled content" (FOUC).

Let's access the CSS via a <link> tag.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <style>
        .dom-50kB {
            width: 200px;
            height: 200px; 
            overflow: hidden;
            display: inline-block;
            margin: 1em;
            border: dashed 1px black;
        }

    </style>
    
    <!-- 👇 50kB CSS -->
    <link href="./index.css" rel="stylesheet"/> 
</head>
<body id="body">
    <div class="dom-50kB">
        // 50kB of dom elements here
    </div>

    <div class="dom-50kB">
        // 50kB of dom elements here
    </div>

    <div class="dom-50kB">
        // 50kB of dom elements here
    </div>

</body>
</html>
Code Example: 3.3-css-link

We might think that the <link> tag will behave similar to our images - it'll load in parallel, the HTML will continue to stream in, and when it's complete the styles will apply.

This is the not the case! - They do indeed load in parallel, but the stylesheets are render blocking.

See my Stack Overflow question here and this related question.

The reason that stylesheets are render blocking - is to prevent these FOUCs.

Note that the HTML document will continue to stream, it just won't be displayed until the stylesheet is loaded.

Note depending on the size of our document and the size of our CSS, that we've potentially worsened the loading performance of our application - the extra 'waiting for server response' means that the content could take longer to display than if we'd inlined the styles into the document head.

So why don't we do this?

Caching. By having two separate files, each can be cached independently. Chances are, the user has already downloaded the CSS, because they've visited the website before, or because they've just visited a different page. The HTML is likely to change more often than the CSS, and we don't want to invalidate any CSS caching every time the HTML changes.

Render blocking, not parse blocking

If we take the previous example, and add some images to start of our document

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <style>
        .dom-50kB {
            width: 200px;
            height: 200px; 
            overflow: hidden;
            display: inline-block;
            margin: 1em;
            border: dashed 1px black;
        }

    </style>
    
    <!-- 👇 50kB CSS -->
    <link href="./index.css" rel="stylesheet"/> 
</head>
<body id="body">
    <!-- 👇 images -->
    <img src="https://place-hold.it/300?image-1"/>
    <img src="https://place-hold.it/300?image-2"/>

    <div class="dom-50kB">
        // 50kB of dom elements here
    </div>

    <div class="dom-50kB">
        // 50kB of dom elements here
    </div>

    <div class="dom-50kB">
        // 50kB of dom elements here
    </div>

</body>
</html>
Code Example: 3.3-css-link

Note that although nothing will be displayed until the stylesheet has completed loading, the image will still start downloading immediately.

This is because the browser continues parsing the the document, it just doesn't render the document.

This example highlights the difference between three concepts:

This MDN documentation is a helpful overview.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <style>
        .dom-50kB {
            width: 200px;
            height: 200px; 
            overflow: hidden;
            display: inline-block;
            margin: 1em;
            border: dashed 1px black;
        }
    </style>
    
</head>
<body id="body">
    <div class="dom-50kB">
        // 50kB of dom elements here
    </div>

    <div class="dom-50kB">
        // 50kB of dom elements here
    </div>

    <div class="dom-50kB">
        // 50kB of dom elements here
    </div>

    <!-- 👇 50kB CSS -->
    <link href="./index.css" rel="stylesheet"/> 

    <div class="dom-50kB">
        // 50kB of dom elements here
    </div>

    <div class="dom-50kB">
        // 50kB of dom elements here
    </div>

    <div class="dom-50kB">
        // 50kB of dom elements here
    </div>

</body>
</html>
Code Example: 3.4-css-link-in-middle

In this example we put the <link> tag in the middle of the document to demonstrate that the HTML will stream in up until it reaches the link tag, and which point the rendering will be blocked until the stylesheet is loaded.

So if we don't care about FOUCs and we want to prioritise showing the the user the content as soon as possible, we can put our <link> tags at the end of the document.

The problem with this approach is that it's not until the end of the document that the stylesheet start loading, meaning that the use will be delayed in seeing the final result of the page.

The above problem can be mitigated by preloading our stylesheet.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <!-- 👇 preload the stylesheet -->
    <link href="./index.css" rel="preload" as="style"/> 

    <style>
        .dom-50kB {
            width: 200px;
            height: 200px; 
            overflow: hidden;
            display: inline-block;
            margin: 1em;
            border: dashed 1px black;
        }
    </style>    

</head>
<body id="body">
    <div class="dom-50kB">
        // 50kB of dom elements here
    </div>

    <div class="dom-50kB">
        // 50kB of dom elements here
    </div>

    <div class="dom-50kB">
        // 50kB of dom elements here
    </div>

    <!-- 👇 50kB CSS -->
    <link href="./index.css" rel="stylesheet"/> 

    <div class="dom-50kB">
        // 50kB of dom elements here
    </div>

    <div class="dom-50kB">
        // 50kB of dom elements here
    </div>

    <div class="dom-50kB">
        // 50kB of dom elements here
    </div>

</body>
</html>
Code Example: 3.5-css-link-in-middle-with-preload

The resource will start loading as soon as the <link rel="preload"/> tag is encountered - but won't be render blocking until the <link rel="stylesheet"> tag is encountered.

Summarising



Questions? Comments? Criticisms? Get in the comments! 👇

Spotted an error? Edit this page with Github