Performance for a mid-range phone on mobile data
A static HTML and CSS site is the fastest thing on the web by default. You have no framework, no database, no server-side rendering. So performance work here is almost entirely about one thing — not sending more bytes than you need — and the biggest single decision is images.
The audience this course keeps naming is a mid-range Android on mobile data in Pune. On office wifi you cannot see what they see, which is why the measurements matter more than the rules.
The three numbers
Google's Core Web Vitals are what Lighthouse reports and what search ranking uses. Three, and each has one main cause on a static site.
LCP — Largest Contentful Paint. When the biggest visible thing finishes rendering. Target: under 2.5 seconds. On a static site the LCP element is almost always your hero image or your biggest heading. If it is an image, its size is your LCP.
CLS — Cumulative Layout Shift. How much the page jumps as it loads. Target: under 0.1. Caused by images with no dimensions, and by fonts swapping. This is the one that makes you tap the wrong link.
INP — Interaction to Next Paint. How long the page takes to respond to a tap. Target: under 200ms. Almost never a problem on a static site, because there is no JavaScript blocking the main thread. Worth knowing so you recognise it.
Run Lighthouse on a throttled connection and it reports all three. Unthrottled numbers from your laptop are meaningless — you will score 100 and your visitor will wait four seconds.
Images are the whole game
Sort the Network tab by size. The top rows are images, usually by a factor of ten over everything else. So:
Resize the original. The single most common cause of a slow page is a 4000-pixel phone photograph dropped straight in. Roughly twice the largest displayed size is enough — past that, the file grows and nobody can see a difference.
Use a modern format. AVIF is about half a JPEG; WebP about 70%. Both are supported
everywhere current, with <picture> from module 4 for the fallback.
Serve the right size with srcset, and get sizes right — module 4's point, and the
commonest error is leaving sizes at its 100vw default for an image displayed in a 300px
column.
Never a PNG of a photograph. It can be five times the JPEG for no visible benefit.
SVG for anything drawn. A logo as SVG is 2–5 KB and sharp at any size.
loading="lazy" below the fold, fetchpriority="high" on the hero. And never lazy-load
the hero; you would be delaying the one image the visitor came for.
A rough budget worth holding: a hero under 150 KB, other images under 80 KB, and the whole page under about 500 KB. Those are not laws, and if you are ten times over one of them you have found your problem.
Fonts are second
From module 2's typography lesson, as a performance matter:
system-ui costs zero bytes. It is already on the device and renders on the first paint.
For a small business site this is very often the right answer, and the fastest possible one.
If you do load a font:
One family, two weights. A variable font is one file for every weight, usually smaller than three static ones — about 25–40 KB Latin-subset.
woff2 only. The eot/ttf lists in old tutorials are for Internet Explorer.
Self-host. A font CDN is an extra DNS lookup and connection before the font is even requested, and the shared-cache benefit stopped existing when browsers partitioned caches by site.
font-display: swap, or the text is invisible for up to three seconds.
Preload the one font on your critical path, with crossorigin — without it the file
downloads twice:
<link rel="preload" href="/fonts/inter.woff2" as="font" type="font/woff2" crossorigin>
Subset it. unicode-range means only the characters the page needs are downloaded — and
check U+20B9 is included, or ₹ renders in a fallback font, which looks visibly wrong on a
price list.
And the CLS half: a font swap moves the page unless the fallback occupies the same space.
size-adjust, ascent-override and descent-override fix that, and are worth the ten
minutes for one font.
CSS
Small, and there are two things worth knowing.
Your stylesheet is render-blocking. From module 1: the browser will not paint until it has your CSS, deliberately, because painting first would show unstyled content and then jump. So a slow stylesheet is a blank screen.
Which means one CSS file, not ten @imports. Plain @import is a serial round trip —
the browser fetches main.css, parses it, discovers the imports, then fetches each. Eleven
round trips before paint, and on mobile data a round trip is 100–300ms. Concatenate for
production, as the naming lesson said.
Delete dead CSS. The Coverage panel from the previous lesson tells you how much of each file was never used. A stylesheet at 15% coverage is sending nine times more than it needs.
Beyond that, do not micro-optimise selectors. Selector matching is not your bottleneck on a
page this size, and .a .b .c versus .c is a maintainability argument, not a performance
one.
The rendering cost, which is the other half
Bytes are download time. Rendering is what happens sixty times a second afterwards, and it is where a mid-range phone actually struggles.
From module 6, because it belongs here too:
Animate transform and opacity. Not width, height, top, left, margin or
padding. Those trigger layout on every frame. Record in the Performance panel and look for
purple Layout bars; a cheap animation has none.
backdrop-filter is expensive. The frosted-glass sticky header is noticeably slow on a
mid-range phone. One small element, never something large or animated.
Large box-shadow blurs cost paint time. A 60px blur across a full-width element is
real work. Two subtle layers, as module 2 said, are both prettier and cheaper.
will-change costs memory and creates a stacking context. Use it on the one thing you are
about to animate, and remove it afterwards.
Measuring honestly
Four things, in order of how much they change what you build:
Throttle. Network to Slow 4G, CPU to 4×, in devtools. Then reload. This is the single most useful habit in this lesson, and it will change decisions you thought were fine.
Lighthouse, on the deployed URL. Localhost has no network latency, so it flatters you. Run it against the real site.
The Network tab, sorted by size. Find your biggest file and justify it. If you cannot, that is your work.
A real phone on mobile data. Module 4's trick — bind the server to 0.0.0.0 and open it
on your phone. Nothing else tells you how it feels.
What good looks like for a site like the capstone
HTML 5–15 KB
CSS 10–30 KB
Fonts 0 KB (system-ui) or 30–50 KB
Hero image 80–150 KB
Other images 40–80 KB each
Total, first view under 500 KB
Requests under 20
LCP on Slow 4G under 2.5s
A small business site exceeding any of those by much has something specific wrong, and it is nearly always one image.
The things not to do
Do not add a framework for a static site. No React, no jQuery, no CSS framework. You have spent seven modules learning to do it in HTML and CSS; adding 40 KB of JavaScript to a page with no interactivity is pure cost.
Do not add analytics you will not read. It is script, cookies and a consent banner. If you want numbers, something lightweight and privacy-respecting costs a fraction of Google Analytics.
Do not embed a YouTube player for a two-minute video. Roughly a megabyte of JavaScript before a frame plays. Self-host it, or use module 1's static-image-and-link pattern.
Do not embed a live map when a static image linking to the map app is faster, more private, and what the visitor wanted anyway.
Do not optimise what you have not measured. Most performance advice on the web is about problems a static site does not have. Throttle, look at the Network tab, and fix the biggest row.
Check your work
The three Core Web Vitals and their targets. LCP under 2.5s, CLS under 0.1, INP under 200ms.
What LCP usually is on a static site. The hero image — so its size is your LCP.
What causes CLS. Images with no dimensions, and font swaps.
Why INP is rarely a problem here. No JavaScript blocking the main thread.
Why untrottled Lighthouse numbers are meaningless. No latency, and a fast CPU.
The commonest cause of a slow page. An unresized photograph.
Roughly how much to resize to. Twice the largest displayed size.
What system-ui costs. Nothing — it is already on the device.
What happens to a preloaded font with no crossorigin. It downloads twice.
Which character to check in a font subset. ₹, U+20B9.
Why one CSS file rather than ten imports. @import is a serial round trip, and CSS is
render-blocking.
What not to micro-optimise. Selectors.
Which properties are cheap to animate. transform and opacity.
Three rendering costs worth knowing. backdrop-filter, large shadow blurs, and
will-change memory.
The single most useful habit. Throttle to Slow 4G and 4× CPU, then reload.
Four things not to add. A framework, unread analytics, a video embed, a live map.
Practice
- Run Lighthouse on your page unthrottled, then on Slow 4G with 4× CPU. Compare the scores.
- Sort the Network tab by size and write down your three biggest files.
- Take your largest image, check its pixel dimensions against its displayed size, and resize it. Measure the saving.
- Save the same photograph as PNG, JPEG, WebP and AVIF. Compare all four.
- Add
srcsetwithsizes="100vw"for an image displayed in a 300px column, note which file is fetched, then fixsizes. - Identify your LCP element in the Lighthouse report. If it is an image, halve its size and re-measure.
- Remove
widthandheightfrom your images, throttle, and read the CLS. Add them back. - Build a page with
system-uiand measure the total weight. Then add a web font and measure again. - Preload a font without
crossoriginand count the requests. - Put
₹450on a page with a Latin-subset font and check in devtools which font rendered the₹. - Split your CSS into eight
@imports, throttle, and time the first paint. Then concatenate. - Run the Coverage panel and note your unused CSS percentage.
- Animate a panel with
left, record it in the Performance panel, and count the Layout bars. Then usetranslateX. - Add
backdrop-filter: blur(10px)to a sticky header and scroll on a real phone with 4× CPU throttling. - Compare a 60px single shadow with two subtle layers, in the Performance panel's paint profiling.
- Replace a YouTube embed with the static-image-and-link pattern and compare total requests and bytes.
- Open your deployed site on a real phone on mobile data and time it with a stopwatch.
Official documentation
- web.dev — Core Web Vitals — What LCP, CLS and INP measure and the thresholds.
- web.dev — Optimize LCP — Including the image-specific causes, which are the ones that apply here.
- web.dev — Optimize CLS — Image dimensions and font swaps, in detail.
- web.dev — Best practices for fonts — Preloading, subsetting and measuring the swap.
- MDN — Responsive images —
srcsetandsizes, which is most of the saving. - Chrome DevTools — Lighthouse overview — What each audit measures.
- Chrome DevTools — Network throttling — Setting up the honest test.
Next: reviewing HTML and CSS, which is different from reviewing other code.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship