BSprites: Combined web assets using Typed Arrays and Data URIs
Disclaimer: This was a weird idea I had one day and put this together the same evening. I’ve not tested it cross-browser or in a production environment. I’ve not benchmarked this either vs actually downloading all the images. It’s kind of a ‘what if’ project at the moment. If you think it’s really dumb or cool, I’d be really interested to hear your thoughts.
Generally, whenever a browser loads a new image, it will make a new HTTP request to the server. This doesn’t always mean establishing a new TCP connection, because browsers will leave TCP connections open for reuse if they’re to the same hostname. However, these connections are managed by a ‘connection pool’. Most browsers do not do HTTP pipelining, which means the connection waits for a response before sending a new request. This means your requests are queued. If you need to load 300 images from the same hostname this could have serious implications for your user experience.
CSS sprites
A strategy that people frequently use to mitigate this is to visually combine multiple images into a single image, which can then be served with a single HTTP request. This requires someone to lay out the images onto a larger canvas, at known pixel coordinates. The image is then positioned using CSS in the browser to reveal the individual images.
The drawbacks of this method are fairly obvious:
- Image metadata is opaque: Requires the consumer of the images to know at which coordinates to find the individual images, there’s no way to serve these to some consumer you may not know, unless you also provide the metadata separately
- You need to make another image file for every combination of images you wish to serve
- If you need to add a new image, you have to recreate the sprite and update any metadata
- All images within a sprite must be of the same content type e.g.
image/png
,image/jpg
etc - Creating the images can be cumbersome and open to human error.
Using data URIs
Browsers have moved on quite a lot in the time the CSS sprite hack was established. There are 2 new features that open up other possibilities.
Data URIs
Data URIs are a way to embed inline a resource as a base64 encoded string of the actual bytes that make up that resource. A data URI has the following form:
data:[<MIME-type>][;charset=<encoding>][;base64],<data>
Data URIs can take the place of a URL in the src
attribute of an image. Here’s an example which is a small red dot:
<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA
AAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO
9TXL0Y4OHwAAAABJRU5ErkJggg==" alt="Red dot" />
Data URIs can also be used in CSS:
ul.checklist li.complete {
padding-left: 20px;
background: white url('data:image/png;base64,iVBORw0KGgoAA
AANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAABlBMVEUAAAD///+l2Z/dAAAAM0l
EQVR4nGP4/5/h/1+G/58ZDrAz3D/McH8yw83NDDeNGe4Ug9C9zwz3gVLMDA/A6
P9/AFGGFyjOXZtQAAAAAElFTkSuQmCC') no-repeat scroll left top;
}
This means if performance is critical to you, you can transfer a web page along with all images in a single HTTP request by including them inline in the HTML. Or with an extra request you can include them in the CSS.
Examples from Wikipedia
Disadvantages to using data URIs
There are however some disadvantages to the above:
- If the HTML/CSS is modified and invalidated in the browser cache, all your images need to be downloaded again even if they themselves haven’t been modified
- Encoding as Base64 takes up more space than the original binary data, meaning your image data is (~33%) heavier on the wire than if you just sent the image as binary data
ArrayBuffer
Javascript is traditionally unsuited to dealing with binary data, but modern browsers have ArrayBuffer
which is a data type that points to a fixed size raw allocation of bytes in memory. The ArrayBuffer
type can’t be manipulated directly in Javascript, but you can create a ‘view’ on a ArrayBuffer
with Typed Arrays which can then be used to read/write data to the arraybuffer
.
Typed arrays are like normal javascript arrays, except every element in the array is a fixed capacity number and they also have a buffer
property that points to the underlying arraybuffer
. The array elements will overflow, so if you have a Uint8Array
with an element set to 255
and you add 1
to that element, it (and the underlying buffer) will overflow to 0
.
In the example below, an ArrayBuffer
is instantiated with 2 bytes of memory. All the bits are set to 0 by default.
When creating a Uint8Array
from this buffer, we get an array, whose buffer
property points at our original buffer and contains the elements [0,0]
.
The XMLHttpRequest
object in Javascript has a responseType
property. This can be set to arraybuffer
and will bypass any interpretation of the received data, instead just passing an arraybuffer
object directly to the onload
callback.
A new approach to sprites
The disadvantages of CSS Sprites and embedding images with data URIs inside HTML and CSS can be overcome by:
- Serving the images separately to any HTML/CSS
- Serving binary data rather than base64 encoded data
- Serving the combined resource dynamically based on input parameters
Server side
- Create a webservice that receives a request for a set of images e.g. http://service.com/images?img1.jpg&img2.jpg
- Have the server sequentially read the raw bytes from each image file into a buffer
- When reading each image, maintain a metadata data structure which keeps track of the byte positions of each image inside the buffer along with mime type and filename
- Combine the metadata and all of the image data into a single binary buffer
- Respond to a request with content-type
application/octet-stream
and the entire buffer as the response body - Also include a header
X-Metadata-Length
which tells the client how long the metadata header is in bytes
Client side
-
Request the data from the given URL, and specify the
xhr.responseType
asarraybuffer
var request = new XMLHttpRequest(); request.responseType = 'arraybuffer'; request.open('GET', 'http://service.com/images?img1.jpg&img2.jpg', true);
-
According to the value of the
X-Metadata-Offset
take aslice()
from the buffer at the corresponding offset -
Interpret the metadata chunk as a UTF-8 string and then parse as JSON.
-
Use the
slice()
method on the rest of the arraybuffer to create subarrays corresponding to each image
- Convert the array buffers to base64 strings
- Create the
Image
DOM elements and set their Data URIs
Example Implementations
Client side
github.com/mtharrison/bsprite-client - A simple browser module for requesting bsprites from a compliant server.
Server side
github.com/mtharrison/bsprite-go - A go package for creating and serving bsprites using net/http
.
Node module coming soon too!