Skip to content

Claude Code Production EngineeringStep 4 of 5

Claude Code Performance Optimization: Core Web Vitals Workflow

"Make the website faster" is not a task an agent can complete. This is the loop that works: measure, find the one bottleneck, change exactly one thing, measure again, and revert it if it did not move.

Claude Code Guides: Claude Code Performance Optimization. Two gauges labelled before and after, with an arrow between them.

What "faster" has to mean before you start

"Make the website faster" is not a task an agent can complete, because it is not a task at all. Faster for whom, measured how, and how would anyone know it worked?

Three different things get called performance and they disagree with each other regularly:

Kind What it is What it is good for
Lab / synthetic One simulated load, controlled conditions — Lighthouse Diagnosis. Reproducible, so a change can be attributed
Field data What real visitors experienced, aggregated Truth. This is what Google reports and what users felt
The score A weighted composite of lab metrics Very little. It is a summary, not a goal

Field data wins every disagreement. A site can score 100 in the lab and be slow for the people using it, usually because the lab run had a fast connection, no third-party scripts loading, and a warm cache. Optimise against field data where you have it, and use lab tools to work out why.

Core Web Vitals, precisely

Three metrics, with the thresholds Google publishes on web.dev:

Metric Measures Good
LCP — Largest Contentful Paint When the largest element in the viewport finished rendering Within 2.5 seconds
INP — Interaction to Next Paint How long the page takes to visibly respond to interactions 200 milliseconds or less
CLS — Cumulative Layout Shift How much content moves unexpectedly during load 0.1 or less

Two details that change how you read a report. The targets are assessed at the 75th percentile of page loads, segmented across mobile and desktop — so "our median is fine" is not a pass, and a mobile failure is not offset by a desktop success.

And First Input Delay is gone. INP was promoted from experimental in 2023 and became a stable Core Web Vital in 2024, retiring FID. Any advice still optimising for FID predates that, which is a useful freshness test for a performance article — including one an assistant wrote you.

The loop, and the one-variable rule

A six-step performance loop: measure a baseline, locate the single largest contributor, state a hypothesis with an expected change, change exactly one thing, measure again under the same conditions, then keep the change or revert it. The fourth step, changing one thing, is highlighted as the discipline the whole loop depends on.
Change five things at once and you have learned nothing about any of them, however much the score moved.

Step four is the entire discipline. It is also the one an agent will break by default: asked to improve performance, it will happily convert images, defer scripts, inline critical CSS and add preloads in one pass. The score may move. You will not know which change did it, whether two of them cancelled out, or which one to revert when something breaks next month.

Step six is the one people skip. A change that produced no measurable improvement is not neutral — it is code you now maintain. Revert it.

Take a baseline you can return to

Before touching anything, record enough to compare against later, on the same URL, the same viewport, and the same network conditions:

Record Why
LCP, INP, CLS The metrics you are actually moving
The LCP element Which node it was. This changes as you optimise, and knowing it is half the diagnosis
TTFB Separates a server problem from a front-end one
Transfer size and request count Rough, but it catches a regression fast
JavaScript bytes, and how many are third-party Usually the largest lever on INP
Viewport and throttling used Without it the comparison is meaningless

Measure mobile first. Indexing is mobile-first and mobile is where the constraints bite; a desktop-only baseline hides the problem you are trying to find.

Lighthouse, PageSpeed Insights, DevTools

Lighthouse is a lab tool and its output varies run to run — machine load, network, extensions. Run it three times and take the median rather than trusting one run. Its opportunities list is a good starting point and a poor priority list: it is ordered by estimated saving, not by what actually constrains your page.

PageSpeed Insights runs Lighthouse and, where enough real traffic exists, shows Chrome UX Report field data beside it. When both are present, read the field data first and use the lab section to explain it. A new site will have no field data at all, which is not a fault — it means the lab numbers are all you have, and they should be treated as directional.

Chrome DevTools is where diagnosis actually happens. Network for the waterfall and what blocks what; Performance for long tasks and main-thread time; Coverage for how much CSS and JavaScript is unused on first load. The Performance panel is the only one of the three that tells you why INP is bad rather than that it is.

Diagnosing LCP

Identify the element first. Everything else is guessing until you know which node Google is timing.

new PerformanceObserver((list) => {
  const e = list.getEntries().at(-1);
  console.log('LCP', Math.round(e.startTime), e.element);
}).observe({ type: 'largest-contentful-paint', buffered: true });

Then the cause is almost always one of four, and they need different fixes:

  • Slow server response. High TTFB. No amount of front-end work fixes this — look at caching, the database, or the host.
  • The resource starts late. The hero image is discovered only after CSS or JavaScript has run. A preload, or simply putting it in the HTML rather than injecting it, fixes this.
  • Render-blocking resources. Stylesheets and synchronous scripts in the head delaying first paint.
  • The element is lazy-loaded. Self-inflicted and extremely common — see the mistakes section.

Diagnosing INP

INP is a main-thread problem. The page is busy, so it cannot respond.

Open the Performance panel, interact with the page, and look for long tasks — anything over 50 ms blocks input. Then attribute them: your own event handlers, framework hydration, or a third-party script. In practice it is usually the third one.

The interventions, in the order worth trying: remove the third-party script if it cannot justify itself; split large bundles so less executes on load; break long tasks up so the main thread can yield; and stop doing layout-triggering work inside high-frequency handlers.

Note that INP cannot be measured without a real interaction. A tool that reports an INP figure for a page nobody touched is reporting an estimate — worth less than an honest absence.

Diagnosing CLS

The most fixable of the three, and almost always the same handful of causes: images and video without width and height; web fonts swapping to a differently-proportioned face; content injected above existing content — banners, notices, ads; and sections that render after data arrives without reserved space.

new PerformanceObserver((list) => {
  for (const e of list.getEntries()) {
    if (!e.hadRecentInput && e.value > 0.001) console.log(e.value, e.sources);
  }
}).observe({ type: 'layout-shift', buffered: true });

The sources array names the nodes that moved, which turns "CLS is 0.24" into a specific element in seconds.

Images

Usually the largest single win on a content site.

  • Serve at the size rendered. A 2400px image in a 600px slot wastes most of its bytes.
  • Modern formats — AVIF, then WebP, with a fallback.
  • width and height on every image. This is a CLS fix, not a nicety.
  • srcset and sizes so each device gets an appropriate file.
  • loading="lazy" below the fold — and never on the LCP image.
  • Consider fetchpriority="high" on the LCP image so it is not queued behind less important requests.

JavaScript

Start with the audit nobody wants to do: list every third-party script, name what it does, and say who asked for it. Analytics nobody reads, a chat widget nobody answers, three tag managers — each one costs main-thread time on every visit.

Then Coverage in DevTools, which shows how much of each bundle actually executed on load. High unused percentages point at code splitting or a dependency doing far more than you need.

Beyond that: defer for scripts that need the DOM, async for genuinely independent ones, and a hard look at any dependency pulled in for a single function.

CSS and fonts

CSS in the head blocks rendering by design. The question is how much of it is needed for the first screen. Coverage answers that too. Critical CSS — inlining what the first viewport needs and loading the rest asynchronously — is effective and adds a build step and a way to get out of sync, so it is worth measuring before adopting.

Fonts are a reliable source of both LCP and CLS problems:

  • Subset to the characters you use.
  • font-display: swap so text is visible immediately, accepting a swap.
  • Preload the one face used above the fold, and only that one — preloading everything makes them compete.
  • Choose a fallback with similar metrics, or tune it with size-adjust, so the swap does not shift layout.
  • Consider whether a system font stack would do. It is free and instant.

Shopify, where you control less

Worth its own section, because the usual advice assumes control you do not have.

What you do not control: the platform's own scripts, checkout, and the CDN. Removing platform JavaScript to improve a score breaks analytics, consent handling and several storefront features. Do not.

What you do control, and where the wins actually are:

  • Apps. The single largest lever on most stores. Each one injects script on every page. Audit them: name each, say what it does, and uninstall what is not earning it. An uninstalled app can leave script behind — check the rendered page afterwards rather than assuming.
  • Image requests. Use image_url: width: N with a real srcset rather than serving one large file.
  • Liquid in loops. Repeated lookups inside a `for` loop over a large collection cost server time and show up as TTFB.
  • Theme assets. A purchased theme frequently ships a stylesheet in the hundreds of kilobytes for a storefront using a fraction of it.
  • Tracking scripts added by hand to the theme rather than through Customer Events, where they are sandboxed.

A measured example from this store: the homepage renders LCP in 676 ms with CLS at 0.0000, over 145 requests and about 1 MB transferred. The request count looks alarming and almost all of it is platform code. Chasing it down would cost functionality and gain nothing a visitor would feel. The Shopify-specific build workflow is covered in building a Shopify store with Claude Code.

The performance prompt

We are optimising the performance of this page: [URL]

Work in this order and stop at each step for me to confirm:

1. BASELINE. Measure and report: LCP with the identified element,
   INP if a real interaction is possible (say so if not), CLS with
   the shifting nodes, TTFB, transfer size, request count, and
   JavaScript bytes split first-party vs third-party. State the
   viewport and throttling used.

2. LOCATE. Name the single largest contributor to the worst
   metric, with the evidence. Not a list of twelve opportunities.

3. HYPOTHESISE. State the one change you propose, what you expect
   it to move, and roughly by how much.

4. CHANGE ONE THING. Only that. Do not tidy anything else.

5. MEASURE AGAIN. Same tool, same viewport, same throttling.
   Report before and after side by side.

6. DECIDE. If it did not move the metric meaningfully, revert it
   and say so. Do not keep a change because it seems sensible.

Never lazy-load the LCP element. Never remove functionality to
improve a score. If you cannot measure something, say so rather
than estimating it.

Reporting before and after

Metric Before After Change
LCP (mobile, 4× CPU throttle) 3.4 s 2.1 s −1.3 s
LCP element img.hero img.hero unchanged
CLS 0.18 0.02 −0.16
Transfer 2.1 MB 0.9 MB −1.2 MB
Change made Hero served as AVIF at rendered size, with width and height set

One row for the change, so the table records what was done as well as what happened. Without it you have numbers nobody can reproduce.

Mistakes that cost the most

  • Optimising the score instead of the experience. The score is a weighted summary. Moving it without moving a metric a user feels is theatre.
  • Changing many things at once. Attribution is gone, and so is your ability to revert cleanly.
  • Lazy-loading the LCP image. The most common self-inflicted regression there is: a blanket "lazy-load all images" rule directly delays the metric it was meant to help.
  • Removing functionality for a number. Deleting analytics or consent handling to gain points is a bad trade you will have to undo.
  • Measuring desktop only. The thresholds are assessed for mobile and desktop separately.
  • Ignoring field data because lab data is prettier. Field data is what happened.
  • Treating the opportunities list as a work queue. It is ordered by estimated saving, not by what constrains your page.
  • Not measuring afterwards. Then it is not optimisation, it is redecoration.

Where to go next

Performance is one pass of a wider inspection — the website audit guide covers the rest, and the SEO workflow covers where Core Web Vitals sit among the other technical signals. For building performance in rather than retrofitting it, start with the complete website workflow. The free launch checklist and audit checklist both carry the performance items in short form.

Sources and further reading

Read these next

See how this fits into Claude Code Production Engineering

Continue your learning path

  1. Next

    A migration keeps its traffic when every URL that mattered still returns 200 or redirects in one hop to its replaceme...

The whole sequence: Claude Code Production Engineering

Free download

The CLAUDE.md Starter Kit, free

Four working CLAUDE.md files you can drop into a project today, plus the one-page checklist for what belongs in one and how to tell whether yours is actually working.

  • CLAUDE.md for a static marketing site
  • CLAUDE.md for a web application, with security and migration rules
  • CLAUDE.md for a Shopify theme, including the gotchas that cost hours
  • CLAUDE.md for a shared package in a monorepo
  • A one-page checklist, and how to test the file is actually working
What are you working on?

The download appears here as soon as you submit. I will also email you when there is a new guide worth reading. No fixed schedule, no selling your address, unsubscribe from any email. See the privacy policy.