Skip to main content

How to speed up a page: 7 CWV fixes

Seven measured fixes: LCP images, caching, JavaScript, INP, fonts, CLS and lazy loading. How to find what visitors wait for, and how to check a fix worked.

Maciej Zmitrukiewicz Founder, CometWeb

I build CometWeb Insight, a website audit app that ends with a ranked to-do list. I write about what I measure on real sites: performance, SEO, accessibility and CO₂e.

Published: Updated:
20 min read

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

Core Web Vitals table with good thresholds: LCP up to 2.5 s, INP up to 200 ms, CLS up to 0.1. Thresholds apply at the 75th percentile, separately for mobile and desktop.
Each metric describes a different kind of waiting, so first work out which one falls outside the good range.web.dev: Web Vitals, checked 24 Sep 2026

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

SourceUse it forDo not conclude from it
Lighthouse / the lab section of PageSpeedReproducing a page load and finding resource problemsA standard navigation test without interaction does not measure INP; TBT is not a numeric stand-in for it
CrUX / the field section of PageSpeedJudging the experience of eligible Chrome usersAn 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 interactionsThe 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:

Four consecutive parts of LCP time for an image: first byte of HTML, request start, transfer and render. A smaller file only shortens the transfer.
A smaller file shortens one part out of four, so first check which one is the longest.web.dev: Optimize Largest Contentful Paint

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.

Comparison of median LCP over 5 trials at a 390 px viewport: an image inserted by a script after 650 ms appeared at 704 ms, the same image with its URL in the initial HTML at 60 ms.
With its URL in the initial HTML, the image appeared at 60 ms; the same image inserted by a script after 650 ms appeared only at 704 ms.Research pack, local lab, 13 Sep 2026, 5 trials

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 typeExample Cache-ControlCondition
Public CSS or JS with a content hash in the file namepublic, max-age=31536000, immutableEvery change produces a new URL; an existing file never changes content
Public HTML that must be revalidatedno-cacheThe response may be stored but must be validated before reuse
Sensitive account or payment responseprivate, no-storeAn 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.

In the Network panel, check the 0 header and compare the transferred size with the decoded size. WOFF2 files and modern image formats carry their own compression, so if such a file has no extra br, that's usually fine.

Check the effect in a load recording, which should show less unneeded code on the page in question, with no regressions in appearance or behaviour. Compare compressed sizes, because the files in your project folder show a different number.

Step 4: Improve responsiveness to clicks (INP): long tasks and third-party scripts

INP measures how quickly the page responds to an interaction. According to web.dev, it counts the wait before the handler runs, the handler itself and the presentation of the next frame.

Your page can show a product quickly and then freeze when someone picks a size. Record specific actions in your Performance panel: opening the menu, applying a filter, adding to the cart, validating a form. Look for heavy event handlers, repeated layout work and tasks that hog the main thread (web.dev).

Split your work into chunks and give the browser time to handle input and render in between:

async function yieldToBrowser() {
  if (globalThis.scheduler?.yield) {
    await globalThis.scheduler.yield();
    return;
  }
  await new Promise(resolve => setTimeout(resolve, 0));
}

Call it inside the heavy loop, between chunks of work. await Promise.resolve() won't do the job here, because it doesn't yield to a new task. A single very expensive operation has to get cheaper or run differently. The 50 ms threshold in the guide to long tasks defines a long task, and you set your interaction budget separately.

Spread your marketing scripts out over time

Chat widgets, video players, maps, analytics and A/B tests add both downloads and execution work. List the vendors, the pages where each one is needed and the moment each one starts.

Push everything to the first interaction and you can overload the browser at exactly that moment. Also test the state after consent is given, when extra integrations appear (web.dev). With long lists, test virtualisation on each one separately, including with a keyboard and a screen reader.

Check the effect. The interaction you targeted runs more smoothly in a repeatable scenario, the result of the operation is correct, and your own interaction measurements show no regression. A lower TBT in a load test is a useful signal, though it doesn't settle any particular INP 2.

Step 5: Limit fonts and choose how text is displayed

Download only the variants you use, in WOFF2, and decide on purpose what readers see before they arrive. In my 13-font test from the intro, WOFF2 only beat plain Brotli for TrueType outlines. I wrote up the full measurement in the WOFF2 vs TTF comparison.

Bar chart of size reduction for 13 fonts converted to WOFF2: smallest 42.6%, median 54.7%, largest 70.8%. The Athelas.ttc collection at 94.5% was dropped from the sample.
More than half the fonts lost more than half their size, but that's file size; the time to show text needs its own measurement.WOFF2 vs TTF research pack, 13 fonts, September 2026

Write down which families and weights you actually use, and check whether your fonts come from your own server and an external stylesheet at the same time. According to web.dev, WOFF2 is the right default format for modern browsers. Compare a variable font with the full set of variants you need.

When subsetting, meaning cutting unused characters out of a font, keep every character from your content and prices, plus names and translations. Beyond the basic alphabet, you'll need characters such as €, curly quotes “ ”, the en dash – and the ellipsis …, plus any accented letters your languages use.

font-display: swap shows a fallback font quickly, but the later swap can change text widths. optional may keep the fallback for the whole visit (MDN). Match the fallback font's metrics, and preload a family only after measuring, with a matching request mode (crossorigin).

Check the effect in the Network panel and on the finished page. Nothing downloads twice, and the text has the right characters and variants and stays still. A smaller font file is a failure if part of the content now renders in a random fallback.

Step 6: Reserve space for images, ads and messages (CLS)

CLS describes layout stability, meaning whether page elements move without warning. According to web.dev, the score is calculated from unexpected shifts within session windows, including shifts after the page first appears.

Give images width and height values that match the resource, and keep the right aspect ratio in CSS. For embeds and ads, reserve space before the content arrives, allowing for different ad sizes and mobile crops (web.dev).

Check the consent banner, form errors, cart updates and messages above product lists. Don't hide information your visitor needs just to improve a number.

A controlled example: width and height took CLS to zero without a faster image

I built two variants of a local test page. In both, the image appeared after a fixed delay of 650 ms, but only one had width and height. I ran five trials per variant and viewport size in Chromium 144.0.7559.96.

0.127 → 0.000 median CLS at 390 × 844 px, without vs with width and height ?
0.109 → 0.000 median CLS at 1280 × 900 px, without vs with width and height ?
708 / 708 ms median LCP at 390 × 844 px, without / with dimensions: the image still waited for the same timer ?
Test viewportMedian CLS without dimensionsMedian CLS with dimensionsMedian LCP: without / with dimensions
390 × 844 px0.1270.000708 / 708 ms
1280 × 900 px0.1090.000712 / 708 ms

LCP stayed at about 708 ms because the image waited for an identical timer, so I'm not claiming a speed-up.

The image was embedded as a data: URL, so the experiment left out the network, compression and CDNs. It's a demonstration of the mechanism: I didn't measure cometweb.io or real user traffic here, and I'm not promising this result for your shop. You'll find the methodology with every trial and the code in the materials for reproducing the test (in Polish; the JSON results and script are language-neutral).

Check the effect by watching the layout when resources arrive slowly, after interactions and while scrolling. One short test without shifts only tells you about that one state of the product.

Step 7: Defer resources below the first screen (lazy loading)

Use lazy loading for images and iframes outside the initial viewport. A photo further down an article gets loading="lazy" and keeps its dimensions. According to web.dev, the browser may start downloading as the resource approaches the screen.

<img
  src="/images/product-detail.webp"
  width="960"
  height="640"
  loading="lazy"
  alt="Close-up of the stitching by the product's handle"
>

A map or video player farther down the page can use a lightweight preview and an accessible play button. Measure the first interaction as well, because delaying the download can shift the wait to the user's click 10.

Check the effect across the full journey, because what counts is the transfer for the whole visit. The initial load should skip unneeded resources, while scrolling and the first use of a deferred feature still work.

How to check whether your fixes actually worked

Test one hypothesis at a time, such as "the image appears late because its URL is only added by a script", and compare runs before and after the change under the same conditions. If the result hasn't moved, check whether the measurement reproduced the problem at all, and whether another constraint still dominates 6.

For an important fix, I run at least five repetitions, though Google doesn't require it. With a handful of runs it's a rough guide with no statistical significance, but for a ship-or-hold decision it's enough.

Compare the median and the range, and don't set the slowest "before" run against the fastest "after" run.

Treat the Lighthouse composite score as secondary to the metrics (scoring rules). On four cometweb.io pages, seven runs gave me score ranges of 4 to 10 points.

In your test record, note everything that can change the result:

  • the URL and release version,
  • the viewport,
  • the tool,
  • CPU and network conditions,
  • whether cache and consent were set,
  • sign-in state,
  • the action you performed.

HAR files and logs can contain sensitive data, so remove it before you share them.

After the release, walk through the critical features (sign-in, payment, forms, navigation, accessibility) and watch your own measurements and the CrUX collection periods. Changes in traffic composition, devices and campaigns can affect these measurements. A better performance metric alone does not establish higher conversion.

Record the results before and after in the measurement and retest card (in Polish). The website quality benchmark shows how to compare like with like on scope, dates and retest conditions. The CometWeb answers hub has short, sourced definitions of Core Web Vitals and retesting.

Who in your team leads the speed-up work

The site owner leads: they name the important journeys and sign off on how you'll know a fix worked. A developer then works on a specific request, component or interaction. On WordPress, start by reviewing your theme and plugins, then the cache layers, instead of adding several tools that do the same job.

For a single public URL, use the CometWeb performance auditor and read which sources were actually available: browser measurements, field data or only HTTP checks. That matters, because only a browser can measure LCP and INP. The methodology page explains how CometWeb labels lab and field data.

The Performance module in CometWeb Insight, our website audit app, is for working on priorities and re-running analyses across the whole site:

  • it separates CrUX field data (LCP, INP, CLS over 28 days) from lab measurement,
  • when data is missing, it shows N/A with a reason instead of a zero,
  • it points to scripts that block the main thread and CSS that blocks rendering,
  • it keeps a measurement history for the same URL before and after a release.

Three takeaways

  1. The list of fixes is the easy part. The hard part is working out what the visitor is really waiting for, and that decides what you do first.
  2. A smaller file shortens only one stretch of the path. A font, an image or a stylesheet speeds up the page only if it was blocking what the visitor waits for.
  3. A repeated measurement confirms the fix. A plugin, a new image format or a Lighthouse score of 100 stays a hypothesis until a measurement backs it up.

This week, open the Lighthouse report for your most important page, find the LCP element, and do five runs before the change and five after. For many pages at once, start a free audit in CometWeb Insight. The median will tell you the rest.

Frequently asked questions

Do I need a score of 100 in PageSpeed Insights?

No, a score of 100 in PageSpeed Insights isn't the goal. The Lighthouse score describes a lab test and is a composite of several metrics. It does not confirm that every interaction works well, and it does not confirm a Google ranking. What matters more are the real barriers visitors hit and the absence of regressions after a release.

Are a CDN, WebP or a cache plugin enough?

A CDN, WebP or a cache plugin helps when it reduces the cost that is actually causing the problem. A CDN does not remove a heavy click handler, and a smaller image can still be waiting for JavaScript. Let the measurement decide which fix to apply.

How do I check INP when Lighthouse does not report it?

You check INP by testing interactions in a Performance recording and collecting proper field data where you can. Do not copy TBT into the INP field. If there is no interaction measurement, record the data as missing.

Sources and scope

The cometweb.io numbers in Step 3: the JS size before first render (329 → 208 KB gzipped) comes from the build, and the 6.39 → 5.46 s LCP from Lighthouse on the production homepage (simulated mobile profile, median of 7 runs, September 2026). That's a lab measurement, because CrUX has no data for cometweb.io. The font test comes from the research pack for the WOFF2 vs TTF comparison. The image and layout test applies only to the local samples described above. This guide contains no GSC data and no forecast of traffic or sales. Adjust the names and paths in the code examples to your project.

  1. Google Search Central: Understanding page experience in Google Search results.
  2. web.dev: Web Vitals.
  3. Google for Developers: About PageSpeed Insights.
  4. Chrome for Developers: CrUX API.
  5. Chrome for Developers: CrUX methodology.
  6. web.dev: Why lab and field data can be different.
  7. web.dev: Optimize Largest Contentful Paint.
  8. web.dev: Optimize resource loading with the Fetch Priority API.
  9. web.dev: General HTML performance considerations.
  10. web.dev: Efficiently load third-party JavaScript.
  11. web.dev: Best practices for fonts.
  12. Chrome for Developers: Lighthouse performance scoring.

Check your site in Insight

Free audit for performance, SEO, accessibility and security, with clear priorities.

No credit card Report with date and sources Free account in the app