Why Gupta Handloom House Lost Its Customers
Mr. Gupta runs a handloom saree shop in a small town. His nephew built him a website with a product page for each saree. The photos were beautiful, straight from a good camera, each one about 5 MB. On the shop’s Wi-Fi the page looked fine.
Then real customers arrived, mostly on phones with 4G that drops to one bar inside the market. The page showed a white screen for several seconds. The text jumped around as photos appeared. People pressed the back button and bought from a bigger shopping app instead.
Speed matters for two reasons. First, people leave slow pages: every extra second of waiting means fewer visitors stay. Second, Google measures speed. Its Core Web Vitals (loading speed, how quickly the page responds, and how much the layout jumps) are part of how it ranks pages in search. In this lesson you will fix Mr. Gupta’s page using only HTML attributes. No new tools, no framework.
Lazy Loading Images: loading="lazy"
By default, the browser downloads every image on the page as soon as it can, even images far below the screen that the user may never scroll to. That wastes data and slows down the images that matter.
Adding loading="lazy" tells the browser: “do not download this image until the user scrolls near it”. Run this example, open the Network tab in DevTools if you like, and scroll down slowly.
<h2>Handloom Sarees</h2>
<p>Scroll down to see more designs.</p>
<!-- The first image is near the top, so it loads normally -->
<img src="https://picsum.photos/id/292/800/500" width="800" height="500"
alt="Cotton saree in mustard yellow with a red border">
<p style="height: 900px;">Our full collection is below. Keep scrolling.</p>
<!-- These are far down the page, so the browser waits until the user scrolls near them -->
<img src="https://picsum.photos/id/312/800/500" width="800" height="500" loading="lazy"
alt="Silk saree in deep green">
<img src="https://picsum.photos/id/326/800/500" width="800" height="500" loading="lazy"
alt="Printed saree in sky blue">
Notice that the first image has no loading="lazy". Images that are visible when the page opens should load normally. The value loading="eager" is the default and means “load now”.
Lazy loading iframes
The same attribute works on <iframe>. Embedded maps and videos are heavy, because each one loads a whole page with its own scripts. Lazy loading them is often the single biggest saving on a page.
<h2>Find Our Shop</h2>
<p>Gupta Handloom House, Station Road. The map loads only when you scroll to it.</p>
<iframe src="https://www.openstreetmap.org/export/embed.html?bbox=77.20,28.60,77.24,28.63&layer=mapnik"
width="400" height="300" loading="lazy"
title="Map showing the location of Gupta Handloom House"></iframe>
width and height: Stop the Page from Jumping
When the browser reads an <img> without a size, it does not know how much space to keep. It draws the text first, then pushes it down when the image arrives. This jump is called layout shift, and Google measures it as CLS (Cumulative Layout Shift). It is also why people tap the wrong button on a jumping page.
Add width and height with the real pixel size of the image. Modern browsers use them to work out the aspect ratio (the shape, such as 3:2) and reserve the right space, even when CSS makes the image smaller with max-width: 100% and height: auto.
<style>
img { max-width: 100%; height: auto; display: block; }
</style>
<h3>Without width and height</h3>
<img src="https://picsum.photos/id/225/600/400" alt="Tea leaves in a wooden bowl">
<p>This text jumps down when the image above finishes loading.</p>
<h3>With width and height</h3>
<img src="https://picsum.photos/id/225/600/400" width="600" height="400" alt="Tea leaves in a wooden bowl">
<p>This text stays still, because the browser kept the right space ready.</p>
The jump is easiest to see on a slow connection. In Chrome DevTools, open the Network tab, change No throttling to Slow 4G and reload. This is also a good way to feel what Mr. Gupta’s customers feel.
Responsive Images: srcset and sizes
A phone screen about 400 pixels wide does not need a 1600 pixel photo. With srcset you list several sizes of the same image, and the browser picks the smallest one that still looks sharp.
srcsetlists each file with its real width, usingw:photo-400.jpg 400w.sizestells the browser how wide the image will be shown.(max-width: 600px) 100vw, 600pxmeans: “on screens up to 600 pixels wide, the image fills the screen width; otherwise it is 600 pixels wide”.srcstays as the fallback for very old browsers.
<style>
img { max-width: 100%; height: auto; }
</style>
<img
src="https://picsum.photos/id/292/800/500"
srcset="https://picsum.photos/id/292/400/250 400w,
https://picsum.photos/id/292/800/500 800w,
https://picsum.photos/id/292/1600/1000 1600w"
sizes="(max-width: 600px) 100vw, 600px"
width="800" height="500"
alt="Cotton saree in mustard yellow with a red border">
<p>Right-click the image and choose "Open image in new tab" to see which size your browser picked.</p>
The browser also considers the screen’s pixel density. A phone with a sharp screen may choose the 800w file even though the image is shown 400 pixels wide. That is correct behaviour: you give the choices, the browser decides.
Modern Formats: picture with WebP and AVIF
WebP and AVIF are newer image formats. For the same photo they are usually much smaller than JPEG at similar quality. The <picture> element lets you offer them while keeping JPEG for any browser that cannot show them. The browser reads the <source> elements from top to bottom and uses the first type it supports. The <img> inside is required: it holds the alt, the size and the fallback.
On a real site you would create each format with an image tool and save the files next to your page:
<picture>
<source type="image/avif" srcset="saree-800.avif">
<source type="image/webp" srcset="saree-800.webp">
<img src="saree-800.jpg" width="800" height="500"
alt="Cotton saree in mustard yellow with a red border">
</picture>
Those files are not available inside the playground, so this runnable version uses the WebP and JPG files that the picsum.photos service provides (it has no AVIF files, so there is one source here):
<style>
img { max-width: 100%; height: auto; }
</style>
<picture>
<source type="image/webp" srcset="https://picsum.photos/id/292/800/500.webp">
<img src="https://picsum.photos/id/292/800/500.jpg" width="800" height="500"
alt="Cotton saree in mustard yellow with a red border">
</picture>
fetchpriority="high" for the Main Image
Google measures LCP (Largest Contentful Paint): the time until the biggest thing on the screen, usually the main photo, appears. On a product page, that photo is what the customer came to see. fetchpriority="high" asks the browser to download it before other, less important files. Use it on one or two images at most. If everything is high priority, nothing is.
Loading Scripts: defer vs async
A normal <script src> in the head blocks the page. The browser stops reading HTML, downloads the script, runs it, and only then continues. On a slow connection the customer stares at a blank screen.
| How you write it | Downloads | Runs | Good for |
|---|---|---|---|
<script src> | Stops the HTML | Immediately | Rarely the right choice in the head |
<script src defer> | In the background | After the HTML is read, in order | Your own site scripts |
<script src async> | In the background | As soon as it arrives, any order | Independent scripts like a visit counter |
<head>
<!-- Blocks the page: the browser stops reading HTML until this downloads and runs -->
<script src="old-slider.js"></script>
<!-- defer: downloads in the background, runs in order after the HTML is read -->
<script src="cart.js" defer></script>
<script src="product-gallery.js" defer></script>
<!-- async: downloads in the background, runs as soon as it arrives, in any order -->
<script src="https://analytics.example.com/counter.js" async></script>
</head>
Why does order matter? If product-gallery.js uses a function from cart.js, both need defer so they run in the order written. With async, the gallery could run first and fail. The defer and async attributes only work on scripts with a src. This demo shows why a script needs the HTML to be ready. Open the console after running it:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Defer demo</title>
<script>
// This normal inline script runs before the body exists
console.log('In head, price element found:', document.getElementById('price') !== null);
</script>
</head>
<body>
<p id="price">Rs. 1,250</p>
<script>
// Here the HTML above is ready, just like a deferred script would see it
console.log('After body, price element found:', document.getElementById('price') !== null);
document.addEventListener('DOMContentLoaded', () => {
console.log('DOMContentLoaded: deferred scripts run just before this moment');
});
</script>
</body>
</html>
Resource Hints: preconnect and preload
Before downloading anything from another server, the browser must look up the address and set up a secure connection. On mobile networks this can take a noticeable moment. Two <link> tags in the head help:
rel="preconnect"opens the connection to another server early, such as the server that holds your images or fonts.rel="preload"starts downloading a specific file early, such as a font that the CSS will only ask for later. Theasattribute says what kind of file it is. Fonts also needcrossorigin.
<head>
<meta charset="UTF-8">
<title>Handloom Sarees | Gupta Handloom House</title>
<!-- preconnect: open the connection to the image server early -->
<link rel="preconnect" href="https://images.example.com">
<!-- preload: fetch this file now, it is needed very soon -->
<link rel="preload" href="/fonts/shop-heading.woff2" as="font" type="font/woff2" crossorigin>
<link rel="stylesheet" href="/css/shop.css">
<script src="/js/cart.js" defer></script>
</head>
Use both sparingly. Preloading files the page does not really need just steals bandwidth from the files it does need.
Real-Life Example: Mr. Gupta’s Fast Product Page
Here is the saree page with every technique from this lesson: a responsive main image with high priority, fixed sizes, and lazy “You May Also Like” images.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Cotton Saree, Mustard Yellow | Gupta Handloom House</title>
<link rel="preconnect" href="https://picsum.photos">
<style>
body { font-family: sans-serif; max-width: 640px; margin: auto; padding: 12px; }
img { max-width: 100%; height: auto; display: block; }
.more { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
</style>
</head>
<body>
<h1>Cotton Saree, Mustard Yellow</h1>
<!-- Main product image: the most important picture, so it loads first -->
<img
src="https://picsum.photos/id/292/800/500"
srcset="https://picsum.photos/id/292/400/250 400w,
https://picsum.photos/id/292/800/500 800w"
sizes="(max-width: 640px) 100vw, 640px"
width="800" height="500"
fetchpriority="high"
alt="Mustard yellow cotton saree with a red woven border">
<p><strong>Price: ₹1,250</strong>. Handwoven in Chanderi. Free delivery within the city.</p>
<button type="button" id="add">Add to cart</button>
<p id="msg" role="status"></p>
<h2>You May Also Like</h2>
<div class="more">
<img src="https://picsum.photos/id/312/400/300" width="400" height="300" loading="lazy" alt="Green silk saree">
<img src="https://picsum.photos/id/326/400/300" width="400" height="300" loading="lazy" alt="Blue printed saree">
<img src="https://picsum.photos/id/360/400/300" width="400" height="300" loading="lazy" alt="Pink cotton saree">
<img src="https://picsum.photos/id/365/400/300" width="400" height="300" loading="lazy" alt="White saree with gold border">
</div>
<script>
document.getElementById('add').addEventListener('click', () => {
document.getElementById('msg').textContent = 'Added to cart.';
});
</script>
</body>
</html>
Try these changes:
- Add a
1600woption to the main image’ssrcset. - Remove the
widthandheightfrom one small image and reload with Slow 4G throttling. Watch the grid jump. - Add a lazy-loaded map
<iframe>at the bottom with a cleartitle.
Measuring with Lighthouse
Do not guess about speed: measure it. Chrome has a free tool called Lighthouse built into DevTools.
- Open your page in Chrome and press F12 (or Cmd + Option + I on a Mac).
- Open the Lighthouse tab. Choose Mobile and tick Performance.
- Click Analyze page load. After a short wait you get a score out of 100, your LCP and CLS values, and a list of suggestions, such as “Properly size images”.
Run it in an Incognito window so browser extensions do not affect the result, and run it more than once, because scores change a little each time.
Common Mistakes
- Lazy loading the main image. The most important picture at the top then waits, and LCP gets worse. Fix: never lazy load images visible when the page opens, and give the main one
fetchpriority="high". - Wrong width and height values. If they do not match the real shape of the image, it looks stretched. Fix: use the real pixel size of the file.
- Using async for scripts that depend on each other. Fix: use
deferwhen order matters. - Uploading camera photos directly. A 5 MB photo shown 400 pixels wide wastes the customer’s data. Fix: resize and compress before uploading, and offer WebP or AVIF.
Practice Task: Speed Up a Sweet Shop Page
Annapurna Mithai Bhandar wants its Kaju Katli page to load fast before Diwali. Fix the starter page so that (the sample photos will not match the alt text, which is fine for practice):
- The head has a
preconnecttohttps://picsum.photos - The offer banner script no longer blocks the page
- The main image has
width,heightandfetchpriority="high" - The two smaller images have
width,heightandloading="lazy"
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Fresh Sweets | Annapurna Mithai Bhandar</title>
<!-- TODO 1: preconnect to https://picsum.photos -->
<script src="https://cdn.example.com/offer-banner.js"></script> <!-- TODO 2: make this load without blocking -->
</head>
<body>
<h1>Kaju Katli, 500 g</h1>
<!-- TODO 3: add width, height and fetchpriority to the main image -->
<img src="https://picsum.photos/id/429/800/500" alt="Kaju katli pieces arranged on a silver plate">
<h2>More Sweets</h2>
<!-- TODO 4: add width, height and lazy loading to these -->
<img src="https://picsum.photos/id/431/400/300" alt="Box of motichoor laddoos">
<img src="https://picsum.photos/id/435/400/300" alt="Plate of gulab jamun">
</body>
</html>
Show one possible answer (try it yourself first!)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Fresh Sweets | Annapurna Mithai Bhandar</title>
<link rel="preconnect" href="https://picsum.photos">
<script src="https://cdn.example.com/offer-banner.js" defer></script>
<style>
img { max-width: 100%; height: auto; }
</style>
</head>
<body>
<h1>Kaju Katli, 500 g</h1>
<img src="https://picsum.photos/id/429/800/500" width="800" height="500" fetchpriority="high"
alt="Kaju katli pieces arranged on a silver plate">
<h2>More Sweets</h2>
<img src="https://picsum.photos/id/431/400/300" width="400" height="300" loading="lazy"
alt="Box of motichoor laddoos">
<img src="https://picsum.photos/id/435/400/300" width="400" height="300" loading="lazy"
alt="Plate of gulab jamun">
</body>
</html>
Key Takeaways
- Slow pages lose visitors and rank lower, because Google uses Core Web Vitals such as LCP and CLS.
- Use
loading="lazy"for images and iframes below the first screen, never for the main image. - Always set
widthandheightto prevent layout shift. - Use
srcset,sizesand<picture>to send smaller, modern images to each device. - Load your own scripts with
defer, independent ones withasync, and usepreconnectandpreloadsparingly. - Measure with Lighthouse in Chrome DevTools before and after every change.