Twenty-four client logos, one request
A wall of client logos used to cost 24 image requests. Now it costs one: how the sheet is built and the fallback a CSS background needs.
The home page of this site shows twenty-four client logos. The first version loaded twenty-four image files. Each was small, but each was a separate request, and a logo wall has an awkward property: all of them are needed at the same moment.
It is now one image. Here is how.
Build one sheet
A script takes every logo in a folder and does four things to each:
- Flatten it onto white, so transparent and opaque logos end up on the same plate.
- Trim the white margin, so a logo with a wide border is not smaller than its neighbours.
- Scale it to fit a fixed cell, 360 by 225 pixels, with padding. Scaling goes both ways, so a 150-pixel source appears as large as a 2000-pixel one.
- Paste it into its cell in a grid six columns wide.
The result is one WebP image, 2160 by 900 pixels, around 140 KB. Next to it the script writes a small index file: the number of columns, the number of rows, and the list of logo names in order.
Adding a client is dropping a file into the folder and running the script.
Show one cell per tile
Each tile on the page is an empty element with the sheet as its background. Two CSS properties select the cell:
.logo-cell {
background-image: var(--client-wall);
background-size: 600% 400%; /* 6 columns, 4 rows */
background-position: 40% 33.33%; /* column 2 of 0..5, row 1 of 0..3 */
}
background-size scales the sheet so that one cell fills the tile. background-position in percentages has a useful rule: x / (columns - 1) and y / (rows - 1). With that, the same numbers work at any tile size, so the wall is responsive with no JavaScript.
The tile keeps the cell's aspect ratio, 8 to 5, and that is all the layout it needs.
Do not forget what a background cannot do
An <img> tells you when it fails to load. A CSS background does not. If the sheet is refused, every tile stays blank and nothing in the page knows.
So the page loads the sheet once through an Image object first. It is the same request the tiles would make, so it costs nothing extra. If that fails, one CSS variable is switched to the same file under a fresh address, and every tile follows. The variable is the reason the background is written as var(--client-wall) and not a fixed URL.
Tiles also carry role="img" and a label with the client's name, since a background image is invisible to a screen reader.
Only load it where it is shown
The first build had a quiet mistake. The code that prepared the sheet ran as soon as the client list was imported, and several pages import that list just to count the clients. So every page downloaded 140 KB of logos it never displayed.
The preparation is now a function that only the two pages with a logo wall call. Pages that show a number download nothing.
The same idea for vendor marks
The technology section has a different problem: twenty-odd vendor logos in different colours and shapes, some with backgrounds. Those are stored as single-colour outlines and drawn in one ink, so they read as one set. Five of them had no ready-made outline, so each was reduced to a black and white mask and traced into a vector path. That data is about 60 KB, so it lives in its own file and loads only on the home page.
What it bought
| Before | After | |
|---|---|---|
| Requests for the logo wall | 24 | 1 |
| Files for a typical page | 18 | 11 |
| Weight of a typical page, uncompressed | 900 KB | 440 KB |
Sprites are an old technique. They went out of fashion when HTTP/2 made extra requests cheap. They still earn their place when many small images are always needed together.
The clients themselves, with what was delivered for each, are on the clients page.