Progressive CSS rendering with or without data URLs

26 September 2020   0 comments   Web development, Web Performance, JavaScript

You can write your CSS so that it depends on images. Like this:

li.one {
  background-image: url("skull.png");
}

That means that the browser will do its best to style the li.one with what little it has from the CSS. Then, it'll ask the browser to go ahead and network download that skull.png URL.

But, another option is to embed the image as a data URL like this:

li.one{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHL...rkJggg==)

As a block of CSS, it's much larger but it's one less network call. What if you know that skull.png will be needed? Is it faster to inline it or to leave it as a URL? Let's see!

First of all, I wanted to get a feeling for how much larger an image is in bytes if you transform them to data URLs. Check out this script's output:

▶ ./bin/b64datauri.js src/*.png src/*.svg
src/lizard.png       43,551     58,090     1.3x
src/skull.png        7,870      10,518     1.3x
src/clippy.svg       483        670        1.4x
src/curve.svg        387        542        1.4x
src/dino.svg         909        1,238      1.4x
src/sprite.svg       10,330     13,802     1.3x
src/survey.svg       2,069      2,786      1.3x

Basically, as a blob of data URL, the images become about 1.3x larger. Hopefully, with HTTP2, the headers are cheap for each URL downloaded over the network, but it's not 0. (No idea what the CPU-work multiplier is)

Experiment assumptions and notes

It's a fairly commonly known fact that data URLs have a CPU cost. That base64 needs to be decoded before the image can be decoded by the renderer. So let's stick to fairly small images.

The experiment

I made a page that looks like this:

li {
  background-repeat: no-repeat;
  width: 150px;
  height: 150px;
  margin: 20px;
  background-size: contain;
}
li.one {
  background-image: url("skull.png");
}
li.two {
  background-image: url("dino.svg");
}
li.three {
  background-image: url("clippy.svg");
}
li.four {
  background-image: url("sprite.svg");
}
li.five {
  background-image: url("survey.svg");
}
li.six {
  background-image: url("curve.svg");
}

and

<ol>
  <li class="one">One</li>
  <li class="two">Two</li>
  <li class="three">Three</li>
  <li class="four">Four</li>
  <li class="five">Five</li>
  <li class="six">Six</li>
</ol>

See the whole page here

The page also uses Bootstrap to make it somewhat realistic. Then, using minimalcss combine the external CSS with the CSS inline and produce a page that is just HTML + 1 <style> tag.

Now, based on that page, the variant is that each url($URL) in the CSS gets converted to url(data:mime/type;base64,blablabla...). The HTML is gzipped (and brotli compressed) and put behind a CDN. The URLs are:

Also, there's this page which is without the critical CSS inlined.

To appreciate what this means in terms of size on the HTML, let's compare:

Considering that gzip (accept-encoding: gzip,deflate) is almost always used by browsers, that means the page is 15KB more before it can be fully downloaded. (But, it's streamed so maybe the comparison is a bit flawed)

Analysis

WebPagetest.org results here. I love WebPagetest, but the results are usually a bit erratic to be a good enough for comparing. Maybe if you could do the visual comparison repeated times, but I don't think you can.

WebPagetest comparison
WebPagetest visual comparison

And the waterfalls...

WebPagetest waterfall, with regular URLs
With regular URLs

WebPagetest waterfall with data URLs
With data URLs

Fairly expected.

Next up, using Google Chrome's Performance dev tools panel. Set to 6x CPU slowdown and online with Fast 3G.

I don't know how to demonstrate this other than screenshots:

Performance with external images
Performance with external images

Performance with data URLs
Performance with data URLs

Those screenshots are rough attempts at showing the area when it starts to display the images.

Whole Performance tab with external images
Whole Performance tab with external images

Whole Performance tab with data URLs
Whole Performance tab with data URLs

I ran these things 2 times and the results were pretty steady.

I tried Lighthouse but the difference was indistinguishable.

Summary

Yes, inlining your CSS images is faster. But it's with a slim margin and the disadvantages aren't negligible.

This technique costs more CPU because there's a lot more base64 decoding to be done, and what if you have a big fat JavaScript bundle in there that wants a piece of the CPU? So ask yourself, how valuable is to not hog the CPU. Perhaps someone who understands the browser engines better can tell if the base64 decoding cost is spread nicely onto multiple CPUs or if it would stand in the way of the main thread.

What about anti-progressive rendering

When Facebook redesigned www.facebook.com in mid-2020 one of their conscious decisions was to inline the SVG glyphs into the JavaScript itself.

"To prevent flickering as icons come in after the rest of the content, we inline SVGs into the HTML using React rather than passing SVG files to <img> tags."

Although that comment was about SVGs in the DOM, from a JavaScript perspective, the point is nevertheless relevant to my experiment. If you look closely, at the screenshots above (or you open the URL yourself and hit reload with HTTP caching disabled) the net effect is that the late-loading images do cause a bit of "flicker". It's not flickering as in "now it's here", "now it's gone", "now it's back again". But it's flickering in that things are happening with progressive rendering. Your eyes might get tired and they say to your brain "Wake me up when the whole thing is finished. I can wait."

This topic quickly escalates into perceived performance which is a stratosphere of its own. And personally, I can only estimate and try to speak about my gut reactions.

In conclusion, there are advantages to using data URIs over external images in CSS. But please, first make sure you don't convert the image URLs in a big bloated .css file to data URLs if you're not sure they'll all be needed in the DOM.

Bonus!

If you're not convinced of the power of inlining the critical CSS, check out this WebPagetest run that includes the image where it references the whole bootstrap.min.css as before doing any other optimizations.

With baseline that isn't just the critical CSS
With baseline that isn't just the critical CSS

Comments

Your email will never ever be published

Related posts