How to make your website faster and pass Core Web Vitals
Compressing your images will not fix this. Three numbers decide whether Google calls your site fast, and two are set by your architecture, not your assets.

Compressing your images will not fix this. Three numbers decide whether Google calls your site fast, each is graded at the 75th percentile of real page loads rather than at the average, and they update on a 28-day trailing window. Two of the three are set by your architecture, not your assets: a page assembled by a server on every request starts the race behind a file that already exists. Budget weeks.
What this actually is
Core Web Vitals are three field-measured metrics. Google states plainly that "Core Web Vitals are used by our ranking systems," and equally plainly that relevance still outranks them.
Largest Contentful Paint (LCP): time until the largest text block or image in the viewport finishes rendering. Good is 2.5 seconds or less. Poor is over 4.0 seconds. Anything between needs improvement.
Interaction to Next Paint (INP): the latency of a page's interactions, from tap to the next frame painted. Good is 200 milliseconds or less. Poor is over 500. INP became a stable Core Web Vital on 12 March 2024, replacing First Input Delay (FID). Any guide still telling you to optimise FID is working from a retired standard.
Cumulative Layout Shift (CLS): a unitless score for how much visible content jumps around during the page's lifespan. Good is 0.1 or less. Poor is above 0.25.
Now the part most people miss. These are not one score you average. They are three independent gates, and each one is judged at the 75th percentile of page loads, segmented across mobile and desktop.
Read that percentile precisely, because the loose version of it sends people looking for the wrong thing. It is a percentile of page loads, not a group of visitors and not a device profile. Sort every recorded load of the page from fastest to slowest, take the value that 75 percent of loads come in at or below, and that value is the one graded against the threshold. Your median load does not decide it. Your fastest does not either. A page can be good on three loads in four and still fail the gate, because only the value at the cut is scored. One person visiting six times over a month contributes six loads, each counted on its own.
The data comes from the Chrome User Experience Report (CrUX), which collects from real Chrome users who have opted into reporting. Search Console reports it over the last 28 days.
That gives you the false dichotomy that wastes the most time in this repair: Lighthouse says 98 and Search Console says Poor, so one of them must be broken. Neither is. Lighthouse is lab data, one simulated load, one throttled connection, one device profile, run right now. CrUX is field data, thousands of real loads, real hardware, real networks, aggregated over four weeks. They measure different populations at different moments. When they disagree, the field data is the one that ranks you.
INP makes the gap concrete. Lighthouse loads the page in a simulated environment with nobody tapping anything, so it does not report an INP for a standard page-load audit at all. It reports Total Blocking Time (TBT) in its place, which Google describes as "lab-measurable and ... a proxy for INP". TBT tells you whether your main thread is busy enough to make interactions feel slow. It does not tell you how slow any of them actually were.
That is a limit of the page-load audit, not of lab measurement generally. You can measure INP in the lab, by doing the tapping yourself: open the Chrome DevTools Performance panel and use the page, and it captures your local INP alongside LCP and CLS, on your device and your connection. What you get back is one person's INP on one machine. That is enough to find the interaction handler that blocks for 400 milliseconds. It is not enough to predict the field number, because you are not a distribution.
How to check
Four tools, in the order you should reach for them.
Search Console, Core Web Vitals report. Left nav, under Experience. Open Mobile first, then Desktop. This is the number that counts. It groups URLs by similar page templates, and only indexed URLs appear at all.
PageSpeed Insights at pagespeed.web.dev. Paste a URL. The top block is field data from CrUX, and it carries two tabs: one for this URL, one for the whole origin. Open both. A page too thin for URL-level data still shows the origin, and the two numbers answer different questions. The bottom block is a Lighthouse lab run. Same page, both data types, one screen, which makes it the fastest way to see the lab-versus-field gap for yourself.
Chrome DevTools. F12, Lighthouse panel, mode Navigation, device Mobile, Analyze page load. Use this while you work, because it runs against your local build and behind authentication. Then switch to the Performance panel and click through the page for the INP number the Lighthouse run cannot give you.
Lighthouse from the terminal, for repeatable measurement:
npm install -g lighthouselighthouse https://example.com --view
Add --preset=desktop for the desktop profile, and --output=json --output-path=./run-2026-08-17.json to keep a series you can diff after each change. The command line and Node workflows both require Chrome installed on the machine.
And one more, for the content delivery network (CDN) work later:
curl -I https://example.com/assets/styles.css
Read cache-control, age, and your provider's hit indicator (cf-cache-status on Cloudflare).
What you will see
"No data available" in Search Console, or a missing field block in PageSpeed Insights. This is not a pass, and it is not one cause. CrUX admits a page only when it clears two bars: publicly discoverable, meaning it returns a 200 and carries no noindex meta tag or X-Robots-Tag header, and sufficiently popular, meaning enough distinct visitors for the distribution to mean something. Google does not publish that second threshold. So work the boring explanations first, in this order: the property was added too recently to have 28 days behind it, the URL is not indexed, a noindex or a non-200 status is disqualifying it, or the traffic really is too thin. Only the last one is a waiting game. Until it clears, fall back to lab data and judgement.
Origin-level data but no URL-level data. Common on small sites. Origin-level CrUX pools the page-load experiences of every page under the origin into one distribution, so it is weighted by where your traffic actually goes rather than being an average across pages. A slow homepage taking most of your visits therefore sets the origin grade, and your otherwise fine article inherits it. Once the origin itself qualifies, loads of pages that would never qualify on their own still count into that pool.
Poor in the field, near-perfect in the lab. Two causes, and they compound: your real visitors are on slower devices and networks than Lighthouse simulates, and the 28-day window still contains weeks of the version you already fixed.
Good in the lab and poor only on Mobile. The desktop profile hides main-thread cost. Mobile CPUs do not.
CLS good on first load and poor in the field. Field CLS accumulates over the whole page lifespan, including shifts caused by a late-loading advert, cookie banner, or embedded widget that the lab run finished before.
Every metric green and rankings unchanged. Entirely possible. Page experience is one input, and Google says explicitly that it will still show the most relevant content when page experience is sub-par. Speed removes an obstacle. It does not manufacture demand.
How to fix
In the order that moves the numbers.
Images. This is the cheap win, and it is worth doing properly.
Format: serve AVIF with WebP and JPEG fallbacks through <picture>, so old browsers still get a file. Dimensions: stop shipping a 4000px camera original into an 800px slot. Resize to the largest size the layout actually renders, then double it for high-density screens, and no further. Responsive delivery: give the browser a srcset with two or three widths and a sizes attribute describing the slot, and let it choose.
Always set width and height attributes, on every image, without exception. The browser computes the aspect ratio from those two numbers and reserves the box before a single byte of image data arrives. Leave them off and the text below reflows the moment the image lands. Unsized images are the most common single cause of CLS on small business sites.
loading="lazy" on everything below the fold. Never on your LCP image: lazy-loading the hero delays the exact element the metric is timing. Put fetchpriority="high" on it instead.
Render-blocking resources. Lighthouse names the offending files under "Eliminate render-blocking resources." Every stylesheet in <head> blocks the first paint until it downloads and parses. Inline the critical CSS for above-the-fold content, and load the rest asynchronously. Every synchronous <script> in <head> blocks HTML parsing: add defer, or async if execution order genuinely does not matter.
Then count your third-party tags. The chat widget, the tag manager, the two analytics scripts, the review badge, the font loader. Each one executes JavaScript on the main thread, and the main thread is the resource INP is measuring. This is usually the least technical and most political part of the repair, because someone owns each of those tags and will want it kept.
Move from dynamically generated pages to pre-rendered ones. This is the item that decides Time To First Byte, and it is the reason this guide says weeks.
Today, a request to a typical WordPress page boots PHP, loads the plugin stack, runs a series of MySQL queries, assembles a template, and only then emits the first byte. Every request. Every visitor. Caching plugins hide some of it. They do not remove it.
Pre-rendering generates each page to plain HTML at build time. The result is a folder of files with no database behind it. Astro, Next.js static export, Hugo, Eleventy, and Jekyll all do this.
What the migration actually involves, and this is where estimates break:
Inventory every URL you currently serve, from the sitemap and from server logs, not from memory. Port the templates, which means rewriting theme PHP as components. Move the content, either into markdown and data files or by keeping the existing CMS as a headless source the build reads from. Replace everything that needed a runtime: a static file cannot process a form submission, run a search box, gate a members area, or accept a comment. Each becomes a third-party service or a serverless function. Map every old URL to its new address with 301 redirects. Regenerate the sitemap. Wire a build pipeline so a non-developer can publish without waiting for you.
The catch nobody mentions in the sales pitch: you are trading request-time flexibility for build-time speed. On a 500-page site, every content edit triggers a full rebuild.
Buy and configure the CDN. Static files copied to servers worldwide so they are served from near the visitor. Cloudflare's free plan covers a small site. Bunny, Fastly, and Amazon CloudFront are usage-priced.
The sequence matters, and getting it out of order takes your site down.
1. Lower the time to live (TTL) on your existing DNS records to 300 seconds, and wait out the old TTL before you touch anything else. TTL is how long a resolver is allowed to cache each record, so lowering it is what makes a record change, or a rollback of one, reach real users in minutes instead of hours. Know its boundary while you are setting it: TTL governs records. It does not govern the nameserver delegation held at your registrar, which is a separate mechanism on its own clock, so this step does not buy you a fast rollback out of step 3's full setup.
2. Create the account, add the domain, and let the provider import your existing DNS records. Then check the imported zone line by line against your registrar's. Missing MX records is how a migration silently kills the company's email.
3. Point traffic at the CDN. The two routes differ in how fast you can undo them, which is the part worth deciding on purpose. Full setup means changing the nameservers at your registrar, handing the whole zone to the provider. It is the clean option and the slow one: Cloudflare's guidance is that with some registrars you wait up to 24 hours for the nameserver update to take effect, and the record TTLs you lowered in step 1 have no bearing on that, because delegation is cached separately from records. Partial setup means a canonical name (CNAME) record, a DNS alias pointing one name at another, aliasing your hostname to the provider's. It leaves DNS where it is and reverses fast, because reversing it is a record change, which is exactly what the lowered TTL does cover.
4. Wait for the Transport Layer Security (TLS) certificate to issue and go active before you enable any force-HTTPS or HSTS setting. Flip that switch early and every visitor gets a certificate error. Set the origin connection to full or strict so the CDN-to-server hop is encrypted too, not just the visitor-to-CDN hop.
5. Set cache-control headers. HTML gets no-cache or a short max-age, because HTML is how a visitor discovers everything else changed. Static assets get public, max-age=31536000, immutable.
And here is the trap the header exists to spring. immutable tells the browser not to revalidate, even on reload. max-age=31536000 is one year. Cache an asset with both at a filename that never changes, and every returning visitor holds your old CSS for up to a year. Purging the CDN clears the edge servers. It does not reach a laptop in Bogotá. There is no server-side lever that evicts it. Your only exits are changing the filename or waiting out the year. Ouch.
The fix is content-hashed filenames, produced by your build: styles.a3f9c1.css. Change the content, the hash changes, the URL changes, the cache key changes, and the browser fetches fresh because as far as it is concerned this is a file it has never seen. Long max-age and immutable are safe only once hashing is in place. In that order, never the reverse.
6. Purge the cache on every deploy, then verify with curl -I. Do not assume the configuration took.
What it costs
The image pass: one to two days for a twenty-page site, longer if your CMS has no image pipeline and every file needs regenerating by hand.
Render-blocking and third-party cleanup: a day of engineering, plus however long it takes to get agreement on removing two marketing tags.
The static rebuild: three to eight weeks for a brochure site with forms and a blog. Longer with anything transactional. If someone quotes you a weekend, they have not inventoried your URLs.
CDN setup: half a day of configuration, up to 24 hours of waiting on the nameserver change at some registrars if you take the full setup, and then a week of finding the one cache rule you got wrong. Money is the smallest line: free to a few dollars a month at small-site volume.
Then the wait. Field data moves on a 28-day trailing window, so a fix deployed today is roughly half-reflected at two weeks and fully reflected at about a month. You cannot deploy at noon and check at one.
Call it a quarter of part-time work, honestly scoped. Not an afternoon.
Want the numbers for your own site before you commit to any of this? Run a free web audit.
References
Web Vitals - Google, web.devLargest Contentful Paint (LCP) - Google, web.devInteraction to Next Paint (INP) - Google, web.devCumulative Layout Shift (CLS) - Google, web.devINP is now a Core Web Vital - Google, web.devUnderstanding page experience in Google Search results - Google Search CentralCore Web Vitals report - Google Search Console HelpChrome UX Report methodology - Google, Chrome for DevelopersWhy lab and field data can be different - Google, web.devLighthouse overview - Google, Chrome for DevelopersLighthouse source and releases - GoogleChrome, GitHubPerformance panel overview - Google, Chrome for DevelopersPageSpeed Insights - GoogleWhat's new in PageSpeed Insights - Google, web.devOptimize Largest Contentful Paint - Google, web.devOptimize Interaction to Next Paint - Google, web.devOptimize Cumulative Layout Shift - Google, web.devServe responsive images - Google, web.devBrowser-level image lazy loading - Google, web.devDefer non-critical CSS - Google, web.devEliminate render-blocking resources - Google, Chrome for DevelopersTime to First Byte (TTFB) - Google, web.dev<img>: The Image Embed element - MDN, MozillaCache-Control header - MDN, MozillaAdd a site to Cloudflare - Cloudflare DocsFull DNS setup - Cloudflare DocsFull setup troubleshooting - Cloudflare DocsTime to Live (TTL) - Cloudflare DocsUniversal SSL - Cloudflare DocsCache-Control at the edge - Cloudflare DocsPurge cache - Cloudflare DocsCloudflare plans - CloudflareBunny CDN pricing - BunnyAmazon CloudFront pricing - Amazon Web ServicesAstro - AstroNext.js static exports - VercelHugo - HugoEleventy - EleventyJekyll - Jekyll











