perf hacks
Core Web VitalsLCP

LCP - Largest Contentful Paint

Deep dive into optimizing Largest Contentful Paint for faster perceived load times

Largest Contentful Paint (LCP) measures how long it takes for the largest content element in the viewport to become visible. It's the most impactful Core Web Vital for perceived performance.

LCP BREAKDOWNTTFB<800msResourceLoad DelayResource Load TimeRenderDelayLCPTarget<2.5s0ms~800ms~2000ms

Understanding LCP

Score Thresholds

ScoreRatingUser Perception
≤ 2.5sGoodPage feels fast
2.5s - 4sNeeds ImprovementNoticeable delay
> 4sPoorFrustrating wait

What Elements Count as LCP?

The LCP element can be:

  • <img> elements
  • <image> inside SVG
  • <video> poster images
  • Background images via url()
  • Block-level text elements (<p>, <h1>, etc.)

The LCP element often changes as the page loads. The final LCP is measured when the largest element finishes rendering.

LCP Breakdown

LCP consists of four sub-parts:

LCP=TTFB+Load Delay+Load Time+Render Delay\text{LCP} = \text{TTFB} + \text{Load Delay} + \text{Load Time} + \text{Render Delay}
ComponentTargetDescription
TTFB< 800msServer response time
Resource Load Delay< 10% of LCPTime before resource starts loading
Resource Load TimeMinimizeTime to download the resource
Render Delay< 10% of LCPTime from load to paint

Common LCP Issues in Next.js

1. Non-Priority LCP Images

// ❌ Bad: LCP image loads lazily by default
<Image src="/hero.jpg" alt="Hero" width={1200} height={600} />

// ✅ Good: Priority loading for LCP image
<Image
  src="/hero.jpg"
  alt="Hero"
  width={1200}
  height={600}
  priority
/>

2. Slow Server Response (TTFB)

// ❌ Bad: Fetching data on every request
export async function getServerSideProps() {
  const data = await fetchFromSlowAPI();
  return { props: { data } };
}

// ✅ Good: Use ISR for better TTFB
export async function getStaticProps() {
  const data = await fetchFromSlowAPI();
  return {
    props: { data },
    revalidate: 60
  };
}

3. Render-Blocking Resources

// ❌ Bad: Loading heavy component synchronously
import HeavyChart from '@/components/HeavyChart';

// ✅ Good: Dynamic import for non-LCP content
import dynamic from 'next/dynamic';

const HeavyChart = dynamic(() => import('@/components/HeavyChart'), {
  loading: () => <ChartSkeleton />,
  ssr: false
});

4. Large Unoptimized Images

// ❌ Bad: Full-size image without optimization
<img src="/hero-4000x2000.jpg" alt="Hero" />

// ✅ Good: Responsive images with Next.js
<Image
  src="/hero.jpg"
  alt="Hero"
  fill
  sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 1200px"
  priority
/>

Optimization Strategies

Preload Critical Resources

app/layout.tsx
import Head from 'next/head';

export default function Layout({ children }) {
  return (
    <>
      <Head>
        {/* Preload LCP image */}
        <link
          rel="preload"
          as="image"
          href="/hero.webp"
          type="image/webp"
        />
        {/* Preconnect to image CDN */}
        <link
          rel="preconnect"
          href="https://images.example.com"
        />
      </Head>
      {children}
    </>
  );
}

Optimize Hero Section

components/Hero.tsx
import Image from 'next/image';

export function Hero() {
  return (
    <section className="relative h-[600px]">
      {/* LCP image with all optimizations */}
      <Image
        src="/hero.jpg"
        alt="Hero background"
        fill
        priority
        quality={85}
        sizes="100vw"
        placeholder="blur"
        blurDataURL="data:image/jpeg;base64,/9j/4AAQ..."
        className="object-cover"
      />

      {/* Text content renders immediately */}
      <div className="relative z-10 p-8">
        <h1 className="text-5xl font-bold">Welcome</h1>
      </div>
    </section>
  );
}

Server-Side Rendering Optimization

// Use streaming for faster initial paint
import { Suspense } from 'react';

export default function Page() {
  return (
    <>
      {/* Critical content renders first */}
      <Hero />

      {/* Non-critical content streams in */}
      <Suspense fallback={<ProductsSkeleton />}>
        <ProductList />
      </Suspense>
    </>
  );
}

Font Loading Strategy

// Fonts can be LCP elements if they're the largest text
import { Inter } from 'next/font/google';

const inter = Inter({
  subsets: ['latin'],
  display: 'swap',
  preload: true,
  fallback: ['system-ui', 'arial'],
});

export default function RootLayout({ children }) {
  return (
    <html className={inter.className}>
      <body>{children}</body>
    </html>
  );
}

Diagnosing LCP Issues

Using Chrome DevTools

Open Performance panel Press F12 → Performance tab → Record a page load

Find LCP marker Look for the "LCP" label in the Timings row

Identify the element Hover over LCP to see which element triggered it

Analyze the waterfall Check when the LCP resource started/finished loading

Programmatic Measurement

lib/web-vitals.ts
import { onLCP, type LCPMetric } from 'web-vitals';

export function measureLCP() {
  onLCP((metric: LCPMetric) => {
    console.log('LCP:', metric.value, 'ms');
    console.log('LCP Element:', metric.entries[0]?.element);

    // Send to analytics
    sendToAnalytics({
      name: 'LCP',
      value: metric.value,
      rating: metric.rating,
      element: metric.entries[0]?.element?.tagName,
    });
  });
}

Advanced Techniques

Critical CSS Inlining

next.config.js
module.exports = {
  experimental: {
    optimizeCss: true, // Enables CSS optimization
  },
};

Image CDN Configuration

next.config.js
module.exports = {
  images: {
    loader: 'custom',
    loaderFile: './lib/image-loader.ts',
    formats: ['image/avif', 'image/webp'],
    deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048],
    imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
  },
};

Predictive Prefetching

// Prefetch likely next pages
import { useRouter } from 'next/router';
import { useEffect } from 'react';

function usePrefetchOnHover(href: string) {
  const router = useRouter();

  return {
    onMouseEnter: () => router.prefetch(href),
    onFocus: () => router.prefetch(href),
  };
}

LCP Optimization Checklist

Images

Add priority to LCP images The most critical optimization - tells Next.js to preload the hero image.

Use WebP/AVIF formats Modern formats are 25-50% smaller than JPEG/PNG.

Implement responsive sizes Ensure correct image size loads for each viewport.

Add blur placeholder Improves perceived performance while images load.

Preload critical images Use <link rel="preload"> for above-the-fold images.

Server Response

Use static generation when possible Pre-rendered pages have the fastest TTFB.

Implement ISR for dynamic content Best of both worlds: static speed with dynamic updates.

Add CDN caching headers Cache static assets and responses at the edge.

Optimize database queries Slow queries directly impact server response time.

Use edge functions for personalization Move personalization logic closer to users.

Rendering

Minimize render-blocking resources Defer or inline CSS/JS that blocks first paint.

Inline critical CSS Include above-the-fold styles directly in the HTML.

Defer non-critical JavaScript Load analytics and widgets after main content.

Use streaming SSR with Suspense Start sending HTML before all data is ready.

Reduce DOM size Fewer nodes means faster rendering.

Real-World Impact

OptimizationLCP Improvement
Adding priority to hero image-300ms to -800ms
SSG instead of SSR-200ms to -500ms
Image CDN (Cloudflare/Vercel)-100ms to -400ms
Font optimization-50ms to -200ms
Critical CSS inlining-50ms to -150ms

On this page