IT
🚀

Improving Core Web Vitals in Practice — How I Cut LCP from 3s to 1.2s

LCP (Largest Contentful Paint) directly impacts Google search rankings. By sequentially applying image optimization, font loading strategy, and code splitting, LCP was reduced from 3.0s to 1.2s, and CLS dropped from 0.28 to 0.04.

Key Summary

  • LCP (Largest Contentful Paint) is a core metric that directly impacts Google search rankings.
  • By sequentially applying image optimization, font loading strategy, and code splitting, LCP was reduced from 3.0 seconds to 1.2 seconds.
  • CLS was improved from 0.28 to 0.04 by specifying explicit dimensions for images and ad areas and eliminating layout shifts.
  • Based on a real production Next.js project — the same principles apply to React and Vue projects.

Introduction — Why Core Web Vitals?

When Google rolled out the Page Experience update in 2021, Core Web Vitals moved beyond simple UX metrics and became an SEO ranking factor. Since then, Google has continued increasing the weight of these signals, and as of 2024, INP (Interaction to Next Paint) has fully replaced FID as an evaluation metric.

An e-commerce project I operate was scoring a dismal 38 points on PageSpeed Insights mobile just six months ago. LCP was 3.0 seconds, CLS was 0.28, INP was 320ms — all three metrics in the "Needs Improvement" zone. This article documents the entire process of turning those numbers around.


How Can You Quickly Understand the Three Core Web Vitals?

LCP — The "First Impression" Speed of a Page

LCP measures the time it takes for the largest content element in the viewport (usually the hero image or H1 text) to render. Google's threshold is 2.5 seconds or less for Good; above 4.0 seconds is Poor.

On e-commerce sites, the hero banner image is overwhelmingly the LCP candidate. If this one image loads too slowly, every other optimization becomes meaningless.

CLS — Does the Layout "Jump"?

CLS (Cumulative Layout Shift) measures how much elements unexpectedly shift during page loading. Late-loading ad banners or images without explicit dimensions cause CLS to spike. 0.1 or below is the Good threshold.

INP — How Quickly Does the Page Respond to User Input?

INP measures the response delay for all interactions — clicks, taps, keyboard input. It replaced FID in March 2024, and 200ms or below is the Good threshold. Heavy JavaScript bundles block the main thread and worsen INP.


Diagnosing the Problem — Initial Measurements

Accurately recording metrics before optimization is the starting point. These tools were used in order:

  1. 1PageSpeed Insights — Provides both field data (real user data) and lab data (Lighthouse) simultaneously
  2. 2Chrome DevTools > Performance tab — Inspect rendering timeline and LCP candidate element
  3. 3WebPageTest — Real-world measurement from CDN edge servers, visual confirmation via filmstrip
  4. 4Vercel Analytics / Sentry — Core Web Vitals collected from real user sessions

The diagnosis narrowed the bottlenecks to three areas:

  • Hero image — 4.2MB JPEG, inserted directly via tag with no optimization
  • Google Fonts — 3 font families loaded synchronously via @import
  • Bundle size — 1.8MB main chunk, multiple libraries included without tree-shaking

What Were the Three Methods That Cut LCP from 3.0s to 1.2s?

Method 1 — Image Optimization and Preload

This had the biggest impact. The hero image handling was completely redesigned.

Before

html
<img src="/banner.jpg" alt="Main Banner" />

After — Next.js Image Component + WebP Conversion

jsx
import Image from 'next/image';

<Image
  src="/banner.webp"
  alt="Main Banner"
  width={1920}
  height={800}
  priority          // LCP target images must always have priority
  quality={80}
  sizes="(max-width: 768px) 100vw, 1920px"
/>

Adding the priority prop alone causes Next.js to automatically inject a tag for the image. After converting to WebP, file size dropped from 4.2MB to 340KB — approximately a 92% reduction.

For pure HTML/React projects, add a preload tag directly in :

html
<link
  rel="preload"
  as="image"
  href="/banner.webp"
  type="image/webp"
/>

This single change dropped LCP from 3.0s to 1.8s.

Method 2 — Font Loading Strategy

Loading Google Fonts via @import causes the browser to pause CSS parsing and make a font request. This wait time substantially increases LCP.

Before

html
<style>
  @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap');
</style>

After — + display=swap + Self-hosting

html
<!-- Step 1: Preconnect to eliminate DNS resolution delay -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />

<!-- Step 2: Load asynchronously with display=swap -->
<link
  href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap"
  rel="stylesheet"
  media="print"
  onload="this.media='all'"
/>

Using media="print" then switching to media="all" on load causes the font CSS to load asynchronously without blocking rendering. display=swap displays a fallback font immediately while the web font loads, keeping text visible.

For maximum performance, self-hosting fonts eliminates the Google Fonts round-trip entirely:

css
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('/fonts/inter-regular.woff2') format('woff2');
}

This step brought LCP down from 1.8s to 1.4s.

Method 3 — Code Splitting and Bundle Optimization

A 1.8MB main chunk means users must download and parse nearly 2MB of JavaScript before the page becomes interactive.

Key actions taken:

  1. 1Dynamic imports — Heavy libraries loaded only when needed:
jsx
// Before: always loaded
import Chart from 'chart.js';

// After: loaded only when the chart component is actually rendered
const Chart = dynamic(() => import('chart.js'), { ssr: false });
  1. 1Tree-shaking audit — Replaced import _ from 'lodash' with individual function imports:
js
// Before: loads entire lodash library
import _ from 'lodash';

// After: loads only debounce
import debounce from 'lodash/debounce';
  1. 1Third-party script lazy loading — Analytics and chat widget scripts deferred to load after page is interactive:
html
<script src="/analytics.js" defer></script>

After bundle optimization, main chunk size dropped from 1.8MB to 620KB, and LCP improved from 1.4s to 1.2s.


How Was CLS Reduced from 0.28 to 0.04?

The two biggest CLS offenders were ads and images without explicit dimensions.

Ad Area: Reserve Space in Advance

css
/* Reserve minimum height so ad slot never causes layout shift */
.ad-container {
  min-height: 90px;  /* banner ad */
  min-width: 728px;
}

Images: Always Specify Width/Height

jsx
/* Before: no dimensions → causes layout shift during load */
<img src="/product.jpg" alt="Product" />

/* After: explicit dimensions → browser reserves space */
<Image
  src="/product.jpg"
  alt="Product"
  width={400}
  height={300}
/>

Dynamic Content: Use Placeholders

jsx
// Show skeleton placeholder while data is loading
{isLoading ? (
  <div className="h-48 bg-gray-200 animate-pulse rounded" />
) : (
  <ProductCard data={data} />
)}

These three changes brought CLS from 0.28 down to 0.04.


Results Summary

MetricBeforeAfterChange
LCP3.0s1.2s-60%
CLS0.280.04-86%
INP320ms95ms-70%
PageSpeed Mobile3891+53 pts

Check Your Own Site's Core Web Vitals

Use these tools to measure your current score:


Frequently Asked Questions (FAQ)

Q1. Does improving Core Web Vitals directly raise search rankings?

Core Web Vitals are one of Google's official ranking factors. Google has publicly confirmed they are a Page Experience signal. However, other factors like content quality and backlinks still carry more weight, so Core Web Vitals improvement should be viewed as a necessary condition rather than a guarantee.

Q2. Which Core Web Vitals metric has the biggest impact on rankings?

LCP is the most visible to users and has the most direct impact on bounce rates, which indirectly affects rankings. CLS also directly affects UX, so both are high-priority targets. INP's influence as a newer metric is still being established.

Q3. How do I check my LCP element?

Open Chrome DevTools, go to the Performance tab, record a page load, and look for "LCP" in the timeline. Alternatively, running PageSpeed Insights will tell you exactly which element is the LCP candidate.

Q4. Does Google Fonts hurt Core Web Vitals?

Yes. Loading Google Fonts via @import blocks rendering and is a common cause of LCP degradation. Using combined with display=swap or self-hosting fonts is strongly recommended.

Q5. How much does Next.js help with Core Web Vitals?

Quite significantly. Next.js provides automatic image optimization (WebP conversion, lazy loading, size optimization), built-in font optimization (next/font), and route-based code splitting as built-in features. These alone can lead to dramatic Core Web Vitals improvements without extra configuration.

Q6. How often should Core Web Vitals be checked?

Monitoring at least once a month is recommended. PageSpeed Insights reflects real user data collected over the past 28 days, so changes take time to appear. Using Google Search Console to track field data trends over time is more practical than one-off measurements.

🔧 Related Free Tools

Related Products (Core Web Vitals)[Ad/Affiliate]

As an Amazon Associate, Coupang Partner, and AliExpress affiliate, I earn from qualifying purchases at no extra cost to you.

Related Posts