Back in the web's youth, you'd embed an image with <img src="some/file.jpg" />
.
Everyone who read your site would download and see that image. After
all, it was only a small file a few pixels high and wide.
Times have changed. Web pages contain huge images. People visit web
pages from various devices with different screen sizes and resolutions.
Moreover, hi-DPI screens have more pixels per area, requiring
higher-resolution images.
Web pages need to adapt. But how?
Naive approach
The easiest means developer-wise would be to simply provide images with
double (or triple) the traditional resolution.
Unfortunately, this has the drawback of delivering 4x (or 9x) as large
image files to everyone. Even people with a 3.5 inch display would need
to load the full 4K landing page image over their prepaid mobile
connection.
That's bad.
Flexible approach (the future)
What if we gave each client exactly the image size it needed? It would
mean more work for the developer, but combine the best user experience
with the best possible load time for the client.
That's called a "responsive image". Like a responsive web page, it sort
of "adapts" to the client's screen size. Unlike a responsive web page,
there is some effort required to achieve this.
Using responsive images is a trade-off between storage and complexity on
the server side, and perceived performance and quality on the client
side. Offering 100 versions for an image will give each device the
optimum image file as quick as possible, but consume lots of space on
the server and make rendering and maintenance a nightmare.
Hence, it is about finding the sweet spots of valuable image sizes.
Nowadays, browsers are quite good in interpolating images, so you do not
actually need to target all resolutions - which, by the way, is
impossible. You could never target all mobile device resolutions.
Let's get to the meat. Here's the process (inspired by Matt Wilcox).
Do this for each image location in your application (e.g. header banner,
image thumbs, gallery, etc.):
- Determine required image widths
- Generate them
- Render them
However, it is not totally as simple as this, so we'll talk about the
process in detail.
1. Finish the design
You'll save yourself wasted time when you only start optimizing image
performance after all conditions have settled, including responsive
design! Remember, premature optimization is the root of all evil.
2. Pick a standard version
You should always have a standard image that's just as large as it needs
to be for a normal density desktop screen.
3. Determine responsive image versions
Next, resize the window though all breakpoints and make a list of the
widths the image goes through. Then add pixel density variants to their
right.
Example:
Breakpoint |
x1 |
x1.5 |
x2 |
← Pixel density |
lg |
1310 |
1965 |
2620 |
Standard image and retina image (see below) |
md |
1110 |
1665 |
2220 |
The versions above are still fine for these widths |
sm+xs |
890 |
1335 |
1780 |
1 version for mobile screens, those with Retina can still use 1310 |
The selected versions are: 890, 1310 and 1965. For an example image,
their sizes are 110kB, 212kB and 430kB.
Because width matters way more than height in responsive design, the
image sizes are denoted with the new w
unit. Thus, the selected image
sizes above are called 890w
, 1310w
and 1965w
.
How to choose image versions
How many versions you choose depends on the degree of client
optimization you want to achieve. More images means possibly better
performance, but also more complexity on the server.
- When image sizes are close (as in 10%), drop the smaller one. E.g.
1200, 1280 → 1280
- There are reasonable limits for image size and density factor. Read on.
Reasonable lower limit
Almost all desktop browsers are used at widths beyond 1000px. Most
mobile devices sport double-density screens that are 320px or more in
width. Thus, images below 640w are not useful for full width images.
(Images smaller than full-width on a 320px screen are mostly so small in
file size that you generally should not need to create variants. Usually
these images are quite small on desktop, too, so you might simply reuse
the desktop image on mobile.)
A 640w JPG with 65kB would be only 20kB at 320w. While this looks like
two thirds saved (wow!), it's only 45kB. So you'd create an extra image
version just to save 45kB per image on one of the few non-retina tiny
screens. Probably there are places where you can optimize better.
Reasonable factor limit
The intuitive upper resolution limit is the max size an image can get,
multiplied by the screen's pixel density. However, since browsers are
quite good at interpolating images nowadays, you do not need to feed
every client pixel. Under most circumstances, 1.5 times larger will do
fine for retina double-density devices. (I've manually tested this.)
For a 800w JPG and a 2x version of 300kB, the 1.5x version has 187kB.
That's a 40% or 113kB reduction, which is quite much. Still, both
variants are visually almost indistinguishable with only very sharp
edges slightly blurrier in the smaller image. I call it "loss-less
enough" ;-).
Considerations
The above limits and suggestions target the "average" case, where you
want improve bandwidth usage and retina user experience with little
effort. Of course you need to derive your own strategy depending on your
audience.
4. Generate image versions
Now that you know which image widths you need, you'll have to generate
them.
I'm using Carrierwave
to store image uploads and to generate these versions. In the respective
uploader, add image versions like this:
version :banner_1965 do
process resize_to_fill: [1965, 600]
end
version :banner_1310, from_version: :banner_1965 do
process resize_to_fill: [1310, 400]
end
version :banner_890, from_version: :banner_1310 do
process resize_to_fill: [890, 272]
end
Affixing the versions with their width makes using them much easier, see
below.
5. Render the image
We're almost done. However, the technically hardest part remains: How do
we give the browser the right image variant? We cannot reliably know
upfront which screen size he has, nor how large the image will be
rendered.
Fortunately, a solution is already in the making. It has quite complex
powers, but in the form we'll be using it consists of two additional
attributes on the img
tag: srcset
and sizes
.
- srcset
- Contains a list of image URLs together with their width:
srcset="large.jpg 1965w, medium.jpg 1310w, small.jpg 890.jpg"
- sizes
- Contains a list of "reduced media queries" (which work slightly
different from CSS) and the corresponding image size:
sizes="(max-width: 900px) 500px, 1310px"
. An entry without a media
query is taken as default value.
However, since I'm already using the awesome lazysizes
(I can recommend this great library, btw) for lazy image loading, I'll
also use it for managing responsive images. Because it lazy loads the
image anyway, it can also select the appropriate size on the fly. Thus
it makes the sizes attribute dispensable.
We'll use the following markup:
<img src="fallback/image.jpg" # Fallback image for old browsers
class="lazyload" # Activate lazy image loading
# A transparent 1px gif that keeps modern browsers from loading the src
srcset="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="
data-sizes="auto" # Tell lazysizes to manage the `sizes` attribute
data-srcset="large.jpg 1965w, medium.jpg 1310w, small.jpg 890px" />
To generate this in Rails, we'll use the following helper …
# Expects the version names to end with _<image width>, e.g. banner_1965
def responsive_image_tag(uploader, versions:)
url_widths = versions.map do |version|
url = uploader.send(version).url
width = version[/_(\d+)$/, 1]
[url, width]
end
srcset = url_widths.map { |url, width| "#{ url } #{ width }w" }.join(', ')
image_tag url_widths.first[0], # lazysizes recommends src == data-srcset[0]
class: 'lazyload',
# This transparent gif keeps modern browsers from early loading the
# fallback `src` that is only for old browsers
srcset: 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==',
data: {
srcset: srcset,
sizes: 'auto',
}
end
… and use it like this:
= responsive_image_tag @page.image, versions: %i[banner_1965 banner_1310 banner_890]
Voilà! (This example image does not use lazysizes because it is not
installed on this blog. Nontheless it's using the srcset
and sizes
attributes to have the browser choose the appropriate version.)
Conclusion
We've built a simple system of responsive images that delivers
appropriate image sizes to each client. It is as easy as determining the
required image widths, defining them in a Carrierwave uploader and
rendering them with the supplied helper method.
I hope you enjoyed this post. Send me a note
when you've incorporated responsive images somewhere!
Resources