Guide · Frontend
Image Optimization: Formats, Sizes and Lazy Loading
30 min readUpdated By Saket Bhatnagar
Researched and drafted with AI, reviewed and edited by Saket Bhatnagar.
In short
- Image optimization means sending each visitor the smallest image file that still looks right on their screen. It must be in a format their browser can open, and arrive when the page needs it.
- Pick the format by what is in the picture. Photos suit AVIF or WebP, two newer formats that make smaller files, with JPEG as the fallback. Logos and icons suit SVG (drawn shapes) or PNG.
- Make several widths of every photo. List them in srcset, the menu of files, and describe the display width in sizes, a layout hint, so a phone never downloads a desktop-sized file.
- Load first-screen images straight away, with fetchpriority high (a download-order hint) on the main one. Mark the rest loading lazy, so they wait until they come within a set distance of the screen. Give every image a width and height so text does not jump.
- Beginners can stop after the video section. Engineers then get our tests in Chromium 141, the open-source core of Chrome. Lazy images start loading 1,250 px below the screen on 4G, but 3,000 px when the browser has no connection-speed estimate, and JavaScript reports 4g for both. They also get AVIF encode cost, the time a server spends compressing a file, and sizes auto, which lets the browser measure each image slot itself.
What goes wrong when a shop sends full-size product photos?
Without image optimization, the shop sends every visitor the same huge camera file, so a phone downloads megabytes of pixels it can never show. The page stays grey for seconds, and text jumps as each photo lands.
Picture a common setup. A marketplace sells hand-painted sarees, dupattas and wall hangings. The catalogue team uploads camera photos about 4,000 pixels wide, around 3.5 MB each. The setup is made up, but the problem is real.
The category page shows 24 products, and every photo loads at once. One visit pulls about 84 MB of images. On office Wi-Fi the page feels fine. On a phone with a weak mobile signal, product cards stay grey and text keeps jumping down the screen.
On a common phone 390 CSS pixels wide, a product card fills the whole width. A CSS pixel is the unit page layout uses, and a sharp screen packs several real pixels into each one, so this card can use about 1,170 real pixels. The 4,000 pixel photo is more than three times wider than that. Since area grows with the square, it carries more than ten times the pixels the screen can use.
This guide fixes that page one choice at a time: the format, the size, when each image loads, the jumping and the product videos. Each fix comes with a way to check it worked.
What is image optimization?
Image optimization is the work of making images download fast and display correctly without visible loss of quality. It covers four choices: the file format, the pixel size, when each image loads, and how much space it reserves on the page.
A few words come up again and again, so here they are in plain terms.
- Format: the file type, such as JPEG, PNG, WebP or AVIF. Each one stores pixels in a different way.
- Compression: shrinking the file. Lossy compression throws away detail your eye barely notices. Lossless compression keeps every pixel exactly.
- Intrinsic width: how many pixels wide the image file really is, for example 1,280.
- CSS pixels and DPR: CSS pixels are the units the layout uses. The device pixel ratio (DPR) says how many real screen pixels each CSS pixel covers, often 2 or 3 on phones.
- Viewport: the part of the page visible in the browser window right now. Below the fold means further down, out of view until you scroll.
A flex banner printing shop makes the idea easy to picture. A client brings one design. The shop prints it huge for a highway hoarding, medium for a shopfront board and small for handbills. Nobody prints a hoarding-sized sheet and folds it into a handbill.
The marketplace was doing exactly that: every visitor got the hoarding-sized photo. The mapping is simple. The design is your original photo, each print size is a resized copy, and the printer choosing a size is the browser choosing a file.
Where the picture breaks: a printer knows the final size before printing. A browser picks a file before it has worked out the page layout, so you must tell it the size in advance. That is the job of the sizes attribute, covered below.
Which image format should you use: JPEG, PNG, WebP or AVIF?
Use AVIF or WebP for photos, with JPEG as a fallback, and use SVG or PNG for logos, icons and screenshots. The right format depends on what is in the picture.
Photos have smooth changes of colour, like the folds of a cotton saree. Lossy compression works well on them because the lost detail is hard to see. Logos and screenshots have sharp edges and text, where lossy compression leaves blurry halos.
MDN, the main reference for web developers, says lossy WebP files are on average 25 to 35 percent smaller than JPEGs of similar quality. It says lossy AVIF files are around 50 percent smaller than JPEG.
We checked this on four real photos that ship with the scikit-image Python library. We used SSIM, a score of how close a compressed image is to the original, where 1.0 means identical. At the same score of 0.95, WebP saved 20 to 34 percent against JPEG and AVIF saved 38 to 53 percent.
| Format | Compression | Transparency | Best for | Marketplace use |
|---|---|---|---|---|
| JPEG | Lossy only | No | Photos, as the safe fallback | Fallback for every product photo |
| PNG | Lossless | Yes | Screenshots and images that need transparency | Size chart screenshots |
| WebP | Lossy and lossless | Yes | Photos and graphics | Second choice for product photos |
| AVIF | Lossy and lossless | Yes | Photos, usually the smallest files | First choice for product photos |
| SVG | Vector drawing, not pixels | Yes | Logos, icons and simple diagrams | Logo, cart icon and craft badges |
WebP is described in RFC 9649, an informational document published in November 2024 by the IETF, the group behind internet standards. It covers lossy and lossless images, transparency and animation. MDN lists the same features for AVIF.
AVIF has two costs. MDN says it does not support progressive rendering, which means showing a rough picture first and sharpening it as bytes arrive. So a standard AVIF file must arrive in full before it shows. It is also slow to encode, which matters later for the image pipeline.
As of 19 October 2026, caniuse data lists AVIF in Chrome 85, Firefox 93 and Safari 16.4 or later. Older phones that never got updates may not decode it, which is why JPEG stays as the last fallback.
SVG is different from the rest. It stores drawing instructions, such as lines and curves, instead of pixels. So one small file stays sharp on a handbill or a hoarding. It is a poor choice for photos, which cannot be drawn with a few shapes.
How do responsive images work with srcset and sizes?
You list several widths of the same photo in the srcset attribute, and tell the browser in sizes how wide the image will appear. The browser then downloads only the one file that fits the screen.
The srcset attribute is a list of files, each with its true width. The list for one saree reads like this: the 320 pixel file is 320w, the 640 pixel file is 640w, and so on. The w unit means intrinsic width in pixels, not CSS pixels.
The sizes attribute says how wide the image will show, in CSS pixels. The marketplace cards fill the whole width on phones and a third of it on desktops. So the sizes value says 100vw up to 600 pixels wide, and 33vw after that. One vw is 1 percent of the viewport width.
The browser needs this hint because of timing. MDN explains that browsers start downloading images before they have read the CSS and JavaScript. At that moment the browser does not know your layout, and sizes fills that gap.
Here is what the browser does on a 390 pixel wide phone with a DPR of 3. Follow the boxes from left to right and notice the multiplication in the third box. That step is why sharp phone screens need bigger files than you might expect.
How the browser picks one file from srcset
Drag to exploreOne saree photo on a 390 px phone with DPR 3. Notice the DPR step: it turns 390 into 1,170, so the 1280w file wins.
Diagram connections:
- Read the sizes hint to Work out the slot width
- Work out the slot width to Multiply by the DPR
- Multiply by the DPR to Pick a file from srcset
- Pick a file from srcset to Download that file only
- Read the sizes hint: Phone under 600px wide, so 100vw
- Work out the slot width: 100vw on this phone is 390 CSS pixels
- Multiply by the DPR: DPR 3, so it needs about 1,170 real pixels
- Pick a file from srcset: Smallest at least that wide: 1280w
- Download that file only: The other four widths are never fetched
The browser has the final say. MDN says it may also weigh zoom level and network speed, so treat srcset as a menu, not an order. If no file is an exact match, MDN says it picks the first bigger one and scales it down.
We ran this in headless Chromium 141. A 390 pixel phone at DPR 3 fetched w1280, at DPR 2 it fetched w960, and a 1,440 pixel desktop at DPR 1 fetched w640. Each time, only one file was requested.
Nobody types these lists by hand for 6,000 products. A small TypeScript helper builds them. imageUrl is a made-up function that builds the path to one stored version, such as the 640 pixel AVIF of product 812.
type Format = "avif" | "webp" | "jpg";
const WIDTHS = [320, 640, 960, 1280, 1920];
function imageUrl(id: string, width: number, format: Format): string {
return `/img/${id}/w${width}.${format}`;
}
export function buildSrcset(id: string, format: Format, originalWidth: number): string {
return WIDTHS.filter((w) => w <= originalWidth)
.map((w) => `${imageUrl(id, w, format)} ${w}w`)
.join(", ");
}
// Full width on phones, a third of the screen on desktops.
export const CARD_SIZES = "(max-width: 600px) 100vw, 33vw";
console.log(buildSrcset("812", "avif", 1000));
// /img/812/w320.avif 320w, /img/812/w640.avif 640w, /img/812/w960.avif 960wTwo lines matter most. The filter drops any width bigger than the original upload, so the site never stretches a small photo and pretends it is sharp. CARD_SIZES must match the real CSS, or the browser picks the wrong file.
Here is how to check which file a page really used. Run this in the browser console, the developer panel every browser has.
// Paste into the browser console on the category page.
for (const img of document.querySelectorAll("img")) {
const file = img.currentSrc.split("/").pop();
const shown = Math.round(img.getBoundingClientRect().width);
console.log(file, shown + " CSS px", "DPR " + devicePixelRatio);
}
// On a 390 px wide phone with DPR 3, a card prints:
// w1280.avif 390 CSS px DPR 3
// A lazy image that has not loaded yet prints an empty file name.currentSrc is the file the browser chose, and the width is the space it fills. Do not use naturalWidth for this check. In our Chromium test it printed 390, not 1,280. For files listed with w widths, the browser divides the file width by the density it picked.
Some images always show at one fixed CSS size, like the 48 pixel artisan avatar beside each review. For those, srcset can use x descriptors instead: 1x for normal screens and 2x for sharp ones. No sizes value is needed then.
When should you use the picture element instead of srcset?
Use the picture element when you need a different file type or a different crop, not just a different size. It lets you offer AVIF and WebP with a JPEG fallback, or a tighter crop for small screens.
The picture element wraps several source entries and one normal img tag. The browser walks the sources in order and uses the first one it supports. Each source carries a type, such as image/avif, so a browser without AVIF support simply skips it.
The img tag at the end is required. It holds the JPEG fallback, the alt text and the width and height. MDN warns that without it, no image appears at all. In our Chromium test, a picture with only source entries made zero requests and stayed 0 pixels tall.
- First source: AVIF, with its own srcset of five widths and the same sizes.
- Second source: WebP, with the same five widths.
- Final img tag: JPEG srcset, plus sizes, alt text, width and height.
The second use is art direction: changing the crop, not just the scale. The home page banner shows a full wall hanging on desktops. On a phone the whole piece shrinks to a blur of colour. So the team makes a tight crop of the peacock in the centre. A source with a media condition, a screen-width test such as max-width 600px, sends it to narrow screens.
In our test, a 390 pixel phone fetched only the tight crop and a 1,440 pixel desktop fetched only the wide banner.
How does lazy loading work for images?
Lazy loading tells the browser to wait before fetching an image until the visitor scrolls near it. You switch it on by adding loading="lazy" to an img tag, and no JavaScript library is needed.
The loading attribute has two values. MDN says eager, the default, loads the image straight away. Lazy waits until the image is within a distance of the viewport that the browser decides. It works on images and iframes, the boxes that embed another page.
MDN adds one detail many people miss: loading is only deferred when JavaScript is enabled. With scripting off, lazy images load like normal ones. This stops sites from tracking how far you scroll.
The hero image trap
Lazy loading the biggest image on the first screen makes it start later, not sooner. The browser cannot tell where a lazy image sits until it has the layout, so it waits for the stylesheets first.
We tested a hero image behind a stylesheet that took 800 ms to arrive, with the image itself taking 1 second. The eager hero was requested at about 30 ms, alongside the stylesheet. The lazy hero waited until about 850 ms, after the stylesheet came in.
The late start went straight into the paint time. Over three runs each, the eager hero painted at about 1.05 seconds and the lazy hero at about 1.87 seconds.
That hero is usually what browsers measure as Largest Contentful Paint, or LCP: the moment the biggest visible element finishes drawing. The Core Web Vitals guide covers LCP in depth. For images, the rule is short: load the hero eagerly, with high fetch priority.
The diagram below is the rule for every image on the category page. Start at the top question. Notice that only one image on the page gets the high priority path.
Choosing how each image loads
Drag to exploreApplied to every product card when the page is built. Only one image takes the high priority path.
Diagram connections:
- On the first screen? to Load eagerly (Yes)
- On the first screen? to Add loading lazy (No)
- Load eagerly to Is it the hero image?
- Is it the hero image? to Add fetchpriority high (Yes)
- Is it the hero image? to Keep normal priority (No)
- On the first screen?: Visible before any scrolling
- Load eagerly: The default, fetched straight away
- Is it the hero image?: Likely the LCP element
- Add fetchpriority high: Starts at High priority
- Keep normal priority: The browser decides the order
- Add loading lazy: Fetched when within the browser distance
Fetch priority is a hint with the values high, low and auto. Auto is the default and lets the browser decide. MDN says high fetches the image at a high priority relative to other images. Our advice follows from that word relative: keep high for one or two images.
The helper below sets these values for product cards. It takes the card position in the grid and how many cards fit on the first screen. On a phone one card and part of the next show at once, so the team sets four as a safe margin.
interface CardImageAttrs {
loading: "eager" | "lazy";
fetchPriority: "high" | "auto";
width: number;
height: number;
}
export function cardImageAttrs(index: number, firstScreenCount: number): CardImageAttrs {
return {
loading: index < firstScreenCount ? "eager" : "lazy",
fetchPriority: index === 0 ? "high" : "auto",
width: 800,
height: 1000, // every product photo uses a 4:5 crop
};
}
console.log(cardImageAttrs(0, 4).fetchPriority, cardImageAttrs(5, 4).loading);
// high lazyThe loading line is the lazy rule from the diagram. Only the first card gets high priority. The fixed width and height work because every product photo is cropped to the same 4:5 shape, which the next section explains.
How many images load before the shopper scrolls?
Pause and predict. The phone page has 24 cards in one column. The first four are eager and the other 20 are lazy. How many images does the browser fetch before the shopper scrolls at all?
In our Chromium 141 test the answer depended on the network. With 4G emulated it was 4, because the fifth card sat 1,298 pixels below the screen, just past the 1,250 pixel look-ahead distance. With no connection-speed estimate it was 8, because the distance grew to 3,000 pixels. On a 1,440 pixel desktop with three columns, 12 of the 24 loaded on 4G and 21 with no estimate.
Lazy does not mean when it scrolls into view. It means when it comes within the browser's look-ahead distance, which runs from 1,250 to 8,000 pixels in Chromium 141, depending on the connection. The engineer section below has the numbers.
How do you stop images from causing layout shift?
You stop image layout shift by giving every image a width and height in the HTML, or a CSS aspect ratio. The browser then reserves the right space before the file arrives, so nothing jumps.
Layout shift is when visible content moves after it first appears. We loaded one product card with its photo delayed by one second. Without width and height, the image box started 0 pixels tall, and the product name jumped from 16 to 504 pixels down when the photo arrived.
With width 800 and height 1000 on the same card, Chromium reserved a 390 by 488 box at once. The name stayed at 504 pixels and the layout shift score was 0. That score feeds Cumulative Layout Shift (CLS), a Core Web Vitals metric, and 0 means nothing moved. Without width and height, one card alone scored 0.025.
The width and height do not have to match the final display size. MDN explains that the browser uses them to work out the aspect ratio and reserve space. CSS can still make the image fluid, with width set to 100 percent and height set to auto.
Lazy loading makes this more important. With 24 lazy cards that had no width and height, Chromium 141 fetched all 24 at once, with or without 4G emulated. Every image box was 0 pixels tall, so the whole column shrank to about 1,140 pixels, well inside the look-ahead distance.
- Store the width and height of every original when it is uploaded, next to the alt text.
- Crop product photos to one shape, so every card in the grid has the same height.
- Use a light background colour or a tiny blurred preview inside the reserved box, so it never looks broken.
- For background images set in CSS, give the box an aspect ratio or a fixed height yourself.
How do you make product videos and animated GIFs lighter?
Show a light poster image first, download only what is needed before the visitor presses play, and offer a modern format with a fallback. Replace large animated GIFs with short, muted video files.
The marketplace shows a 40 second clip of an artisan painting with a bamboo pen on every craft page. Most shoppers never press play, so the page should not download the clip up front.
- Poster: an image shown before playback. MDN says that without it, nothing shows until the first frame is available. A small AVIF still from the clip works well.
- Preload: a hint with the values none, metadata and auto. Metadata fetches only details such as length. MDN says autoplay takes precedence over preload.
- Sources: several source entries with types, such as WebM and MP4. MDN says the browser plays the first one it understands.
- Autoplay: MDN says modern browsers block autoplay for videos with sound. A looping product clip should be muted, and marked playsinline so iPhones play it inside the page.
We measured preload in Chromium 141 with a 5.1 MB, 40 second WebM clip. Preload none fetched 0 bytes, plus the poster. Metadata fetched 131 KB in one run and 197 KB in another. Auto downloaded the whole 5.1 MB, and so did a preload none video with autoplay muted added.
The playsinline rule comes from Safari on iPhone. WebKit announced in 2016, with iOS 10, that iPhone videos without playsinline play in fullscreen, and that muted videos may autoplay without a tap.
The old spinning-saree animation on the home page was a heavy animated GIF. GIF is an old format with poor compression for photo-like frames. We rebuilt a similar 4 second, 480 by 600 pixel clip from a real photo to see the gap.
| File | Size | Compared with the GIF |
|---|---|---|
| Animated GIF | 9.95 MB | Baseline |
| MP4, H.264 video | 134 KB | About 74 times smaller |
| WebM, VP9 video | 73 KB | About 135 times smaller |
Your own ratio will differ with the content, but the direction is reliable for photo-like animation. WebKit made the same point in 2016, calling GIFs up to twelve times as expensive in bandwidth as video.
Lazy loading for video itself is new. As of 19 October 2026, caniuse lists loading="lazy" on video and audio only in Chromium-based browsers. That means Chrome and Edge 148 or later, Opera 132 or later and Chrome for Android, not Firefox or Safari. Until that spreads, preload none plus a poster does the job.
What do browsers really do with lazy images and priorities?
Browsers start lazy images earlier than most people assume, and they rank image downloads by rules you cannot see in the HTML. Everything here was measured on headless Chromium 141.0.7390.37, a September 2025 build. We did not test Chrome 155, current as of 19 October 2026, or Firefox and Safari. Headless means no visible window; the Playwright 1.56 test tool drove it.
The lazy loading distance
We placed one lazy image at different depths below the screen and recorded when it was requested. With no network throttling, Chromium fetched it up to 2,999 pixels below the screen, and not at 3,000. Phone or desktop and DPR made no difference.
That 3,000 is not the fast-connection value. Our headless browser had no estimate of the connection speed, and Chromium 141's settings file uses 3,000 pixels for that unknown state. With 4G emulated, the same build fetched up to 1,249 pixels and not at 1,250.
Emulated 3G gave 2,500 pixels, 2G gave 6,000 and slow 2G gave 8,000, each matching the settings file. The web.dev article, updated in August 2024, gives 1,250 pixels on 4G and 2,500 on 3G or lower. It says Chrome cut these from 3,000 and 4,000 pixels in July 2020, and that they are hardcoded.
| Connection, and what JavaScript reports | Chromium 141 settings file | Our test, headless Chromium 141 | web.dev article (Aug 2024) |
|---|---|---|---|
| Unknown, no speed estimate yet (reports 4g) | 3,000 px | 3,000 px | Not listed; 3,000 px was the 4G value before July 2020 |
| 4g | 1,250 px | 1,250 px | 1,250 px |
| 3g | 2,500 px | 2,500 px | 2,500 px |
| 2g | 6,000 px | 6,000 px | 2,500 px (listed as 3G or lower) |
| slow-2g | 8,000 px | 8,000 px | 2,500 px (listed as 3G or lower) |
| Offline (reports 4g) | 8,000 px | Not tested | Not listed |
The trap is in JavaScript. navigator.connection.effectiveType, the browser's guess at the connection type, reported 4g in both the unknown and the 4G runs. Chromium's source maps unknown and offline to 4g for scripts, so a page cannot tell which distance applies. This is undocumented as of 19 October 2026; pin your version.
Automated tests can run with no speed estimate, as ours did. So check which distance your test browser used before you trust its lazy loading counts.
Images without dimensions
MDN and web.dev disagree here. MDN says lazy images that do not intersect a visible part of an element will never load, because unloaded images are 0 by 0. web.dev says a gallery of 0 by 0 images may make the browser load everything.
Our test sided with web.dev on Chromium 141. A column of 24 lazy cards with no width or height collapsed to about 1,140 pixels and fetched all 24 images before any scroll. With width and height set, the same page fetched 4 on emulated 4G and 8 with no speed estimate.
Default image priority
We logged the priority Chromium gave each image through its DevTools protocol, the interface that test tools use to inspect the browser. The fetchpriority high image started at High. The next four large images started at Medium, so five in all got a head start. The rest started at Low, as did the image marked fetchpriority low.
That matches web.dev, which says that as of Chrome 117 the first five large images start at Medium. Images in the viewport are later raised to High at layout time, and we saw one such change. As of 19 October 2026, MDN data lists fetchpriority in Firefox 132 and Safari 17.2 or later.
When should you use sizes auto for lazy images?
Use sizes="auto" on lazy images whose display width is hard to write as a media query (a CSS screen-size test), such as cards in a flexible grid. The browser then uses the real layout width, which it already knows by the time a lazy image loads.
MDN says auto is valid only together with loading="lazy". If auto cannot resolve, the browser falls back to the other sizes entries, then to width and height, then to a default 300 by 150 pixels. So write auto followed by your normal sizes value, and set width and height as MDN advises.
In our test, a lazy image in a 200 pixel box on a 1,440 pixel desktop fetched w320 with sizes auto. With only 100vw it fetched w1920. An eager image with sizes auto also fetched w1920, because auto does not apply to eager images.
As of 19 October 2026, MDN browser data (version 8.1.6) lists sizes auto in Chrome 126, Firefox 150 and Safari 27. Safari support is marked partial because of a bug with SVG images that also use object-fit, the CSS rule for how an image fills its box.
How should a website build an image pipeline?
Build the pipeline once, on the server side. Store the original, make every size and format in a background worker, save the dimensions, and serve the results from a CDN. Pages then only build srcset lists.
A CDN, or content delivery network, is a set of servers spread across many cities that keep copies of your files close to visitors. A shopper in Kolkata gets the saree photo from a nearby server, not from the origin far away.
Follow one upload through the diagram below. Notice that the worker writes two things: image files to storage, and facts about the image to the database. The page needs both.
The marketplace image pipeline
Drag to exploreEvery upload becomes up to 15 files: five widths in three formats. Notice the worker also writes width, height and alt text to the database.
Diagram connections:
- Catalogue upload to Original photos
- Original photos to Resize and encode worker (New upload event)
- Resize and encode worker to Resized variants
- Resize and encode worker to Image details
- Resized variants to CDN
- CDN to Shopper's browser
- Catalogue upload: Full-size camera original
- Original photos: Kept forever, never served to shoppers
- Resize and encode worker: Makes 5 widths in AVIF, WebP and JPEG
- Resized variants: Files named by product, width and format
- Image details: Width, height, alt text, widths made
- CDN: Caches variants near shoppers
- Shopper's browser: Picks one file from srcset
Keep originals apart
Move the camera files into their own storage bucket. The website never links to them directly.
Generate variants in the background
A worker makes widths from 320 to 1,920 pixels in AVIF, WebP and JPEG. It skips any width larger than the original.
Save the facts
The worker stores width, height and the list of widths made. The alt text sits in the same record.
Serve through a CDN with long caching
Each file name includes a version, so a file never changes. A new photo gets a new name, and caches can keep files for a year.
Build pages from the facts
The page code calls buildSrcset and cardImageAttrs. No page ever needs to know about the camera original.
Plan worker capacity for AVIF. We timed a 1,280 by 1,600 photo with Pillow 13, using libavif at speed 6. It took about 30 to 50 ms as JPEG and 1.6 to 2.7 seconds as AVIF. Those numbers depend on the encoder speed setting and the CPU.
Some teams resize on the fly instead. The CDN or an image service makes a size the first time someone asks, then caches it. This saves storage and work for photos nobody views, but the first visitor waits for the encode.
Either way, watch out for open-ended size parameters. If any width can be requested, someone can ask for thousands of odd sizes and fill your cache. Allow only the widths in your list.
Here is one visit to the category page on a phone, before and after. The sizes are made up; the count of fetched images comes from our Chromium test with 4G emulated.
| Measure | Before | After |
|---|---|---|
| Photo format | JPEG camera originals | AVIF, with WebP and JPEG fallbacks |
| File per product card | About 3.5 MB at 4,000 pixels | About 90 KB at 1,280 pixels |
| Images fetched before any scrolling | All 24 | 4, the eager cards; 8 with no speed estimate |
| Image bytes before any scrolling | About 84 MB | About 0.36 MB (0.7 MB with no speed estimate) |
| Text jumping as photos arrive | Yes, on every card | No, space is reserved |
How do you explain image optimization in a frontend system design interview?
Explain it as a pipeline plus page rules: how images are stored and resized, then how each page chooses format, size and loading for every image. Tie each choice to a number the interviewer cares about.
Ask about the images first
Ask who uploads them, how big the originals are, how many appear per page, and what share of visitors use phones.
Sketch the pipeline
Originals in storage, a worker that makes sizes and formats, a store of dimensions and alt text, and a CDN in front.
Pick formats by content
AVIF or WebP for photos with a JPEG fallback through the picture element. SVG for icons and logos.
Serve the right size
Explain srcset with w descriptors and an honest sizes value. Mention pixel density with one quick calculation.
Plan loading order
Eager and high priority for the hero, eager for the rest of the first screen, lazy for everything below.
Prevent layout shift
Width and height on every image, from the stored dimensions, with a placeholder colour in the reserved space.
Measure in the field
Track image bytes per page, LCP and layout shift from real visitors, split by phone and desktop.
Questions people ask
- Is AVIF better than WebP?
- For file size, usually yes. MDN describes lossy AVIF as around half the size of JPEG, and lossy WebP as 25 to 35 percent smaller. In our test on four photos, AVIF saved 38 to 53 percent and WebP 20 to 34 percent. AVIF is much slower to encode, so offer it first inside a picture element, then WebP, then JPEG.
- Should I lazy load all images?
- No. Lazy load only images below the first screen. Images visible when the page opens should load eagerly, and the biggest one, often the LCP element, should also get high fetch priority. In our test a lazy hero started about 800 ms later than an eager one and painted at 1.87 seconds instead of 1.05.
- Do I still need width and height if I set the size in CSS?
- Yes. The width and height attributes let the browser work out the aspect ratio before the file arrives, so it can reserve space. CSS can still resize the image freely, for example width 100 percent and height auto. Without them, text jumps when the image lands, and lazy images may all load at once.
- What is the difference between srcset and the picture element?
- Srcset offers the same image at different widths and lets the browser pick one. The picture element lets you offer different formats, such as AVIF then WebP then JPEG, or different crops for different screens. You often use both together: a picture element whose sources each carry their own srcset.
- How many image sizes should I generate?
- Five widths, such as 320, 640, 960, 1,280 and 1,920 pixels, cover the layouts in this guide. Fewer widths mean more wasted bytes between steps. More widths mean more files to store and fewer cache hits per file. Check your real card widths and screen DPRs, then pick steps that cover them.
- How can I check which image file the browser chose?
- Read the image currentSrc in the browser console, or open the network panel and filter by images. currentSrc names the chosen file. Do not rely on naturalWidth for this, because for a srcset file with w descriptors the browser divides it by the density. In our test a 1,280 pixel file reported 390.
- Should I convert animated GIFs to video?
- Usually yes. In our test, a 4 second, 480 by 600 pixel animation was 9.95 MB as a GIF. As MP4 it was 134 KB, and as WebM 73 KB. Mark the video autoplay, muted, loop and playsinline so phones play it inside the page, and give it a poster image.
Sources and further reading
- MDN Web Docs: Responsive images (srcset, sizes, picture, art direction)
- MDN Web Docs: The img element (loading, fetchpriority, sizes auto, width and height)
- MDN Web Docs: Image file type and format guide (WebP and AVIF sizes, AVIF progressive rendering)
- caniuse: AVIF image format (browser versions as of October 2026)
- RFC 9649: WebP Image Format (IETF, Informational, November 2024)
- MDN Web Docs: The video element (poster, preload, autoplay, sources)
- web.dev: Browser-level image lazy loading for the web (distance thresholds, images without dimensions)
- Chromium 141.0.7390.37 source: Blink settings.json5 (lazy loading image distance for each connection type)
- Chromium 141.0.7390.37 source: network_information.cc (unknown and offline reported to scripts as 4g)
- web.dev: Optimize resource loading with the Fetch Priority API (default image priorities)
- WebKit blog: New video policies for iOS (playsinline, muted autoplay, GIF cost), 2016
- caniuse: Lazy loading via attribute for video and audio (support as of October 2026)