Performance Is a Design Decision

LCP broken into its four parts with the proportion each contributes, INP replacing FID, and the specific design choices that decide each number.

Date

/

Category

Web design

Web design

/

Writer

Keon Hwui Kim

Keon Hwui Kim

On 12 March 2024 the Chrome team retired a metric. First Input Delay, which had measured only the delay before a page began responding to the first tap or click, was replaced as a Core Web Vital by Interaction to Next Paint. The announcement gave developers until 9 September 2024 to move their tooling across. INP had been introduced as an experimental metric in May 2022 and announced as a future Core Web Vital in May 2023, so the change was neither sudden nor quiet. It was still a much bigger change than it looked, because FID measured a single moment near the start of a visit and INP measures the whole visit.

I mention the date because it marks the point at which the performance conversation stopped being something a designer could hand entirely to an engineer. LCP is about what you chose to put at the top of the page. CLS is about whether you reserved room for the things you knew were coming. INP is about how much work happens when someone touches the thing you drew. None of those are build configuration. They are layout decisions with a stopwatch attached.

The three numbers, precisely

The thresholds are published by Google and they are not approximate. A page is measured at the 75th percentile of page loads, segmented across mobile and desktop, which means a quarter of your visitors can be having a worse time than the number suggests.

Metric

Good

Needs improvement

Poor

Design decision most responsible

Largest Contentful Paint

2.5 seconds or less

2.5 to 4.0 seconds

Over 4.0 seconds

What you chose as the hero, and how it is delivered

Cumulative Layout Shift

0.1 or less

Between 0.1 and 0.25

Over 0.25

Whether the layout reserves space for late content

Interaction to Next Paint

200 ms or less

201 to 500 ms

Over 500 ms

How much the interface redraws on a single tap

LCP, in Google's own words, reports the render time of the largest image, text block, or video visible in the viewport, relative to when the user first navigated to the page. The candidate set is narrow and worth memorising, because it tells you exactly which element on your design is being timed: img elements, image elements inside an svg, video elements (the poster image or first frame), any element with a CSS background image loaded through url(), and block-level elements containing text. Elements at zero opacity are excluded, as are elements covering the full viewport, which the browser treats as backgrounds.

So the LCP element is, almost always, the thing you spent the most time art-directing.

The hero is the metric

The HTTP Archive's 2024 Web Almanac found that images accounted for 73.3% of LCP elements on mobile and 83.3% on desktop. Text made up 26.3% and 16.3% respectively. That single statistic reframes the whole problem: in three quarters of cases, the number Google grades you on is the load time of one picture that a designer chose.

Google breaks LCP into four subparts, and gives rough proportions for a typical page:

LCP = TTFB
    + resource load delay
    + resource load duration
    + element render delay

  TTFB                     ~40%   server and network, before
                                  anything is requested
  resource load delay      ~10%   the gap between TTFB and the
                                  browser starting to fetch the
                                  LCP resource
  resource load duration   ~40%   downloading the LCP resource itself
  element render delay     ~10%   from download complete to pixels
                                  on screen
LCP = TTFB
    + resource load delay
    + resource load duration
    + element render delay

  TTFB                     ~40%   server and network, before
                                  anything is requested
  resource load delay      ~10%   the gap between TTFB and the
                                  browser starting to fetch the
                                  LCP resource
  resource load duration   ~40%   downloading the LCP resource itself
  element render delay     ~10%   from download complete to pixels
                                  on screen
LCP = TTFB
    + resource load delay
    + resource load duration
    + element render delay

  TTFB                     ~40%   server and network, before
                                  anything is requested
  resource load delay      ~10%   the gap between TTFB and the
                                  browser starting to fetch the
                                  LCP resource
  resource load duration   ~40%   downloading the LCP resource itself
  element render delay     ~10%   from download complete to pixels
                                  on screen

Note where a designer's decisions land. Resource load duration is file size and format, which is the difference between a 1.4 MB full-bleed photograph and a 180 KB one. Resource load delay is discoverability: if the hero is a CSS background image, or is injected by a component that only runs after JavaScript has parsed, the browser cannot start the download until late. The Almanac found that 35% of mobile pages have an LCP element that is not statically discoverable in the HTML.

Two pieces of guidance from the Chrome team are blunt enough to quote. The first: Never lazy-load your LCP image, as that will always lead to unnecessary resource load delay. The Almanac found that 16% of mobile pages were still lazy-loading their LCP image in 2024, which was an improvement on 18% in 2022 but not much of one. The second concerns the fetchpriority attribute, which climbed from being used on 0.03% of mobile pages in 2022 to 15% in 2024: setting a high priority on more than one or two images makes priority setting unhelpful. Priority is a ranking, not a blessing. If a designer asks for six images above the fold and all six get marked high, none of them are.

A hero image is not a decoration that happens to have a file size. On three pages in four, it is the metric.

Fonts, and the second you cannot see

The font-loading problem has two failure modes, both of which have been named since long before Core Web Vitals existed. A flash of invisible text happens when the browser holds back rendering while a web font downloads. A flash of unstyled text happens when the browser renders a fallback immediately and swaps in the real font later. The CSS font-display descriptor chooses which one you get, by setting the length of two periods. During the block period, per MDN, an element using the font must render an invisible fallback font face. During the swap period, it renders a visible fallback and swaps when the font arrives.

font-display

Block period

Swap period

What the user sees

auto

Defined by the browser

Defined by the browser

Unpredictable across engines

block

2 to 3 seconds

Infinite

Invisible text, then the web font

swap

0 ms

Infinite

Fallback immediately, then a reflow

fallback

100 ms

3 seconds

Brief blank, fallback, late fonts abandoned

optional

100 ms

None

Brief blank, then fallback for this visit

Both failure modes damage a metric, and they damage different ones. block hurts LCP whenever the largest element is text, which is a quarter of mobile pages. swap protects LCP and then hurts CLS, because the fallback and the web font almost never occupy the same space, and every line of text moves when the swap happens.

The way out is not to pick a side. It is to make the fallback and the web font take up the same room. CSS gives you four descriptors for this: size-adjust, which the Chrome team describes as a global glyph adjustment for the font face, plus ascent-override, descent-override and line-gap-override, which set the height above the baseline, the height below it, and the recommended line gap. Declare a local fallback with those metrics tuned to your web font and the swap becomes, in the team's phrase, a seamless swap.

@font-face {
  font-family: "Fallback for Brand";
  src: local("Helvetica Neue"), local("Arial");
  size-adjust: 104%;
  ascent-override: 92%;
  descent-override: 24%;
  line-gap-override: 0%;
}

@font-face {
  font-family: "Brand";
  src: url("/fonts/brand.woff2") format("woff2");
  font-display: swap;
}

body {
  font-family: "Brand", "Fallback for Brand", sans-serif;
}
@font-face {
  font-family: "Fallback for Brand";
  src: local("Helvetica Neue"), local("Arial");
  size-adjust: 104%;
  ascent-override: 92%;
  descent-override: 24%;
  line-gap-override: 0%;
}

@font-face {
  font-family: "Brand";
  src: url("/fonts/brand.woff2") format("woff2");
  font-display: swap;
}

body {
  font-family: "Brand", "Fallback for Brand", sans-serif;
}
@font-face {
  font-family: "Fallback for Brand";
  src: local("Helvetica Neue"), local("Arial");
  size-adjust: 104%;
  ascent-override: 92%;
  descent-override: 24%;
  line-gap-override: 0%;
}

@font-face {
  font-family: "Brand";
  src: url("/fonts/brand.woff2") format("woff2");
  font-display: swap;
}

body {
  font-family: "Brand", "Fallback for Brand", sans-serif;
}

The percentages above are illustrative, not universal. They have to be measured against the specific pair of fonts, and Chrome's guidance points at calculator tools that generate the adjusted @font-face block for a given target. What matters for a designer is the principle: a typeface choice is a layout-stability choice, and the number of weights you specify is a download budget.

Space you did not reserve

CLS has a formula, and it is short enough that there is no excuse for treating it as a black box.

layout shift score = impact fraction x distance fraction

impact fraction
    = the combined visible area of all unstable elements
      across two frames, as a share of the viewport

distance fraction
    = the greatest horizontal or vertical movement of any
      unstable element, divided by the viewport's largest
      dimension

CLS = the largest total score within any single session window

session window:
    shifts less than 1 second apart, maximum 5 seconds total
layout shift score = impact fraction x distance fraction

impact fraction
    = the combined visible area of all unstable elements
      across two frames, as a share of the viewport

distance fraction
    = the greatest horizontal or vertical movement of any
      unstable element, divided by the viewport's largest
      dimension

CLS = the largest total score within any single session window

session window:
    shifts less than 1 second apart, maximum 5 seconds total
layout shift score = impact fraction x distance fraction

impact fraction
    = the combined visible area of all unstable elements
      across two frames, as a share of the viewport

distance fraction
    = the greatest horizontal or vertical movement of any
      unstable element, divided by the viewport's largest
      dimension

CLS = the largest total score within any single session window

session window:
    shifts less than 1 second apart, maximum 5 seconds total

Read the impact fraction again. It is an area measure. A thin notification bar that pushes the page down by 60 pixels scores badly not because 60 pixels is far, but because every visible element below it counts as unstable. This is why cookie banners, promotional strips and late-arriving alerts are so expensive: they move the entire viewport.

Google's list of common causes is short and every item on it is a design artefact rather than a bug: images or videos with unknown dimensions, fonts that render larger or smaller than their fallbacks, third-party ads or widgets that resize themselves, resources that load asynchronously, and DOM elements added dynamically. The fix, in each case, is to reserve the space. Width and height attributes on media, from which modern browsers compute an aspect ratio automatically. CSS aspect-ratio where the dimensions are fluid. A min-height on the slot where an ad or an embed will arrive, sized to what you expect.

Two details are worth knowing because they change what you are allowed to design. First, layout shifts that occur within 500 milliseconds of a user input are not counted. An accordion that opens when someone clicks it is free. An accordion that opens by itself is not. Second, composited animations using translate cannot affect other elements, so they do not count towards CLS at all, whereas animating top, left, box-shadow or box-sizing does. The same visual move is either free or costly depending on which property carries it.

What INP actually measures, and why motion is implicated

INP watches clicks, taps and key presses. Scrolling, hovering and zooming are excluded. For each qualifying interaction it measures three phases:

input delay waiting for the main thread before any event handler runs
processing duration running all the event handler callbacks
presentation delay from the last callback finishing to the next frame being painted
input delay waiting for the main thread before any event handler runs
processing duration running all the event handler callbacks
presentation delay from the last callback finishing to the next frame being painted
input delay waiting for the main thread before any event handler runs
processing duration running all the event handler callbacks
presentation delay from the last callback finishing to the next frame being painted

Most discussion of INP concentrates on the middle phase, because that is where JavaScript lives. Designers should pay more attention to the third. Presentation delay is rendering work, and rendering work scales with how much of the page has to be recalculated and repainted. The Chrome team is direct about this: large DOMs do require more work to render than small DOMs, and the problem shows up in response to a user interaction, where a large DOM can cause rendering updates to be very expensive.

An interface that changes one button's colour on tap is cheap. An interface where tapping a filter chip re-lays out a grid of 400 cards, each with its own shadow and hover transition, is not. The CSS property content-visibility exists partly for this, letting the browser skip rendering for elements that are not near the viewport.

Above-the-fold motion cuts both ways here. A running animation competes for the main thread during exactly the window when a user is most likely to make their first interaction, so it inflates input delay. And an autoplaying hero video or a carousel that advances on a timer is both a large LCP resource and a source of layout shift if its container is not sized. The Almanac's figures show INP is already the healthiest of the three on desktop at 97% good, but mobile sits at 74%, which is where the underpowered devices are.

Where the numbers actually sit

Measure

Mobile origins rated good

Desktop origins rated good

Largest Contentful Paint

59%

72%

Cumulative Layout Shift

79%

72%

Interaction to Next Paint

74%

97%

All three Core Web Vitals

43%

54%

Fewer than half of mobile origins pass all three. That is the real state of the web in 2024, measured across the whole HTTP Archive crawl rather than across the sort of sites that write about performance.

What I got wrong

For a long time I treated CLS as a development concern and LCP as a compression concern, and I argued about both after the design was signed off. That order is backwards, and the font case is the clearest proof. By the time a build engineer is looking at font-display, the number of weights, the choice of typeface and the size of the display setting are already fixed, and all three of those are what determine whether any value of font-display can produce a good result. The same is true of the hero. Compression can take a photograph from 1.4 MB to 400 KB. Only a designer can decide that the page opens with type instead.

There is one thing I still cannot answer with evidence. Google publishes the thresholds and the percentile, but I have not found a primary source that explains why 2.5 seconds rather than 2.2 or 3.0, beyond the general statement that the thresholds are set so that a good score represents a good experience. The numbers are stable, widely enforced and worth designing to. They are also, as far as I can tell from the public documentation, conventions rather than derivations.

Sources

  • Google, "Web Vitals", web.dev. Definitions, thresholds and the 75th percentile rule for LCP, CLS and INP. web.dev/articles/vitals

  • Google, "Interaction to Next Paint (INP) becomes a Core Web Vital on March 12", web.dev blog, 12 March 2024. Transition dates, including the 9 September 2024 deadline for CrUX and PageSpeed Insights API users. web.dev/blog/inp-cwv-launch

  • Google, "Largest Contentful Paint (LCP)", web.dev. LCP definition, candidate element types, thresholds. web.dev/articles/lcp

  • Google, "Cumulative Layout Shift (CLS)", web.dev. Layout shift score formula, session window definition, thresholds, common causes. web.dev/articles/cls

  • Google, "Interaction to Next Paint (INP)", web.dev. Interaction types counted, the three phases of interaction latency, reporting methodology. web.dev/articles/inp

  • Google, "Optimize Largest Contentful Paint", web.dev. The four LCP subparts and their typical proportions, lazy-loading and fetchpriority guidance. web.dev/articles/optimize-lcp

  • Google, "Optimize Cumulative Layout Shift", web.dev. Reserving space, aspect-ratio, the 500 ms user-input window, composited transform animations. web.dev/articles/optimize-cls

  • Google, "Optimize Interaction to Next Paint", web.dev. Rendering cost, DOM size, content-visibility, layout thrashing. web.dev/articles/optimize-inp

  • Google, "Best practices for fonts", web.dev. The font-display block and swap period table. web.dev/articles/font-best-practices

  • Google, "Improved font fallbacks", web.dev. size-adjust, ascent-override, descent-override and line-gap-override. web.dev/articles/css-size-adjust

  • Mozilla, "font-display", MDN Web Docs. Font block period, font swap period and font failure period definitions. developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display

  • HTTP Archive, "Performance", The Web Almanac 2024. Origin-level Core Web Vitals pass rates, LCP element content types, lazy-loaded LCP images, fetchpriority adoption. almanac.httparchive.org/en/2024/performance

Related reading

More from the Moc Mallen Studio journal