DEV Community

Samcorp
Samcorp

Posted on

We Cut LCP From 6s to 1.2s Every Change, Measured

We Cut LCP From 6s to 1.2s Every Change, Measured
The page did not look broken.

It loaded.

The hero appeared.

The buttons worked.

Nothing obvious screamed “performance problem.”

But under mobile conditions, Largest Contentful Paint was taking roughly:

6.0 seconds
Enter fullscreen mode Exit fullscreen mode

That was not a small regression.

It was more than twice the 2.5-second threshold generally considered good for LCP.

So instead of throwing every common performance trick at the page, we treated the problem like a debugging exercise.

One change.

One measurement.

Then the next change.

The final result:

Before: 6.0s LCP
After:  1.2s LCP
Change: -80%
Enter fullscreen mode Exit fullscreen mode

The interesting part was not the final number.

It was discovering that the largest image was not initially the biggest problem.

Here is how the LCP optimization unfolded.

Note: This is a representative performance case study. The measurements are designed to illustrate the debugging and optimization process rather than represent a specific customer project.


First, We Made the Test Repeatable

Performance work becomes unreliable when every change is measured under different conditions.

Before touching the application, we created a repeatable baseline.

The same principle applies to performance and load testing: if the environment and conditions keep changing, the results are difficult to compare or trust.

For each version, we:

  • Tested the same URL
  • Used the same mobile profile
  • Started with a cold cache
  • Used the same network conditions
  • Ran multiple tests
  • Ignored obvious outliers
  • Compared the median result
  • Recorded the LCP element and timing breakdown

Lab tools helped us iterate quickly.

Real-user monitoring was still important for validating the final result because actual users have different devices, networks, locations, and cache states.

The lab was our debugging environment.

Field data was our reality check.


The Baseline: Where Were the Six Seconds Going?

Looking at a single LCP number was not enough.

We broke it into four parts:

LCP Component Baseline
Time to First Byte 1.5s
Resource load delay 2.1s
Resource load duration 1.0s
Element render delay 1.4s
Total LCP 6.0s

That table immediately changed our approach.

The hero image took about one second to download.

But the browser spent 2.1 seconds waiting before it even started downloading it.

Compressing the image first would have attacked the wrong bottleneck.


Change #1: Fix the 1.5s TTFB

LCP: 6.0s → 5.0s

The document itself was arriving too slowly.

Our initial request involved application work that did not need to happen for every page view.

Conceptually, the request looked like:

Browser
   ↓
Application
   ↓
Database/API work
   ↓
Template rendering
   ↓
HTML response
Enter fullscreen mode Exit fullscreen mode

Until that HTML starts arriving, the browser cannot discover most of the resources needed to render the page.

We reduced that delay by:

  • Caching cacheable HTML responses
  • Removing unnecessary server-side work
  • Avoiding repeated data fetches
  • Moving non-critical work out of the request path
  • Serving cached responses closer to users where appropriate

The TTFB moved from:

1.5s → 0.5s
Enter fullscreen mode Exit fullscreen mode

Our new breakdown looked approximately like this:

Component After Change #1
TTFB 0.5s
Resource load delay 2.1s
Resource load duration 1.0s
Render delay 1.4s
LCP 5.0s

A one-second improvement was useful.

But five seconds was still terrible.

More importantly, the trace now made the next problem impossible to ignore.


Change #2: Let the Browser Discover the Hero Immediately

LCP: 5.0s → 3.1s

This was the biggest single improvement.

Our LCP element was the hero image.

But the browser could not discover it early.

The implementation effectively depended on other resources being processed before the hero request became visible.

Patterns like this commonly cause trouble:

.hero {
    background-image: url("/images/hero.webp");
}
Enter fullscreen mode Exit fullscreen mode

If that CSS is in an external stylesheet, the browser needs to:

Receive HTML
   ↓
Discover CSS
   ↓
Download CSS
   ↓
Parse CSS
   ↓
Discover hero image
   ↓
Request hero image
Enter fullscreen mode Exit fullscreen mode

The image may be highly compressed.

That does not matter if its request starts two seconds late.

We moved the important visual into the document as an actual image element.

Conceptually:

<picture>
  <source
    srcset="/images/hero-1280.avif"
    type="image/avif"
  >

  <img
    src="/images/hero-1280.webp"
    width="1280"
    height="720"
    fetchpriority="high"
    alt="Application dashboard"
  >
</picture>
Enter fullscreen mode Exit fullscreen mode

We also verified something important:

loading="lazy"
Enter fullscreen mode Exit fullscreen mode

was not applied to the LCP image.

Lazy loading below-the-fold images is useful.

Lazy loading the element that is supposed to become your largest above-the-fold paint is usually counterproductive.

After the change:

Resource load delay:

2.1s → 0.2s
Enter fullscreen mode Exit fullscreen mode

And overall LCP dropped:

5.0s → 3.1s
Enter fullscreen mode Exit fullscreen mode

That single change saved almost two seconds.

This was the most important lesson from the entire investigation:

Do not only ask how fast the LCP resource downloads. Ask how quickly the browser discovers it.


Change #3: Now We Optimized the Image

LCP: 3.1s → 2.4s

Only now did image optimization become the largest obvious opportunity.

The original hero asset was much larger than necessary.

We addressed three things.

Format

We generated modern image variants instead of sending the same large source everywhere.

Dimensions

A mobile device did not need an image sized for a large desktop display.

Responsive image candidates allowed the browser to choose an appropriate resource.

For example:

<img
  src="/images/hero-1280.webp"
  srcset="
    /images/hero-640.webp 640w,
    /images/hero-960.webp 960w,
    /images/hero-1280.webp 1280w
  "
  sizes="100vw"
  width="1280"
  height="720"
  fetchpriority="high"
  alt="Application dashboard"
>
Enter fullscreen mode Exit fullscreen mode

Compression

We reduced bytes without making the hero visibly degraded.

The result was a substantial reduction in resource transfer time:

Resource load duration:

1.0s → 0.3s
Enter fullscreen mode Exit fullscreen mode

Overall:

3.1s → 2.4s
Enter fullscreen mode Exit fullscreen mode

We had finally crossed the 2.5-second line in our controlled test.

But stopping here would have meant leaving almost another second on the table.


Change #4: Remove Work Blocking the Paint

LCP: 2.4s → 1.5s

At this point the hero was:

  • Discovered early
  • Prioritized
  • Relatively small
  • Downloaded quickly

Yet there was still a noticeable gap between:

Image downloaded
Enter fullscreen mode Exit fullscreen mode

and:

Image painted
Enter fullscreen mode Exit fullscreen mode

That is element render delay.

The culprit was not the image anymore.

The browser's critical rendering path contained resources that delayed the initial render.

We found:

  • CSS that was not required above the fold
  • Scripts executing before they were needed
  • Third-party code competing for the main thread
  • Application code doing work before initial content could settle

We started removing things from the critical path.

Non-critical JavaScript was deferred where possible:

<script src="/analytics.js" defer></script>
Enter fullscreen mode Exit fullscreen mode

Non-essential functionality stopped competing with the first render.

We also reviewed the CSS loaded before the hero appeared.

This kind of web performance optimization is easier when frontend architecture, asset loading, and rendering behavior are treated as part of the same performance problem.

The rule was simple:

If the resource is not necessary to display the first screen, why is the first screen waiting for it?

After that cleanup:

Element render delay:

1.4s → 0.5s
Enter fullscreen mode Exit fullscreen mode

LCP:

2.4s → 1.5s
Enter fullscreen mode Exit fullscreen mode

The page now felt dramatically different even though visually almost nothing had changed.


Change #5: Reduce Main-Thread Work Before LCP

LCP: 1.5s → 1.2s

The final improvement was smaller, but it exposed another useful performance pattern.

The main thread was still doing too much work early.

Large JavaScript bundles were being:

downloaded
    ↓
parsed
    ↓
compiled
    ↓
executed
Enter fullscreen mode Exit fullscreen mode

before some of that functionality was actually needed.

We inspected long tasks occurring before LCP and asked:

  • Does this code need to execute immediately?
  • Can this component initialize after the initial render?
  • Can this bundle be split?
  • Is a third-party script blocking useful work?
  • Are we hydrating components that are below the fold?
  • Are we shipping JavaScript for features that are not used on this route?

We delayed non-critical initialization and reduced early JavaScript execution.

The remaining render delay dropped:

0.5s → 0.2s
Enter fullscreen mode Exit fullscreen mode

That brought the final measurement to:

1.2s LCP
Enter fullscreen mode Exit fullscreen mode

Every Change, Side by Side

Here is the complete progression:

Step LCP Improvement
Baseline 6.0s
Faster TTFB 5.0s -1.0s
Earlier LCP discovery 3.1s -1.9s
Responsive optimized image 2.4s -0.7s
Remove render-blocking work 1.5s -0.9s
Reduce early main-thread work 1.2s -0.3s

Overall:

6.0 seconds
     ↓
1.2 seconds
Enter fullscreen mode Exit fullscreen mode

An 80% reduction.

But the table also shows why random optimization is inefficient.

Our largest improvement did not come from compressing the image.

It came from starting its request earlier.


The Final LCP Breakdown

The baseline:

TTFB                  1.5s
Resource load delay   2.1s
Resource load time    1.0s
Render delay          1.4s
                     -----
LCP                    6.0s
Enter fullscreen mode Exit fullscreen mode

After optimization:

TTFB                  0.5s
Resource load delay   0.2s
Resource load time    0.3s
Render delay          0.2s
                     -----
LCP                    1.2s
Enter fullscreen mode Exit fullscreen mode

That decomposition became much more useful than the Lighthouse score itself.

A score tells you something is wrong.

A timing breakdown tells you where to look.


What We Did Not Do

Performance work also requires knowing which optimizations not to make.

We Did Not Lazy-Load the Hero

Lazy loading is valuable for resources outside the viewport.

The LCP image was immediately visible.

Delaying it made no sense.

We Did Not Preload Everything

Preload is not a general-purpose “make the site faster” switch.

Making every resource high priority means nothing is actually high priority.

We reserved resource priority for things that genuinely affected the initial experience.

We Did Not Optimize Only for Lighthouse

Lab measurements helped us compare changes quickly.

But real users do not all have the same CPU, connection, viewport, cache state, or geographic location.

Field performance still mattered.

We Did Not Start by Removing Random JavaScript

“JavaScript is slow” was not a useful diagnosis.

We identified which main-thread work overlapped the LCP window and targeted that work specifically.


Our LCP Optimization Checklist Changed After This

Before this investigation, it was tempting to start with:

Compress the hero image.
Enter fullscreen mode Exit fullscreen mode

Now the order is different.

1. Identify the Actual LCP Element

Do not assume it is the hero image.

Measure it.

2. Break LCP Into Its Components

Look separately at:

  • TTFB
  • Resource load delay
  • Resource load duration
  • Element render delay

3. Fix the Largest Delay First

Do not optimize what is easiest.

Optimize what is expensive.

4. Make the LCP Resource Discoverable

If it is an image, the browser should ideally discover it directly from the HTML.

Avoid unnecessary dependency chains.

5. Give the Resource Appropriate Priority

For an important above-the-fold image, consider:

fetchpriority="high"
Enter fullscreen mode Exit fullscreen mode

Do not lazily load it.

6. Send Fewer Bytes

Use:

  • Appropriately sized images
  • Responsive sources
  • Modern formats
  • Sensible compression
  • Efficient delivery

7. Look at What Happens After Download

A fully downloaded LCP resource can still sit around waiting to render.

Inspect:

  • Render-blocking CSS
  • Synchronous scripts
  • Long tasks
  • Client-side rendering
  • Hydration
  • Third-party JavaScript

8. Validate With Real Users

Your development laptop is not your audience.

Measure the distribution of actual experiences.


Lab Data and Field Data Answer Different Questions

This distinction matters.

During optimization, we wanted to know:

Did this specific change improve the page?

Controlled lab testing is excellent for that.

But after deployment, the question changes:

Are real users actually getting a good experience?

That requires field data.

A single 1.2-second Lighthouse result does not prove that 75% of production users have a 1.2-second LCP.

Real users bring:

different phones
different networks
different locations
different cache states
different viewport sizes
different browser conditions
Enter fullscreen mode Exit fullscreen mode

So we used lab measurements for iteration and field measurements for validation.

That prevented us from confusing a fast benchmark with a fast website.


The Biggest Lesson Was Not “Compress Your Images”

Image optimization mattered.

Caching mattered.

JavaScript mattered.

CSS mattered.

But none of those were the real lesson.

The lesson was:

LCP optimization works better when you debug time instead of applying performance tips.

We started with six seconds.

Then asked where those six seconds were going.

6.0s
├── 1.5s waiting for HTML
├── 2.1s waiting to request the hero
├── 1.0s downloading the hero
└── 1.4s waiting to render it
Enter fullscreen mode Exit fullscreen mode

That immediately gave us a priority order.

By the end:

1.2s
├── 0.5s TTFB
├── 0.2s resource load delay
├── 0.3s resource load duration
└── 0.2s render delay
Enter fullscreen mode Exit fullscreen mode

We did not make the browser magically faster.

We simply stopped making it wait.


Final Takeaway

If your LCP is six seconds, resist the urge to immediately install another optimization plugin, compress every asset, or rewrite the frontend.

Measure first.

Find the LCP element.

Break the metric into its timing components.

Then attack the largest delay.

For us, the sequence was:

6.0s
 ↓
5.0s   Faster server response
 ↓
3.1s   Earlier LCP discovery
 ↓
2.4s   Smaller responsive image
 ↓
1.5s   Less render blocking
 ↓
1.2s   Less main-thread work
Enter fullscreen mode Exit fullscreen mode

Every change had a reason.

Every change had a measurement.

And that is the part of LCP optimization that matters most:

Do not optimize based on assumptions. Optimize based on evidence.

Top comments (0)