|
| 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