· 12 min read

What to Pay Attention to When Using Images on the Frontend

# Frontend
This article was auto-translated from Chinese. Some nuances may be lost in translation.

Recently I revisited the images on my blog and noticed that many of my posts were still just throwing out a simple <img src="..."> without setting width or height. When opening them on mobile, you can see the content jump downward as the image loads.

This phenomenon is called Layout Shift, which refers to unexpected movement on the screen. A common cause is that the image is too large, or the network is too slow, so the browser lays out the page without knowing the image’s dimensions, and only adjusts the layout after the image loads. I’ve also seen websites deliberately create layout shift to trick users into misclicking ads. Whether this design is “intentional” or not is debatable, but from the reader’s perspective it just feels bad.

Coincidentally, I came across an article by Jake Archibald整理ing best practices for responsive images, so I took the opportunity to organize my own thoughts and approach. As an aside, I really like HTTP203, hosted by Jake (a show on Chrome for Developers), because it contains many in-depth explorations of browsers and the web.

Why width and height should always be added

A lot of people think: I already set the image width in CSS, so aren’t the HTML width and height redundant? And since writing fixed numbers feels inflexible, they just skip them.

<img src="hero.jpg" alt="hero" />

The problem with writing it this way is that before the image finishes loading, the browser has no idea how large it is, so it lays out the page as if the image has zero height. Once the download progresses far enough for the browser to obtain the image’s intrinsic size information, it goes back and reserves the corresponding space.

As a result, you see the page suddenly jump downward. This is what’s called CLS (Cumulative Layout Shift), one of the Web Vitals metrics that directly affects SEO.

The correct way is:

<img src="hero.jpg" width="1600" height="900" alt="hero" />

By writing width="1600" height="900", the browser knows from the HTML that this image has a 16:9 aspect ratio. Even if CSS uses width: 100%, the browser can still calculate the corresponding height based on the ratio and reserve the space in advance. Once the image finishes loading, the page won’t shift.

The width and height in HTML are not the actual rendered size; they are used for the browser to calculate the aspect ratio. Your CSS still controls the real displayed size:

img {
  width: 100%;
  height: auto;
}

Since 2020, Chrome, Firefox, and Safari have automatically inferred CSS aspect-ratio1 from the width and height attributes, which is essentially equivalent to writing this behind the scenes:

img[width][height] {
  aspect-ratio: attr(width) / attr(height);
}

So as long as you properly fill in width and height, CSS can still scale the image however it wants, and CLS is handled at the same time. Unless your layout is truly determined dynamically and you don’t even know the ratio (I’ll talk about how to handle that later), the default should be to add these two attributes. (Though I still often get lazy and skip them.)

When to use CSS aspect-ratio

In some situations, you can’t get the image’s real dimensions. For example, the image URL is dynamic, or the CMS doesn’t return dimension fields, or you’re using a <div> with background-image for a cover image. That’s when CSS aspect-ratio comes in:

.cover {
  aspect-ratio: 16 / 9;
  width: 100%;
  background-size: cover;
}

aspect-ratio is very convenient when you need to constrain an image’s proportions, such as making square cards, video thumbnails, or wrapping an <iframe> into a 16:9 container. In the past, this required hacks like padding-top: 56.25%; now it can be done in one line.

So when it comes to images, the general rule is:

  • If you know the image’s true dimensions → write them in HTML width and height
  • If you don’t know them, but you can decide the display ratio → use CSS aspect-ratio
  • The two do not conflict, and can be used together

Switching formats: WebP and AVIF

JPEG is a 1992 format, older than I am. Modern formats can be much smaller:

  • WebP — A format proposed by Google, released in 2010. At the same quality, file size can be smaller than JPEG2, and it’s supported by all major browsers3
  • AVIF — An image format based on AV1 video coding, standardized in 2019. For photographic images at medium to high quality, it can be much smaller than JPEG4. Safari started supporting it in 16, while Chrome and Firefox supported it earlier, so it’s now covered by all major browsers5

You can also take a look at this article, The Secret Behind JPEG Compression, to understand the principles behind image compression.

My current approach is: use AVIF whenever possible, WebP as a fallback, and JPEG/PNG as the final fallback. The actual difference depends on the image content — images with large blocks of color and few gradients (such as UI screenshots) compress extremely well with WebP/AVIF; images with lots of high-frequency detail (such as landscape photos) will show a smaller difference, but it’s usually still worth it.

To implement format fallback, you use <picture>.

How picture, source, and srcset work together

The concept of <picture> is actually very simple: list a few <source> elements, and the browser picks the first one it supports from top to bottom; if none are supported, it falls back to <img>.

<picture>
  <source srcset="hero.avif" type="image/avif" />
  <source srcset="hero.webp" type="image/webp" />
  <img src="hero.jpg" width="1600" height="900" alt="hero" />
</picture>

So far this only solves the format problem, not the size problem. A mobile screen may only be 400px wide, so downloading a 1600px-wide image wastes bandwidth. That’s where srcset and sizes come in.

Using srcset to provide multiple resolutions

srcset has two forms, differing by the descriptor used:

  • w descriptor: indicates the image’s actual width in pixels. Used together with sizes; the browser chooses based on layout width and DPR
  • x descriptor: indicates how many device pixel ratio units the image is meant for. Suitable when the displayed size is the same, and you just need to handle Retina displays

The x descriptor is simpler and looks like this:

<img
  src="avatar.jpg"
  srcset="avatar.jpg 1x, avatar@2x.jpg 2x, avatar@3x.jpg 3x"
  width="80"
  height="80"
  alt="avatar"
/>

But if your image displays at different widths on different screens (for example, full width on mobile and a fixed 800px on desktop), the x descriptor is not enough, and you need to use the w descriptor with sizes:

<img
  src="hero-800.jpg"
  srcset="
    hero-400.jpg   400w,
    hero-800.jpg   800w,
    hero-1600.jpg 1600w
  "
  sizes="(max-width: 600px) 100vw, 800px"
  width="1600"
  height="900"
  alt="hero"
/>

sizes tells the browser how wide the image will actually be displayed in the layout. In the example above, it means:

  • When screen width ≤ 600px → the image takes up 100vw (the full screen width)
  • Otherwise → the image is fixed at 800px

Once the browser has this information, it also takes the device’s DPR (device pixel ratio, the ratio between physical pixels and CSS pixels) into account and picks the most suitable version from srcset. For example, if an iPhone screen is 390px wide and has a DPR of 3, it needs 390 × 3 = 1170px of resolution, so it will choose the 1600w version; if it’s a desktop with 800px width and DPR 2, it needs 1600px, so it will also choose 1600w; if it’s a desktop with 800px width and DPR 1, 800w is enough.

Combining format and size

If you combine <picture> and srcset, it looks like this:

<picture>
  <source
    type="image/avif"
    srcset="hero-400.avif 400w, hero-800.avif 800w, hero-1600.avif 1600w"
    sizes="(max-width: 600px) 100vw, 800px"
  />
  <source
    type="image/webp"
    srcset="hero-400.webp 400w, hero-800.webp 800w, hero-1600.webp 1600w"
    sizes="(max-width: 600px) 100vw, 800px"
  />
  <img
    src="hero-800.jpg"
    srcset="hero-400.jpg 400w, hero-800.jpg 800w, hero-1600.jpg 1600w"
    sizes="(max-width: 600px) 100vw, 800px"
    width="1600"
    height="900"
    alt="hero"
  />
</picture>

Each <source> represents all sizes of the same image in a different format. In practice, this may mean producing 3 formats × 3 sizes = 9 files. This is usually handled by a build tool or CDN, with common choices including Astro’s <Image>, Next.js’s next/image, or an image CDN service.

I now use Cloudflare Images a lot. It doesn’t require you to pre-package the same image into multiple AVIF/WebP/JPEG versions and upload them; instead, it determines the format supported by the browser based on the request’s Accept header, performs on-demand conversion, and returns the result while still benefiting from CDN caching. Compared with generating various image versions during the build stage, dynamic image format conversion avoids adding an extra step.

Switching to a different image on mobile (art direction)

What we’ve discussed so far is the same image at different resolutions. But sometimes you want the mobile version and desktop version to be completely different images. This is called art direction.

For example, the desktop version might be a 16:9 landscape banner, but when scaled down on mobile, the main subject becomes tiny and hard to see. A better approach is to use a cropped portrait image on mobile and enlarge the subject.

Another use of <picture> is exactly this. Add a media attribute to <source>:

<picture>
  <!-- Mobile: portrait crop -->
  <source
    media="(max-width: 600px)"
    type="image/avif"
    srcset="hero-mobile-400.avif 400w, hero-mobile-800.avif 800w"
    sizes="100vw"
  />
  <source
    media="(max-width: 600px)"
    type="image/webp"
    srcset="hero-mobile-400.webp 400w, hero-mobile-800.webp 800w"
    sizes="100vw"
  />

  <!-- Desktop: original landscape -->
  <source
    type="image/avif"
    srcset="hero-800.avif 800w, hero-1600.avif 1600w"
    sizes="800px"
  />
  <source
    type="image/webp"
    srcset="hero-800.webp 800w, hero-1600.webp 1600w"
    sizes="800px"
  />

  <img
    src="hero-800.jpg"
    width="1600"
    height="900"
    alt="hero"
  />
</picture>

<source> is also selected from top to bottom, so the ones with media should go first. The mobile image is matched first, and only if that doesn’t apply will the desktop version be used.

There’s one small detail to note: the mobile image may be portrait-oriented, and its aspect ratio may differ from the 16:9 written on <img>. In that case, managing the container ratio consistently with CSS aspect-ratio, or switching it with CSS at different breakpoints, is more stable than relying on fixed attributes on <img>.

Wrapping up with object-fit

When the ratio of the reserved space doesn’t match the image itself (for example, forcing a card to be 1:1 while the image is 16:9), object-fit comes into play:

img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

object-fit works on both <img> and <video>. Two commonly used values are:

  • cover — Fill the container; the image will be cropped. Common for card thumbnails and profile photos
  • contain — Show the entire image; extra space in the container is left empty. Used for content that should not be cropped, such as logos or product images

With object-position, you can adjust the crop alignment position (for example, object-position: top makes cropping start from the top, helping avoid cutting off a person’s chin).

loading and fetchpriority

In addition to size and format, there are now two attributes that are basically standard:

<img
  src="hero.jpg"
  width="1600"
  height="900"
  loading="lazy"
  decoding="async"
  alt="hero"
/>
  • loading="lazy" — The image is downloaded only when it enters the viewport. This is especially effective for long articles, product lists, and masonry layouts, saving a lot of initial loading traffic
  • decoding="async" — Image decoding won’t block the main thread. The default behavior is already close to async, but adding it can avoid synchronous decode paths in some browsers
  • fetchpriority="high" — Conversely, for critical images, you can add this to make the browser prioritize downloading them

Browsers have added many image optimization mechanisms over the past few years, unlike the old days when you had to write complex JavaScript to optimize images yourself. Compared with manual handling in the past, it’s much easier now. What frontend developers need to do is add the attributes that should be there, and let the browser handle the rest.

Footnotes

  1. Setting Height And Width On Images Is Important Again — Smashing Magazine

  2. WebP Compression Study — Google

  3. Can I Use — WebP

  4. AVIF for Next-Generation Image Coding — Netflix Tech Blog

  5. Can I Use — AVIF

Related Posts

Explore Other Topics