Fix Client-Side Rendering for Better LCP

How to improve LCP by using server-side rendering or pre-rendering instead of client-side rendering.
Harlan WiltonHarlan Wilton6 min read Published

When content only appears after JavaScript runs, LCP is delayed until the JS downloads, parses, executes, and renders. This can add seconds to LCP.

What's the Problem?

Client-side rendering (CSR) means the server sends a minimal HTML shell, and JavaScript builds the actual content in the browser.

The LCP timeline with CSR:

  1. Download HTML shell (fast)
  2. Download JavaScript bundle (slow)
  3. Parse and execute JavaScript (slow)
  4. Fetch data from API (slow)
  5. Render content (finally, LCP fires)

With server-side rendering:

  1. Download HTML with content already included (LCP fires)

How to Identify This Issue

View Page Source

  1. Right-click → "View Page Source"
  2. Search for your main content text
  3. If it's not there, you're using client-side rendering

Lighthouse

Look for:

  • High "Total Blocking Time"
  • Large "JavaScript execution time"
  • LCP element rendered by JavaScript (shown in audit details)

Chrome DevTools

  1. Disable JavaScript: Settings → Debugger → Disable JavaScript
  2. Reload the page
  3. If your content disappears, it's client-side rendered

The Fix

1. Use Server-Side Rendering (SSR)

Render HTML on the server so content is available immediately.

// Next.js - automatic SSR with App Router
export default async function Page() {
  const data = await fetchData() // Runs on server

  return <Content data={data} /> // HTML sent to browser
}
<!-- Nuxt - automatic SSR -->
<script setup>
const { data } = await useFetch('/api/content')
</script>

<template>
  <Content :data="data" />
</template>

2. Use Static Site Generation (SSG)

Pre-render pages at build time for the fastest possible LCP.

// Next.js - static generation
export async function generateStaticParams() {
  const posts = await getPosts()
  return posts.map(post => ({ slug: post.slug }))
}

export default async function Page({ params }) {
  const post = await getPost(params.slug)
  return <Article post={post} />
}
// Nuxt - static generation
export default defineNuxtConfig({
  routeRules: {
    '/blog/**': { prerender: true }
  }
})

3. Streaming SSR

Send HTML progressively so users see content before the entire page is ready.

// Next.js App Router - automatic streaming
import { Suspense } from 'react'

export default function Page() {
  return (
    <>
      <Header />
      {' '}
      {/* Sent immediately */}
      <HeroImage />
      {' '}
      {/* LCP element - sent immediately */}
      <Suspense fallback={<Loading />}>
        <SlowContent />
        {' '}
        {/* Streamed later */}
      </Suspense>
    </>
  )
}

4. Hybrid Approach

If you must use CSR for some parts, ensure the LCP element is in the initial HTML.

// LCP element server-rendered, interactive parts client-rendered
export default function Page() {
  return (
    <>
      {/* Server-rendered, appears immediately */}
      <img src="/hero.jpg" alt="Hero" />
      <h1>Page Title</h1>

      {/* Client-rendered, but not LCP */}
      <InteractiveWidget />
    </>
  )
}

5. Inline Critical Data

If you need data for the LCP element, inline it in the HTML.

<script id="initial-data" type="application/json">
  {"hero": {"title": "Welcome", "image": "/hero.jpg"}}
</script>

<script>
  // No network request needed for initial render
  const data = JSON.parse(document.getElementById('initial-data').textContent)
</script>

Framework-Specific Solutions

Next.jsUse App Router (default SSR) or Pages Router with getServerSideProps/getStaticProps. Never use useEffect for LCP content.
NuxtNuxt defaults to SSR. Use useFetch or useAsyncData (not $fetch in onMounted) for data that affects LCP.
React (SPA)Consider migrating to Next.js, Remix, or Astro. For pure React SPAs, pre-render critical pages with tools like react-snap.
Vue (SPA)Consider migrating to Nuxt. For pure Vue SPAs, use vite-plugin-ssr or pre-render with vite-ssg.

Verify the Fix

After implementing:

  1. View Page Source — content should be visible
  2. Disable JavaScript — content should still appear
  3. Run Lighthouse — LCP should improve significantly

Expected improvement: Moving from CSR to SSR can improve LCP by 1-3 seconds.

Common Mistakes

  • Hydration mismatch — Server and client must render the same content. Mismatches cause re-renders and can hurt LCP.
  • Blocking on non-critical data — Only wait for data needed for LCP. Load other data after.
  • Large server bundles — SSR with huge dependencies increases TTFB. Keep server code lean.

Trade-offs

ApproachLCPInteractivityComplexity
CSRPoorFast after loadSimple
SSRGoodDelayed (hydration)Medium
SSGBestDelayed (hydration)Medium
Streaming SSRGoodProgressiveHigher

Often appears alongside:

Test Your Entire Site

Some pages may use CSR while others use SSR. Unlighthouse scans your entire site and identifies pages with client-side rendering issues.