---
title: "Fix Client-Side Rendering for Better LCP"
description: "How to improve LCP by using server-side rendering or pre-rendering instead of client-side rendering."
canonical_url: "https://unlighthouse.dev/learn-lighthouse/lcp/client-side-rendering"
last_updated: "2025-01-12"
---

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 you can't find it, 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.

```jsx
// 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
}
```

```html
<!-- 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.

```jsx
// 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} />
}
```

```ts
// 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.

```jsx
// 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, make sure the LCP element is in the initial HTML.

```jsx
// 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.

```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

<callout icon="i-logos-nextjs-icon">

**Next.js**

Use App Router (default SSR) or Pages Router with `getServerSideProps`/`getStaticProps`. Never use `useEffect` for LCP content.

</callout>

<callout icon="i-logos-nuxt-icon">

**Nuxt**

Nuxt defaults to SSR. Use `useFetch` or `useAsyncData` (not `$fetch` in `onMounted`) for data that affects LCP.

</callout>

<callout icon="i-logos-react">

**React (SPA)**

Consider migrating to [Next.js](https://nextjs.org), [Remix](https://remix.run), or [Astro](https://astro.build). For pure React SPAs, pre-render critical pages with tools like `react-snap`.

</callout>

<callout icon="i-logos-vue">

**Vue (SPA)**

Consider migrating to Nuxt. For pure Vue SPAs, use `vite-plugin-ssr` or pre-render with `vite-ssg`.

</callout>

## 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

<table>
<thead>
  <tr>
    <th>
      Approach
    </th>
    
    <th>
      LCP
    </th>
    
    <th>
      Interactivity
    </th>
    
    <th>
      Complexity
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      CSR
    </td>
    
    <td>
      Poor
    </td>
    
    <td>
      Fast after load
    </td>
    
    <td>
      Simple
    </td>
  </tr>
  
  <tr>
    <td>
      SSR
    </td>
    
    <td>
      Good
    </td>
    
    <td>
      Delayed (hydration)
    </td>
    
    <td>
      Medium
    </td>
  </tr>
  
  <tr>
    <td>
      SSG
    </td>
    
    <td>
      Best
    </td>
    
    <td>
      Delayed (hydration)
    </td>
    
    <td>
      Medium
    </td>
  </tr>
  
  <tr>
    <td>
      Streaming SSR
    </td>
    
    <td>
      Good
    </td>
    
    <td>
      Progressive
    </td>
    
    <td>
      Higher
    </td>
  </tr>
</tbody>
</table>

## Related issues

Often appears alongside:

- [Slow Server Response](/learn-lighthouse/lcp/slow-server-response) - SSR adds server processing time
- [Large Images](/learn-lighthouse/lcp/large-images) - Still need to optimize images with SSR

## 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.
