Skip to content

Commit b1e81ba

Browse files
committed
Add Base64 articles
1 parent 21044ec commit b1e81ba

5 files changed

Lines changed: 598 additions & 3 deletions

File tree

_config.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ description: "Write an awesome description for your new site here. You can edit
55
baseurl: ""
66
url: "https://csswizardry.com"
77
future: true
8-
sw: "0013"
8+
sw: "0015"
99

1010
# Build settings
1111
markdown: kramdown
Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
---
2+
layout: post
3+
title: "Base64 Encoding & Performance: Part 2"
4+
date: 2017-02-12 15:47:21
5+
categories: Web Development
6+
meta: "Statistics, tests, and numbers looking at the performance costs of Base64"
7+
---
8+
9+
**This is the second in a two-part post. [Read Part
10+
1](/2017/02/base64-encoding-and-performance/).**
11+
12+
Hopefully you made it here after reading [part
13+
1](/2017/02/base64-encoding-and-performance/) of this post. If not, I’d
14+
encourage you to start there for some context.
15+
16+
- - -
17+
18+
After writing a somewhat insistent piece on the pitfalls of using Base64
19+
encoding to inline assets (predominantly images) into stylesheets, I decided to
20+
actually gather some data. I set about a simple test in which I would measure
21+
some milestone and runtime timings across ‘traditionally’ loaded assets, and
22+
again over assets who’ve been inlined using Base64.
23+
24+
## The Test, and Making It Fair
25+
26+
I started out by creating two simple HTML files that have a full-cover
27+
background image. The first was loaded normally, the second with Base64:
28+
29+
* [Normal](http://csswizardry.net/demos/base64/)
30+
* [Base64](http://csswizardry.net/demos/base64/base64.html)
31+
32+
The [source image](https://www.flickr.com/photos/rockersdelight/26842162186/)
33+
was taken by my friend [Ashley](https://twitter.com/iamashley). I resized it
34+
down to 1440×900px, saved it out as a progressive JPEG, ran it through JPEGMini
35+
and ImageOptim, and only then did I take a Base64 encoded version:
36+
37+
```
38+
harryroberts in ~/Sites/csswizardry.net/demos/base64 on (gh-pages)
39+
» base64 -i masthead.jpg -o masthead.txt
40+
```
41+
42+
This was so that the image was appropriately optimised, and that the Base64
43+
version was as similar as we can possibly get.
44+
45+
I then created two stylesheets:
46+
47+
```
48+
* {
49+
margin: 0;
50+
padding: 0;
51+
box-sizing: border-box;
52+
}
53+
54+
.masthead {
55+
height: 100vh;
56+
background-image: url("[masthead.jpg|<data URI>]");
57+
background-size: cover;
58+
}
59+
```
60+
61+
Once I had the demo files ready, I hosted them on a live URL so that that we’d
62+
be getting realistic latency and bandwidth to measure.
63+
64+
I opened a performance-testing-specific profile in Chrome, closed every single
65+
other tab I had open, and I was ready to begin.
66+
67+
I then fired open Chrome’s Timeline and began taking measurements. The process
68+
was a little like:
69+
70+
0. Disable caching.
71+
0. Clear out leftover Timeline information.
72+
0. Refresh the page and record Network and Timeline activity.
73+
0. Completely discard any results that incurred DNS or TCP connections (I didn’t
74+
want any timings affected by unrelated network activity).
75+
0. Take a measurement of **DOMContentLoaded**, **Load**, **First Paint**,
76+
**Parse Stylesheet**, and **Image Decode**.
77+
0. Repeat until I got 5 sets of clean data.
78+
0. Isolate the median of each measurement (the median is the [correct average to
79+
take](/2017/01/choosing-the-correct-average/)).
80+
0. Do the same again for the Base64 version.
81+
0. Do it all again for Mobile (ultimately I’m collecting four sets of data:
82+
Base64 and not-Base64 on Desktop and on Mobile[^1]).
83+
84+
Point 4 was an important one: any connection activity would have skewed any
85+
results, and in an inconsistent way: I only kept results if there was absolutely
86+
zero connection overhead.
87+
88+
### Testing Mobile
89+
90+
I then emulated a mid-range mobile device by throttling my CPU by 3×, and
91+
throttled the network to Regular 2G and did the whole lot again for Mobile.
92+
93+
You can [see all the
94+
data](https://docs.google.com/spreadsheets/d/1P720QU6CQ7pZUgCLtkOdUmwpwp_8nEyTMBN2zq5Ajmc/edit?usp=sharing)
95+
that I collected on Google Sheets (all numbers are in milliseconds). One thing
96+
that struck me was the quality and consistency of the data: very few statistical
97+
outliers.
98+
99+
Ignore the _Preloaded Image_ data for now (we’ll come back to that
100+
[later](#a-third-approach)). Desktop and Mobile data are in different sheets
101+
(see the tabs toward the bottom of the screen).
102+
103+
## Some Insights
104+
105+
The data was very easy to make sense of, and it confirmed a lot of my
106+
suspicions. Feel free to look through it in more detail yourself, but I’ve
107+
extracted the most pertinent and meaningful information below:
108+
109+
* Expectedly, the **DOMContentLoaded event remains largely unchanged** between
110+
the two methods on both Desktop and Mobile. There is no ‘better option’ here.
111+
* The **Load event** across both methods is similar on Mobile, but **on Desktop
112+
Base64 is 2.02× slower** (Regular: 236ms, Base64: 476ms). Base64 is slower.
113+
* Expectedly, **parsing stylesheets** is dramatically slower if they’re full of
114+
Base64 encoded assets. On Desktop, parsing was **over 10× slower**. On
115+
**Mobile, parsing was over 32× slower**. Base64 is eye-wateringly slower.
116+
* On Desktop, Base64 images **decoded 1.23× faster than regular images**. Base64
117+
is faster.
118+
* …but on mobile, **regular images decoded 2.05× faster** than Base64 ones.
119+
Base64 is slower.
120+
* **First Paint** is a great metric for measuring perceived performance: it
121+
tells us when the users first starts seeing something. On Desktop, regular
122+
images’ First Paint happened at 280ms, but Base64 happened at 629ms: **Base64
123+
was 2.25× slower**.
124+
* On **Mobile, First Paint** occurred at 774ms for regular images and at 7950ms
125+
for Base64. That’s a **10.27× slowdown for Base64**. Put another way, regular
126+
images begin painting in under 1s, whereas Base64 doesn’t start painting until
127+
almost 8s. Staggering. Base64 is drastically slower.
128+
129+
It’s quite clear to see that across all of these metrics, we have an outright
130+
winner: nearly everything—and on both platforms—is faster if we stay away from
131+
Base64. We need to put particular focus on lower powered devices with higher
132+
latency and restricted processing power and bandwidth, because the penalties
133+
here are substantially worse: **32× slower stylesheet parsing and 10.27× slower
134+
first paint**.
135+
136+
## A Third Approach
137+
138+
One problem with loading images the regular way is the waterfall effect it has
139+
on downloads: we have to download HTML which then asks for CSS which then asks
140+
for an image, which is a very synchronous process. Base64 has the theoretical
141+
advantage in that loads the CSS and the image at the same time (in practice
142+
there is no advantage because although they both show up together, they both
143+
arrive late), which gives us a more concurrent approach to downloading assets.
144+
145+
Luckily, there is a way we can achieve this parallelisation without having to
146+
cram all of our images into our stylesheets. Instead of leaving the image to be
147+
a late-requested resource, we can preload it, like so:
148+
149+
```
150+
<link rel="preload" href="masthead.jpg" as="image" />
151+
```
152+
153+
By placing this tag in the `head` of our HTML, we can actually tell the HTML to
154+
download the image instead of leaving the CSS to ask for it later. This means
155+
that instead of having a request chain like this:
156+
157+
```
158+
|
159+
|-- HTML --| |
160+
|- CSS -| |
161+
|---------- IMAGE ----------|
162+
|
163+
```
164+
165+
We have one like this:
166+
167+
```
168+
|
169+
|-- HTML --| |
170+
|---------- IMAGE ----------|
171+
|- CSS -| |
172+
|
173+
```
174+
175+
Notice a) how much quicker we get everything completed, and b) how the image is
176+
now starting to download before the CSS file. Preloading allows us to manually
177+
promote requests for assets who normally wouldn’t get requested until some time
178+
later in the rendering of our page.
179+
180+
I decided to make a page that utilised a regular image, but instead of the CSS
181+
requesting it, I was going to preload it:
182+
183+
```
184+
<link rel="preload" href="masthead.jpg" as="image" />
185+
186+
<title>Preloaded Image</title>
187+
188+
<link rel="stylesheet" href="image.css" />
189+
```
190+
191+
I didn’t notice any drastic improvements on this reduced test case because
192+
preload isn’t really useful here: I already have such a short request chain that
193+
we don’t get any real gains from reordering it. However, if we had a page with
194+
many assets, preload can certainly begin to give use some great boosts. I
195+
actually use it [on my homepage](/) to [preload the
196+
masthead](https://github.com/csswizardry/csswizardry.github.com/blob/21044ecec9e11998d7a1e12e9f96be2aa990c652/_includes/head.html#L5-L15):
197+
this is above the fold content that is normally quite late requested, so
198+
promoting it this way does yield some significant change in perceived
199+
performance.
200+
201+
One very interesting thing I did notice, however, was the decode time. On
202+
Mobile, the image decoded in 25ms as opposed to Desktop’s 36.57ms.
203+
204+
* Preloaded images on Mobile decoded **1.46× faster than preloaded images did
205+
on Desktop**.
206+
* Preloaded images on Mobile **decoded 3.53× faster that non-preloaded images**
207+
did on Mobile.
208+
209+
I’m not sure why this is happening, but if I were to make a wild guess: I would
210+
imagine images don’t get decoded until they’re actually needed, so maybe if we
211+
already have a bunch of its bytes on the device before we actually have to
212+
decode it, the process works more quickly…? Anyone reading who knows the answer
213+
to this, please tell me!
214+
215+
## Some Interesting Things I Learned
216+
217+
* **Progressive JPEGs decode slower than Baseline ones.** I guess this is to be
218+
expected given how progressive JPEGs are put together, but progressive JPEGs
219+
_are_ better for perceived performance. Still, it is the case that decoding a
220+
progressive JPEG takes about 3.3× as long as a baseline one. (I would still
221+
absolutely recommend using progressive, because they feel a lot faster than
222+
their baseline counterparts.)
223+
* **Base64 images decode in one event,** whereas regular images decode across
224+
several. I’m assuming this is because a data URI can’t be decoded unless it’s
225+
complete, whereas partial JPEG data can be…?
226+
227+
## Improving the Tests
228+
229+
Although I did make sure my tests were as fair and uninfluenced as possible,
230+
there are a few things I could do even better given the time (it’s the weekend,
231+
come on…):
232+
233+
* **Test on an actual device.** I throttled my CPU and connection using
234+
DevTools, but running these tests on an actual device would have no doubt been
235+
better.
236+
* **Use a more suitable image on Mobile.** Because I was keeping as many
237+
variables as possible the same across tests, I used the exact same image for
238+
Desktop and Mobile. In fact, Mobile was only really simulating lowered device
239+
and network power, and was not run with smaller screens or assets. Hopefully
240+
in the real world we’d be serving a much smaller image (in terms of both
241+
dimensions and filesize) to smaller devices. I was not. I was loading the
242+
exact same files on the exact same viewport, only with hobbled connection and
243+
CPU.
244+
* **Test a more realistic project.** Again, these were very much laboratory
245+
conditions. As I noted with preloading, this isn’t the kind of environment in
246+
which it would shine. To the same extent, expect results to be different when
247+
profiling a non-test-conditions example.
248+
249+
- - -
250+
251+
And that concludes my two-part post on the performance impact of using Base64.
252+
It kinda just feels like confirming what we already know, but it’s good to have
253+
some numbers to look at, and it’s especially important to take note of lower
254+
powered connections and devices. Base64 still feels like a huge anti-pattern.
255+
256+
- - -
257+
258+
[^1]: Excuse the semantics here: I’m basically testing on my laptop and an emulated mobile device, but I’m not talking about screensizes.

0 commit comments

Comments
 (0)