CategoryCode and architecture
Website images: size, format and when to load them
A light image comes down to four decisions in the HTML. Ship a file at the size it’s actually shown, with srcset and sizes. Use a modern format such as WebP or AVIF. Declare width and height so the browser can reserve the space. And pick when to load: loading="lazy" only for what sits below the fold, high priority for the main image at the top (web.dev, browser-level lazy loading). The last one is the easiest to get wrong, because one attribute in the wrong place delays the very image people see first.
Why images weigh so much
According to the HTTP Archive, cited in web.dev’s lazy loading article, images are the most requested resource on most sites and use more bandwidth than anything else. Among the 10% of sites that are heaviest on this front, each one sends more than 5 MB of images, on both desktop and mobile.
Weight isn’t the only issue. The big image at the top is often the element measured by Largest Contentful Paint, or LCP, the metric that marks when the main content of the page has shown up. A late image there delays the whole metric.
Which images deserve the weight, and where they sit
Before optimizing, it’s worth asking whether the image needs to be there at all. In NN/g’s eyetracking studies, people study photos that carry information, like products and real people, and ignore the ones that only decorate the page (Jakob Nielsen, NN/g). A decorative image costs bytes and gives no attention back.
Then comes position. In a 2018 NN/g study, people spent about 57% of their viewing time on the first screen and 74% on the first two (Therese Fessenden, NN/g). That’s why the top image deserves priority and the rest can wait for the scroll.
The catch is that the first screen changes from device to device. A responsive layout has two, three or more folds, one for each screen size (Amy Schade, NN/g). Whoever marks up the images in the HTML has to decide without knowing which screen the page will open on.
One file for each screen width
MDN’s responsive images guide separates two problems. The first is resolution switching: the same photo in files of different widths. The second is art direction: a different crop of the photo for a different screen.
Resolution switching only needs two attributes on the <img> tag:
srcsetlists the available files and the real width of each one, likephoto-480.jpg 480w.sizessays how much space the image will take on screen under each condition, like(max-width: 480px) 440px.
With that, the browser picks the smallest file that still fills the space sharply. In MDN’s example, the 800 pixel version weighs 128 KB and the 480 pixel one weighs 63 KB (MDN). On a page with dozens of photos, that gap adds up.
The same guide explains why this lives in the HTML and not in a script. The browser starts downloading images before it reads the page’s CSS and JavaScript. By the time a script worked out the screen width, the big file would already be on its way (MDN).
For art direction, the tool is the <picture> element, with one <source> per crop and an <img> at the end as the fallback.
A modern format, with a fallback
WebP and AVIF store the same photo in fewer bytes than JPEG and PNG. <picture> lets you offer the new format and keep the old one for browsers that can’t use it. Each <source> declares its type in the type attribute, and the browser skips what it can’t read (MDN).
If you use Astro, you don’t have to build this by hand. The <Picture /> component generates AVIF and WebP from a PNG and keeps the original as the fallback. The <Image /> component converts to WebP by default. That only applies to images stored in src/; anything in public/ ships as it is (Astro docs on images).
Reserve the space before the image arrives
Without width and height, the browser doesn’t know the image’s size until it arrives. The text below jumps up and then down, and the reader loses their place. That’s why web.dev recommends declaring both dimensions on every <img> (web.dev). CSS can still make the image flexible, and the aspect ratio is kept. The metric for that jump is CLS, which the post on loading states covers.
The same article has a less obvious case. In a gallery with no dimensions, every image starts at 0 by 0 pixels. They all seem to fit on screen at once, so the browser downloads the whole gallery, even with lazy loading on (web.dev).
Astro’s <Image /> component fills in width and height on its own for local images (Astro docs).
Lazy loading, but only below the fold
The loading="lazy" attribute delays the download until the image gets close to the visible area. It works without a JavaScript library. web.dev’s own warning is blunt: don’t use it on images likely to show up on the first screen, especially the one that becomes the LCP (web.dev). The browser can only defer an image once it knows where it sits on the page, so the most important image on the page ends up waiting.
WordPress learned this the hard way. It started marking almost every image as lazy, including the ones at the top. In a lab test with the Twenty Twenty-One theme, turning lazy loading off made LCP on archive pages 13% to 15% faster (web.dev, the performance effects of too much lazy loading). On the other hand, image bytes more than doubled.
The fix they tested got the best of both. Loading only the first featured image normally and leaving the rest lazy undid the LCP loss and kept the byte savings of the default behavior (web.dev).
Priority for the main image
Writing loading="eager" doesn’t speed anything up. It’s just the browser’s normal behavior, with no extra delay. To ask for one image ahead of the others, the attribute is fetchpriority="high" (web.dev, Fetch Priority API).
The reason is in Chrome’s order of work. Images in the visible area usually start at low priority and only go up once layout is done. Marking the main one skips that wait. On the Google Flights page, giving the background image high priority took LCP from 2.6 to 1.9 seconds (web.dev).
The attribute can lower priority too. In a carousel, only the first image is visible right away. The others can get fetchpriority="low" so they don’t compete with it for the network (web.dev). The design side of carousels is in the post on ecommerce carousels.
How it shows up in my work
Every image on kahue.net has a different job, and the code follows the job.
The portrait on my about page is shown 200 pixels wide, and the WebP file is 400. On a high density screen, the browser uses pixels that already exist instead of stretching the image. Since the portrait sits at the top, it gets width and height, normal loading and fetchpriority="high".
In the case studies, like the one for Vexor, the large image right below the title ships in three widths (768, 1040 and 1400 pixels), with sizes following the width of the page column. It’s marked eager, because Astro’s image component outputs loading="lazy" when nothing is set (Astro reference). The gallery thumbnails sit further down. So they ship at 320 and 640 pixels, with lazy loading.
Each thumbnail links to the full screen in WebP, generated at build time. Without JavaScript, the click opens the image. With JavaScript, it opens the gallery as an overlay. That link used to point to the original PNG file, which was much heavier.
One thing is still missing. The case study cover asks for normal loading, but not for high priority. According to the Astro reference, the priority property handles that in one go for images at the top. I haven’t measured the effect of that change, and this post doesn’t put a number on it.
Where the sources disagree
The two web.dev articles seem to disagree about the risk of lazy loading. The first says that in Chrome for Android on 4G, 97.5% of lazy images finished loading within 10 milliseconds of becoming visible (web.dev). Seen that way, the delay barely exists.
The second found, in HTTP Archive data, a median LCP of 2,922 milliseconds on pages without lazy loading and 3,546 on pages that use it (web.dev). The authors point out that it’s correlation, not cause.
Both can be true. The first number measures offscreen images, which the browser fetches ahead of time while people scroll. The second catches the case where lazy loading landed on the top image, which gets no head start at all.
The common mistake: letting CSS shrink the photo
The most frequent mistake is exporting the image at the original file’s width and letting CSS scale it down on screen. It looks fine on any device, which is why nobody notices. But the phone downloads the pixels of a big monitor to show a fraction of them. MDN calls this a waste of bandwidth, paid for by whoever is on a mobile data plan. The check is simple: open the Network tab in the browser’s dev tools on a narrow screen and see which file came down.
Sources
- web.dev, Browser-level image lazy loading for the web
- web.dev, The performance effects of too much lazy loading
- web.dev, Optimize resource loading with the Fetch Priority API
- web.dev, Largest Contentful Paint (LCP)
- MDN, Using responsive images in HTML
- Astro docs, Images
- Astro docs, astro:assets reference
- Nielsen Norman Group, Photos as Web Content
- Nielsen Norman Group, Scrolling and Attention
- Nielsen Norman Group, The Fold Manifesto: Why the Page Fold Still Matters


