A page can display its main content quickly and still respond slowly to a click. On another page, the text might appear late or the layout might shift while images load. These problems call for different fixes, so I start by checking what visitors are actually waiting for.
The seven areas covered here include images, server response, JavaScript, CSS, fonts and delayed resource loading. Each section explains when a change is worth testing and which measurement can show whether it helped.
Before you start: measure Core Web Vitals on the right page
Core Web Vitals are three Google metrics: how fast the main content appears, how fast the page responds to interaction, and how stable the layout is. According to Google's page experience documentation, they're used by its ranking systems, but good scores don't guarantee rankings. So I work on performance for people, and I don't promise any specific rise in traffic or sales from it.
Start with representative URLs, one for each important page type:
- a landing page,
- a category,
- a product,
- an article,
- an important form or the cart.
Test the first visit and a repeat visit separately, just as you'd keep an anonymous homepage test apart from a signed-in customer's checkout.
Core Web Vitals: three different goals
In field data, compare the 75th percentile, separately for mobile and desktop, because the average and your best run will both mislead you. Aim for all three metrics to sit in the good range (web.dev).
TTFB, the time to the first byte of the response, helps you hunt for causes and sits outside the three Core Web Vitals.
Core Web Vitals triage
Enter LCP, INP, CLS and TTFB values from PageSpeed Insights, Lighthouse or CrUX to find the first bottleneck to investigate. To measure a URL, use the performance auditor.
Missing INP is missing data. According to the PageSpeed Insights documentation, the tool can pass the Core Web Vitals assessment on good LCP and CLS alone when INP data is too thin. That label doesn't tell you how clicks are handled.
Lighthouse, CrUX and your own user measurements
| Source | Use it for | Do not conclude from it |
|---|---|---|
| Lighthouse / the lab section of PageSpeed | Reproducing a page load and finding resource problems | A standard navigation test without interaction does not measure INP; TBT is not a numeric stand-in for it |
| CrUX / the field section of PageSpeed | Judging the experience of eligible Chrome users | An origin result is not the result for a specific page; no data does not mean zero |
| Your own RUM (real user monitoring) | Analysing releases, page types and interactions | The data depends on a correct implementation and need not match the CrUX population or method |
Real visits happen on other devices and connections, so the field result can look different (web.dev). When you collect your own measurements, keep form contents, customer identifiers and URL parameters with personal data out of what you send.
CrUX in PageSpeed and in the current API uses a rolling 28-day window with daily updates. After a release, new visits gradually replace old ones (CrUX API docs), and 28 days after the release the window covers only the new version. The monthly BigQuery dataset is a separate data product, so record the dates of the collection period.
Missing data means too few eligible visits or unmet conditions for inclusion. The CrUX methodology leaves out Safari and Chrome on iOS, so testing on a real iPhone makes sense even when the mobile CrUX result looks good.
Step 1: Speed up the LCP image and let the browser discover it early
First find the LCP element in the report and check it separately on mobile and desktop, since they can differ. According to web.dev, any block of text, including a heading, can be the LCP candidate.
For an image, break the wait into four parts:
If the image is still waiting for JavaScript or a stylesheet, the gain from a smaller file may never show up in the final LCP (web.dev).
Expose the image URL in the initial HTML through src or srcset. If it's hidden only in a data-src attribute that a script reads later, the browser finds it too late.
That's from the same research pack as the CLS test further down. The page was synthetic and had no network, so most of that gap is the imposed timer, but it's the same mechanism on a real site.
Never lazy-load the LCP image (loading="lazy"), because the browser will put off downloading it. If it's a resource that matters for the first screen, fetchpriority="high" helps the browser give it the right priority, but it's only a hint 8. Use it sparingly, because in bulk your resources start competing with each other.
Choose the image's dimensions, format and quality
Compare your current file with WebP and AVIF at a quality level suited to that graphic, and check fine text and edges. PNG or SVG can still suit gradients and transparency better, and web.dev warns against assuming a fixed saving.
A phone can need an image with more pixels than its CSS width, because what counts is the space in the layout and the screen density. srcset lists the candidates, and sizes describes the display width. With wrong values, the attributes do nothing (MDN).
Here's an example for an image with a maximum width of 1120 px and 20 px margins on each side on smaller screens (the file names are placeholders):
<img
class="hero-image"
src="/images/product-1200.webp"
srcset="/images/product-640.webp 640w,
/images/product-1200.webp 1200w,
/images/product-1920.webp 1920w"
sizes="(min-width: 1160px) 1120px, calc(100vw - 40px)"
width="1200"
height="800"
loading="eager"
fetchpriority="high"
alt="The product with its main compartment open"
>
.hero-image {
display: block;
width: calc(100% - 40px);
max-width: 1120px;
height: auto;
margin-inline: auto;
}
This CSS assumes a full-width parent that doesn't add padding, so in a real component match sizes to the actual layout, never the other way round.
Check the effect in the Network panel and in a Performance recording. The browser picks the right file, the request starts earlier or the transfer is smaller, and LCP doesn't get worse in comparable tests. Add a preload only once you've confirmed the image is discovered late, because unnecessary hints compete with other resources.
Step 2: Shorten the server response (TTFB) and choose a safe cache
If the delay starts before the HTML arrives, you fix it on the server, because compressing images won't touch it.
Web.dev lists the causes of a high TTFB: redirects, connection setup, distance, a cache miss and application work. More expensive hosting only treats some of them.
Check your response generation time and database queries, plus any dependencies on external APIs. Prerender content that stays the same between visits, or keep it in a response cache. Record cache hits and misses separately, because the difference tells you more than a CDN logo in your hosting panel.
Browser cache and CDN do different jobs
The browser cache lets your visitor reuse resources they've already downloaded. A shared cache, such as a CDN, can also answer someone visiting for the first time, but the result depends on what it actually stores (MDN).
| Response type | Example Cache-Control | Condition |
|---|---|---|
| Public CSS or JS with a content hash in the file name | public, max-age=31536000, immutable | Every change produces a new URL; an existing file never changes content |
| Public HTML that must be revalidated | no-cache | The response may be stored but must be validated before reuse |
| Sensitive account or payment response | private, no-store | An example of a cautious policy; CDN and application rules must respect it too |
no-store forbids storing a response in any cache the directive applies to (MDN). Keep your cart and account area out of public caching, because a cookie in the response won't protect it from a shared cache.
Check the effect on a first visit, a return visit and a version update, plus signing in and out. Verify the response headers and cache hits, confirm that new files appear after deployment, and make sure users cannot receive another person's content. You can't recall a year-long cache later just by changing the header on the server.
Step 3: Cut unneeded JavaScript and CSS, then check compression
Your biggest win is code the page downloads without needing it. An article page might load an editor, a charting library or form handling that's only used elsewhere. Splitting bundles by page and feature cuts the work done at start-up.
When I deferred the stylesheets on cometweb.io, first paint improved by about 0.1 s, but LCP did not change. The edit had not addressed the main delay.
JavaScript was the brake, so I cut the glossary and the meta table out of the code that loads before first render. JS before first render then dropped from 329 to 208 KB (gzipped), and LCP in a Lighthouse lab test (simulated mobile profile) went from 6.39 to 5.46 s. I walk through the diagnosis in my comparison of lab data and CrUX.
The Coverage panel in DevTools only shows unused code for the scenario you recorded (docs). Before you delete rules, open the menu, trigger a form error and check the mobile version.
You can inline a small amount of critical CSS in the HTML, but don't move the whole stylesheet there, because you'll bloat the document and lose separate caching. Don't defer the styles the first screen needs either, or the visitor sees unstyled content or a layout shift 7.
defer moves script execution, but the cost stays
For an external classic script that depends on the document, this is often useful:
<script src="/assets/menu.abc123.js" defer></script>
defer lets the browser download the script while it parses the HTML and run it later, keeping the order of deferred scripts. async runs a script as soon as it has downloaded, without that guarantee. Scripts with type="module" are deferred by default (MDN).
After the change, check your parsing and execution cost. A script can stop blocking the parser and then block the main thread when your visitor clicks the menu.
Report names: render-blocking resources, excessive DOM, enormous network payload
- Render-blocking resources are stylesheets and scripts that hold up rendering of the main content. You fix them with the steps in this section.
- Excessive DOM means too many HTML elements. A large tree raises the cost of every layout change and can make clicks feel slower (web.dev), so simplify nesting, remove duplicate copies of components and limit the lists you render up front.
- Enormous network payload means too much data for the first view. Compressed images help, as do fewer fonts and libraries and loading below-the-fold content on demand (web.dev).
The last two are easy to confuse. A page with a small transfer can still have a huge DOM generated on the client, and then compression changes nothing.
Minification and compression are separate steps
Minification shortens how the code is written, and HTTP compression, such as Brotli or gzip, shrinks the transferred text response. You still have to remove the feature you don't need, and don't attach a "20% faster site" promise to either of them 9.