When data URIs go awry
Recently I experienced a browser performance issue while working with data URIs, severe enough that the browser would lock up for a few seconds at a time when interacting with just one image. And this was at less than 1080p.
Remind me what data URIs are again
Data URIs (URI, not URL) are used to embed or bake files right into your page without requiring an additional download request or needing a server to host the actual image, but with the downside of still taking up space as text.
It’s worth realising that it takes up more space as this text version than as the original file, just in case you’re planning on using them all over the place.
They’re useful because we can bake images right into the code without requiring additional downloads, or to save data on the fly without needing to store things on the server.
The files are encoded as base64 which is a text representation of their binary format.
Here is an example of a data URI in action, with slightly fewer explosions than in an action movie:
src attribute is only showing a small snippet of the data. It can get pretty lengthy depending on the size of your image. The bits in the beginning up until the comma after
base64 describe the data format, everything after that is the data itself.
You could type it out, if you wanted to spend a lot of time with weird mixtures of letters. People have strange hobbies.
We used people’s spending habits to generate watercolour-style paintings. Every time a painting was generated it would use different colours and layouts, so it was worth letting people download ones they liked after trying a few out. Each time a painting was generated it would add a thumbnail to a list and users could easily download it.
Here are two pictures rendered using the same spending data:
The 2D canvas was being used to render these, and the canvas API provides an easy way to take snapshots and turn them into images:
canvas variable is a reference to the DOM element, not the canvas context. All you need to do it set the
src attribute of an
img element to that
toDataURL result. These are data URIs, but the API refers to them as
How the data URIs were used
To download these thumbnails, users would have to right-click and “save as”. That’s pretty clunky user experience.
We didn’t have a lot of time to work on the project — about a week for the designer and me to collaborate (which included the time to develop it) but cutting corners on this wouldn’t… ahem… cut it. I’m sure somebody took their sunglasses off while reading that out loud.
We opted to let people just click on the pictures and they would show a save dialog. A cheap and easy way to do this is to use the
download attribute of
Clicking on the link will run a file download as though the data URI was a file sitting on a server.
This was wrapped around the previous images, which still also kept their
src attributes as the same data URI.
This made everything much worse.
How could these small images be such a problem?
The browser would lock up for a moment whenever you hovered over the images. It got worse if you moved the mouse through a few thumbnails quickly.
I had some simple CSS transition hover effects on thumbnails. I was lucky if they showed a frame or two of the transition.
Thumbnails were 200 pixels wide by around 180 pixels high. So, not much repainting was needed to do a few hover effects.
Things had worked completely fine with just the
img element thumbnails, even though I was lazily storing the full size image data instead of resizing it. Instead I was just using CSS to resize the image (this was so "right-click, save as" worked).
And then the anchor elements came into play and ruined everything.
The problem was that data URIs are several times slower to decode than using a file or image directly.
I was storing the full quality image in the link for download. Each picture was only 900 x 800 pixels. Except, something we often forget is that’s actually a huge amount of data. It works out to 720,000 individual pixels.
Every time hover over a link, the browser attempts to resolve the endpoint so it can decide a few things, like whether to preload it.
Normally, browsers are pretty zippy at decoding picture formats. This doesn’t turn out to be the case when you’ve encoded them as base64. It has to take the string and turn it into the actual binary format and then decode that.
I would have posted an Xzibit meme in here, but it’s not 2008 anymore and I have no idea what the equivalent “future-land” meme is. Possibly something with VR. Like this app of a fireplace on a TV in VR:
It makes you feel like you’re right there in a cheap hotel room. Seems like they’re using data URIs in there, too with that frame rate.
Anyway, all that decoding makes things very, very slow.
Fixing the problem
I removed the
href attribute in all anchor links and hovers went back to normal. But since removing the attributes broken the entire point of having anchor links in the first place, I needed a small amendment.
hrefs would remain blank while people swung their mice around the screen, but would populate with the original data URI as soon as they were clicked on. The browser resolved the link even though it didn't exist at the start of the interaction.
I had to remove the
href content directly afterwards otherwise the problem would slowly creep back in, like the roaches at that restaurant down the street. You probably shouldn’t go there.
I did something along these lines (D3js was used to affect the DOM):
setTimeout part is needed to turn it off again without affecting the current click event. It needs to happen one moment later otherwise it just sets the
href to empty in that initial click and nothing happens. You can also use
requestAnimationFrame if you prefer.
Hovering was nice and fast again. You know, like it normally is.
It still slowed browsers down when you clicked on a thumbnail, but it seemed like that was while a save dialog was trying to show up, disguising the real problem. It didn’t feel slow any more, since the delay in getting a save dialog didn’t feel excessive.
And that’s all it took.
Data URIs are much slower than you’d think. They’re ok for image tags, but create massive performance problems in anchor links.
If you need to store data URIs in anchor links, only include the actual data URI after a user clicks. Then make sure you remove it. Be nice and tidy in the same way my room never was when I was growing up.