I love when things are easy in web development. For example, a responsive image is simple to create. You write your <img class="img-responsive" src="..." alt="..." />
tag, throw it in your html and then write 4 lines of CSS:
.img-responsive {
max-width: 100%;
height: auto;
}
Great! And you've even made it accessible because you wrote your alt text that described the image in a way that screen readers and people with slow connections will appreciate... Right?
Motivation
My wife, Emma, is an animator and character designer, and I sometimes help her with her website. A big part of her website is her portfolio, which consists of images. Those images need to load for people who want to view them. I also care about hosting costs and would like to keep my bandwidth down. So when she sends me a 1.3MB, 5100x3300 pixel image, I need to make sure that I'm not sending that to a phone that's 414 pixels wide.
Devices
I have a Pixel 2, it's a great phone, I'd call it high end. I get that the Pixel 4 is out these days and there's several generations of Samsung galaxy out there that are the current top of the food chain in the Android market. As far as iPhones go, they're even better in terms of chip power. Looking at new phones right now (black friday sales), the lowest I can find an new iPhone 8 for is ~$350, the lowest new Pixel 3a is $250. These aren't the newest models by any means (iPhone is on 11 I think), these are the low end of what we can call great devices.
But these are the bottom of the high end. When you go a tier down, you get cheaper and less performant devices. When you go to the top of the high end tier, you're getting devices that blow out the several year old laptop I have in terms of processing power and RAM.
You could send the high end devices the 1.3MB image, and they'd load it fine. It would even be responsive because of our fancy 4 lines of CSS. The device could display it and scroll and handle all of it and we wouldn't have to give it a second thought.
My wife and I have a phone plan where we pay by the Gigabyte up to a certain amount and then after that it's free. We usually use around 2GB per month and it's affordable. We bought our phones outright and so it keeps our monthly bill very low compared to a lot of other phone bills I've heard of. But again, we're at the bottom of the high end phone market. I know people using phones that nobody makes commercials for, phones that wouldn't fit the definition of a status symbol.
A 1.3MB image works great on an iPhone 8 or a Pixel 2, but what about a 4 year old Motorola? It might be a little slow. When you're hosting images, you're paying for storage and bandwidth, but the person viewing them is also paying for an internet connection. It's a component of accessibility to make sure that you're costing the people viewing your site as little as possible, while still giving them the best quality image.
The people viewing Emma's web site are mostly in America, people looking at it on relatively high end devices, with good internet connections. But if you're getting any traffic from Asia or Africa or South America, you're getting traffic from people who are using $30-$40 phones. The performance of those devices is not close to a late model iPhone. You have to consider accessibility as more than "Does this work on a screen reader?"
The Web Is Accessible
Fortunately, accessibility is built in to the web, we just love to break it by doing things in a way that is inconsiderate to the people viewing the website.
There's a lot of nuance for deciding how to present your images. A web site with lots of user generated content may decide to apply more compression to their images than a website that is an artist's portfolio hoping to get hired for making high quality art.
Image format is also important to consider, a PNG compresses differently than a JPEG for example. Aspect ratio is also important to consider, a phone held vertically can display a taller image and keep all of it in the viewport more natively than a very widescreen monitor that might cut off the top and the bottom.
Fortunately, because the web starts out accessible, we can get all the way there with that original block of CSS, and just using either the <picture>
element, or with adding to our <img>
element. We don't need to write JS around it until we want to add lazy-loading (another post for another time).
Starting Questions
- Do your users generate the image, or is it static content delivered by an artist?
- Do you need a different image to show up for different sized screens (art direction)?
- Does the layout of your site change around the image as you change screen sizes?
- Do we need to use the same image at different aspect ratios in different places?
What you end up doing will depend on each of these questions, and the grey area grows larger depending on the cases as we go along, you'll also find more nuanced questions to ask as follow up to each of those questions.
Intelligently cropping is a hard problem. For example, let's say you want to use an image that a user has uploaded as a profile picture and display the full thing in one page, but if you shrink to mobile it changes to a photo of just their face. You'd need some kind of facial recognition to crop properly to handle doing that at scale. You can get very close with an upper-crop rather than a center-crop if it involves people, but you're still shooting fairly blind.
For the rest of this post, let's go through something I did for a very specific case of a portfolio website.
Example: Portfolio Website
I want to start off with this example by saying that I don't think there is a perfect answer to how to do this. The best thing we can do is understand the tradeoffs we're making and do our best to make decisions that compromise the things we care about as little as possible.
Let's imagine that our web site changes from having a hamburger menu on mobile, to having a 300px wide side menu on desktop. The images on mobile will be 100% of the device width as you scroll, but on desktop, images will be 100% - 300px wide.
Funny enough, we've already hit the barest possible minimum with the first paragraph of this blog post.
- What size images do we need to serve at different screen sizes?
Let's start off with some assumptions:
- Giving a unique image size per phone screen size is beyond our abilities. So we won't have to generate a 320px wide, 375px wide, 414px wide, 640px, 828px wide etc images to satisfy every phone screen size and pixel density. We should be able to come up with a set of image sizes that covers the phone screens and pixel density fairly evenly.
- The website viewed on a huge screen needs a huge image, but we should come up with a sensible max size.
- The website viewed on a tiny screen needs a tiny image. Still keep in mind, a 414px wide screen at 3x pixel density is still 1242px wide.
- We have some wiggle room with showing slightly larger images because the browser can render a larger image smaller and have it look nicer than it can if it's trying to render a smaller image larger.
- We are not getting into art direction with the picture element in this case. We're going to render the same image at every screen size, just a different dimension and size file.
Given these assumptions, we can reason about some image sizes that we think we should deliver.
- 500px wide. This covers small phones rendering at single pixel density.
- 750px wide. This covers small desktops since remember, the sidebar is 300px wide.
- 1000px wide. This covers high density phone screens and fairly typical laptop screens.
- 1500px wide. This will take you up to 3x pixel density phones, larger laptops and some smaller desktop monitors or people who don't fullscreen their browser.
- 2500px wide. This is our semi-arbitrary sensible-maximum. It wont cover the 4k monitors, but if I find that screen size is popular, I can just add a key for the original.
- How do we generate the images from the original file?
Like all questions, there's the robust answer for scale, and the answer for a small static site. A few years ago I had a rails site that used an image magic gem to automatically generate the right sized images based on what was uploaded. At work, we have dynamically generated and then heavily cached images from a single source file. For my wife's website, we're just going to use ImageMagick because everything else is such gross overkill.
I dumped all of her images into an "originals" folder so I didn't overwrite them, and then I figured that I could write some bash for loop and mess with it until it was generating all the images. But it was slow. ImageMagick has a lot of options that let you build something a little better than a bash for loop. Let's take a look at my end script.
It's important to note that this is a throw away script, it works within the project structure and workflow that Emma and I have. You can pass it individual files, or loop over a glob and pss those into it, but it does just take an individual file as the parameter. You probably want to do something different for your project.
#!/usr/bin/env bash
PARENT_DIR=$(dirname "${1}")
for f in ${1}*.png; do
convert ${f} \
\( +clone -resize 2500 -compress JPEG -quality 90 -background white -flatten -write $PARENT_DIR/$(basename ${f%.*})-2500.jpg +delete \) \
\( +clone -resize 1500 -compress JPEG -quality 90 -background white -flatten -write $PARENT_DIR/$(basename ${f%.*})-1500.jpg +delete \) \
\( +clone -resize 1000 -compress JPEG -quality 90 -background white -flatten -write $PARENT_DIR/$(basename ${f%.*})-1000.jpg +delete \) \
\( +clone -resize 750 -compress JPEG -quality 90 -background white -flatten -write $PARENT_DIR/$(basename ${f%.*})-750.jpg +delete \) \
\( +clone -resize 500 -compress JPEG -quality 90 -background white -flatten -write $PARENT_DIR/$(basename ${f%.*})-500.jpg +delete \) \
null:
done
It loops over all the png files in a folder, takes the file, builds something out of it, outputs it a level up from the originals directory and creates a new filename off of the old one with the size appended to the file name. I'm also using JPEGs since they compress well on web and are supported in all browsers. Since it's a portfolio site, the quality I'm using is 90 since quality is important. If we cared about image quality less, we could probably get away with going down to 70.
+clone
clones the last option.
+delete
deletes the last option.
Running them at the beginning and end of each operation means we're always using the original image to generate the other sizes, so there's no chance of recursive quality degradation. the last option is null:
, and that's just so I can keep every line of generating the size looking the same. Normally on the last option you'd not need to clone or delete or even -write I think. The script just looks weird and is less clear without it.
Now that we have our images generated:
- How do we pick the best image to serve at different screen sizes?
Funny enough, our browser can actually do this for us, it just needs a little guidance.
HTML Image tags can have a couple of attributes aside from src
, we should explore how to have a single image tag select an image to show using srcset
.
Let's look at the HTML spec for srcset:
If the srcset attribute is present and has any image candidate strings using a width descriptor, the sizes attribute must also be present, and is a sizes attribute. The sizes attribute contributes the source size to the source set (if no source element was selected).
That leads us to 3 new questions:
- What is an image candidate string?
- What is a width descriptor?
- What is a sizes attribute?
We can answer these very quickly by reading the spec.
- An Image candidate string, has a valid url, whitespace char(s), and then either a width descriptor or a pixel density descriptor. In the set, we can't have duplicate width descriptors.
- A Width Descriptor has a valid number and a "LATIN SMALL LETTER W".
- A sizes attribute is a source size, a source size list or a source size value. It can have media queries and values for those media queries.
We can put all of this together to write a pretty cool image element.
First, let's generate our srcset. We'll pretend our original image was named img.jpg
<img
...
srcset="
img-500.jpg 500w,
img-750.jpg 750w,
img-1000.jpg 1000w,
img-1500.jpg 1500w,
img-2500.jpg 2500w
"
/>
Now, for our sizes, we need a little more information. We know we have a few constraints such as the 300px static width sidebar on desktop. We know vertical tablet and under is going to be 100vw, but the static sidebars mean we sorta have to hint at conditions above those widths.
Since we're giving hints and not targeting specific devices, we can actually just be fairly imprecise with our device matching, we don't have to say "Well, iPads have this... desktops start here, large desktops start here..."
So for example, how much screen space will the image take up on 1200px wide? Well, it'll be 1200px - 300px = 900px. Which is 75vw.
Let's add in a few more, on a 900px screen, the image will be 66.6...% of the width of the screen, so let's just call it an even 67%. How about another one at 1800px screen? 83vw after we do the math. Anything above that, we want to say it's close enough to 100% that we just want to load the largest image, so we just say 100vw.
This can be done better, and stylistically you may want to build it from a min-width perspective than a max-width perspective. But the point is, for this specific project, as I resize the screen and look at it on different devices, the best image is served at the best time.
<img
...
sizes="(max-width: 768px) 100vw, (max-width: 900px) 67vw, (max-width: 1200px) 75vw, (max-width: 1800px) 83vw, 100vw"
/>
If we put all of it together, we've got some html:
<img
src="img-500.jpg"
sizes="(max-width: 768px) 100vw, (max-width: 900px) 67vw,
(max-width: 1200px) 75vw, (max-width: 1800px) 83vw, 100vw"
srcset="
img-500.jpg 500w,
img-750.jpg 750w,
img-1000.jpg 1000w,
img-1500.jpg 1500w,
img-2500.jpg 2500w
"
alt="A super cool image"
class="img-responsive"
/>
And some css (this plus whatever is required to have the image on the right and the sidebar navigation on the left taking up 300px and then the mobile version):
.img-responsive {
max-width: 100%;
height: auto;
}
You'll generate different sizes if your website is designed differently, you'll write different breakpoints and widths that images take up a those breakpoints. But without a single line of javascript, we've built an image tag that loads the right image for the right screen size.
Fin
HTML starts off accessible, it's up to us to keep it that way, and dealing with images is one of the fastest way to get off track. This hasn't covered the picture element, which lets you do things like serve different image formats to different browsers, for example .webp
isn't supported by every browser yet. We've just scratched the surface of images on the web.
As a developer, you can do your part by understanding your users, making good decisions based on the available information, and executing in an accessible way. If you pull it off, you can save on bandwidth, save other people on bandwidth, make your site faster and more accessible.
Top comments (1)
Nice! thanks