Insight

Modern Website Performance: Core Web Vitals, Architecture and User Experience

Layered performance traces over a faint page wireframe, showing paint timing, interaction response and layout stability.

Core Web Vitals are a proxy, not a goal. Measure server response and the client boundary, send less JavaScript, reserve space, and add budgets so gains are not given back.

11 min read
Web Performance
performanceCore Web Vitalsweb developmentaccessibilityoptimisation

Performance work goes wrong in a predictable way. A team collects a score, changes a handful of things, the number moves, and the underlying experience does not. The cause is usually that the score was treated as the goal rather than as a proxy for something measurable about real users.

This article takes the measurement-first approach: what the browser reports, which architectural decisions move it, and how to tell a real improvement from a lucky run. Nothing here promises a ranking change. Search engines index what they can, and performance is one of many inputs; the defensible argument for doing this work is that it makes the site usable for people.

Start with what the browser measures

Core Web Vitals are the closest thing the platform has to a shared definition of a usable page. Each one measures a different part of the experience, and they only mean something measured on a real device over a real network. The assessment bands published with them are guidance for interpreting a measurement, not targets worth optimising towards for their own sake.

Largest Contentful Paint

When the largest visible element finishes rendering. It is dominated by how quickly the browser can obtain the bytes for the main content, and by how much work sits between the response and first paint: server response time, render-blocking resources, font loading, and hydration.

Interaction to Next Paint

How long the interface takes to respond after a user interacts with it. This is the metric that exposes heavy client-side JavaScript, because a large bundle must be downloaded, parsed, compiled and executed before it can respond to anything. A long task running on the main thread delays the reply to a tap even when the tap handler itself is trivial.

Cumulative Layout Shift

How much visible content moves after it first appears. It is usually caused by content that arrives late without reserved space: an image with no dimensions, a banner inserted above existing content, or a web font that swaps to different metrics after first paint.

Server response is a term most audits skip

Time to first byte is the first item on the critical path and the one most often excluded from the conversation, because it feels like a hosting problem rather than a front-end one. It is usually an application problem: an unindexed query, a synchronous call to a service that is slow today, or a cold start on a platform that scales to zero.

Whatever is on the critical path sets a floor for everything after it. Compressing images is wasted effort while the document is waiting two seconds on a query. Measure the server response separately, on a warm cache and a cold one, before optimising anything in the browser.

Rendering strategy is an architecture decision

How a page is produced determines what the browser has to do before it can show anything, and it is chosen long before anyone opens a performance tool.

  • Server-rendered HTML gives the browser content immediately, and interactivity arrives afterwards. It has the fewest ways to fail on a slow device.
  • Static generation removes the server from the request path entirely for pages that can be built ahead of time.
  • Client-side rendering adds a round trip and a dependency on JavaScript executing before anything appears. Used for the interactive parts of a page, it is a poor choice for the main content.

A page that requires JavaScript to display its content cannot be fast on a slow device, because the slowest part of the chain sets the floor. Rendering on the server by default and layering interactivity on top is what makes the rest of the optimisation possible.

The hydration boundary decides how much JavaScript ships

Server-rendered pages in modern frameworks are frequently still client components underneath, which means the browser downloads the code for that component, parses it, and runs it to attach behaviour to markup that already exists. That work is hydration, and it is pure cost on a page whose content did not need it.

The useful question is not how much JavaScript the project has, but how much of it is on the critical path of a given route. A navigation bar that needs a click handler, a pricing table that needs to expand rows and a chart that animates are all reasonable client boundaries. A heading, a paragraph and a list of links are not. The wider the boundary, the more the browser is asked to do before the page becomes responsive.

Where an effect genuinely needs JavaScript, it should be a progressive enhancement: the content and the fallback work first, and the behaviour is attached on top. That is not only a performance position. It holds up better on constrained devices, it degrades more gracefully when a script errors, it gives assistive technology less to untangle, and it forces the team to keep content separate from interaction, which is usually the right separation anyway.

Send less JavaScript

JavaScript is the most expensive thing most pages load, because it must be downloaded, parsed, compiled and executed. Not every kilobyte costs the same, but the direction is unambiguous, and dependency review is consistently the highest-yield change available.

  • Split code by route so an initial load carries only what that page needs.
  • Delete dependencies that duplicate something the platform already does. This is unglamorous and usually the largest single win.
  • Load third-party scripts after the interaction that needs them, or when the main thread is idle, rather than in the head.
  • Check whether a heavy component is a dependency problem in disguise, with a date picker or a chart library pulled in for one field.

Media, fonts and caching

Images and fonts are where the remaining wins usually are, because they are requested by every visitor and rarely looked at individually.

For images: serve modern formats, request only the size the layout needs, set explicit dimensions so the browser can reserve space before the bytes arrive, and lazy-load anything below the fold. Lazy loading the largest element on the page is counterproductive, since deferring it delays the metric that depends on it.

For fonts: use a swap behaviour so text renders immediately in a fallback, subset to the characters actually used, preload only the faces needed for the first screen, and size the fallback to match the web font's metrics so the swap does not move the page. Self-hosting removes a third-party connection from the critical path and lets you control caching directly.

For delivery: hashed static assets can carry long-lived cache headers, so a repeat visit skips the download entirely, while the HTML document revalidates because it is the thing that changes. A CDN in front of static assets also moves them closer to the user, which matters more for a visitor on a distant or congested network than for one on the same continent.

Animation has a cost, and it is paid on the main thread

Animation is a design asset and a performance decision at the same time. Animating properties that the compositor can handle independently, such as transform and opacity, keeps work off the main thread. Animating properties that trigger layout or paint on every frame puts the whole chain at the mercy of whatever else the page is doing, which is exactly when a user is interacting.

Two rules cover most of it. Animate transform and opacity rather than width, height, top or left. And respect a reduced-motion preference, which is both an accessibility requirement and a way to avoid shipping animation machinery to people who have asked not to receive it. Animations that run only while off screen are worth pausing for the same reason.

For canvas and WebGL scenes, the same logic applies to the render loop: cap the frame rate, stop it when the scene is not visible, and keep per-frame allocation close to zero. A decorative background that costs a few percent of the main thread is a reasonable trade; one that competes with a tap handler is not.

Third-party scripts deserve an owner and a date

Tag managers, analytics, chat widgets, personalization engines and embedded video players all arrive through someone else's priorities. Each one costs download, execution and main-thread time, and none of them appears in your codebase.

The only version of this that works is an inventory: what each script is, who owns it, why it exists, and when it was last reviewed. Scripts nobody claims tend to accumulate, and removing one is usually a product conversation rather than a technical one. Measuring with the tags still in place, then again with a suspect subset blocked, is the fastest way to attribute cost.

Field data and lab data answer different questions

Lab tools give you a repeatable, controlled measurement on a simulated device and network, which is what you want when comparing two versions of a change. Field data comes from real users on real devices, and it is the only source that tells you what actually happened.

The relationship between them is not fixed. A change can improve your lab score while making the real experience worse, particularly when it helps a fast connection and hurts a slow one. Conversely, a real-user number can look healthy while a genuine problem sits inside it, because a wide device and network distribution hides a bad tail.

The useful practice is to report both, segmented by device class and connection type where the tooling allows it, and to treat a disagreement between them as information rather than noise. It usually points at an assumption baked into the lab profile.

Measure where your users are, not where you are

A desktop measurement on a fast connection tells you very little about someone on a mid-range phone on an intermittent connection. Test at the constraints that matter.

  • Throttle CPU and network in the tooling so the run reflects a mid-range device rather than a development machine.
  • Test the pages that carry the business, not only the homepage. A slow product page or checkout step costs more than a slow homepage.
  • Run each measurement several times. Variance is high enough that a change adopted on one run is not a measured improvement.
  • Confirm the slow path is the one you are measuring. Development and staging environments are often faster than production because they have warm caches, no real traffic and no third-party scripts, so a green result there can coexist with a poor real experience.

Diagnose before you optimise

Existing sites are usually not slow everywhere, and finding the largest contributor first is far more effective than a general audit. Work in this order.

  1. Establish a baseline on the pages that matter, on a throttled mid-range profile, and keep the result so later work is comparable.
  2. Split the measurement into the server response, the render-blocking chain and the JavaScript bundle. These have different causes, different owners and different fixes.
  3. Fix the largest contributor only, then re-measure on the same profile.
  4. Check layout shift separately, since a fast page that moves is not an improvement.
  5. Stop when the remaining cost is genuinely distributed. Past that point returns fall away quickly and you are spending effort on noise.

Budgets are what stop the work being repeated

Performance degrades gradually and gets fixed only when it becomes embarrassing. The defences are unglamorous.

  • Budgets enforced in the build, so a bundle that grows past its limit fails a check rather than a quarter of user experience.
  • Performance assertions in the end-to-end suite, checking the pages that matter on a throttled profile.
  • A third-party script inventory with an owner and a review date for each entry.
  • Performance considered in code review at the same time as correctness, not afterwards.

The most durable version of this is architectural. A rendering strategy that does not depend on JavaScript for content, and a component boundary that keeps interactivity small, mean there is far less to regress. Budgets then protect the remainder. The reasoning behind those structural choices is set out in designing scalable web applications.

Optimise the experience, then measure it again

Web performance is an architectural property before it is an optimisation exercise. The reliable levers are unexciting: keep the server response short, render on the server, keep the client boundary narrow, send less JavaScript, reserve space for anything that arrives late, load fonts without blocking, cache what does not change, and give every third-party script an owner.

Measure on the devices your users have, change one thing at a time, confirm the change held rather than the run being variable, and add a budget so the improvement is not quietly given back. That is the whole method, and it does not require a rewrite to begin.

We treat these as acceptance criteria rather than a final pass. See our web and platform work for how that is built into delivery, and the process we follow for where performance checks sit relative to the other gates.

Need a number to hold the team to?

We agree performance budgets during planning and enforce them in the build, where they are cheap, rather than after launch.