How to Optimize Largest Contentful Paint in Modern JavaScript Frameworks
A practical guide to fixing the most impactful Core Web Vitals metric in React, Next.js, and Vue applications
Largest Contentful Paint measures how long it takes for the biggest visible element on your page to finish rendering. Google considers anything under 2.5 seconds "good," but if you are building with React, Next.js, Vue, or Nuxt, you are fighting an uphill battle. Frameworks add layers of abstraction between your code and the browser's first paint, and each layer has the potential to slow things down.
The frustrating part is that most LCP problems in framework-driven sites are not caused by slow servers or massive images. They come from how the framework itself delivers and hydrates content. A static HTML page with the same content would often paint two to three times faster.
This guide walks through the specific steps to diagnose and fix LCP in JavaScript framework projects, with code examples for React and Next.js that you can adapt to Vue and Nuxt.
Step 1: Identify Your LCP Element
Before optimizing anything, you need to know which element the browser considers the LCP candidate. Open Chrome DevTools, go to the Performance panel, and record a page load. In the Timings track, find the LCP marker and click it. The element will be highlighted in the page.
You can also identify it programmatically using the web-vitals library:
import { onLCP } from 'web-vitals';
onLCP((metric) => {
console.log('LCP value:', metric.value);
console.log('LCP element:', metric.entries[0].element);
});
Common LCP elements include hero images, <h1> headings with custom fonts, and video poster images. In single-page applications, the LCP element often changes between routes, so test multiple entry points.
According to the web.dev LCP documentation, the browser recalculates the LCP candidate as new content appears, which means dynamically rendered content that loads late can become the LCP element and tank your score.
Step 2: Optimize Server-Side Rendering
Client-side rendered React apps have a fundamental LCP problem. The browser receives an empty HTML shell, downloads the JavaScript bundle, parses it, executes it, and only then renders content. By the time the LCP element appears, several seconds have passed.
Server-side rendering (SSR) fixes this by sending fully rendered HTML from the server. The browser can paint content immediately while JavaScript loads in the background. If you are using Next.js, React Server Components take this further by keeping component logic on the server entirely:
// This component never ships JavaScript to the client
// app/page.jsx (Server Component by default in Next.js App Router)
async function HeroSection() {
const data = await fetch('https://api.example.com/hero');
const hero = await data.json();
return (
<section>
<h1>{hero.title}</h1>
<img
src={hero.imageUrl}
alt={hero.imageAlt}
width={1200}
height={630}
fetchPriority="high"
/>
</section>
);
}
The key optimization here is the fetchPriority="high" attribute on the LCP image. This tells the browser to prioritize downloading that image over other resources. The MDN fetchpriority documentation covers browser support details.
Photo by Mikhail Nilov on Pexels
For Vue and Nuxt applications, the same principle applies. Use nuxt generate or server-side rendering mode rather than pure SPA mode for any page where LCP matters. Nuxt 3 supports hybrid rendering, where you can SSR landing pages while keeping internal dashboard routes client-rendered.
Step 3: Preload Critical Resources
Even with SSR, the browser still needs to discover and download the LCP resource. If your LCP element is an image, it might not start downloading until the browser parses the HTML, encounters the <img> tag, and begins the request. By preloading, you move that request earlier in the waterfall.
Add a preload hint in your document head:
<link rel="preload" as="image" href="/hero.webp" fetchpriority="high" />
In Next.js, use the next/head component or metadata API. For custom fonts that serve as the LCP element (large headings), preload the font file:
<link rel="preload" as="font" href="/fonts/heading.woff2"
type="font/woff2" crossorigin />
The Chrome DevTools Network panel shows the request waterfall. Look for the LCP resource and check if it starts downloading late. If there is a gap between navigation start and the image request, preloading will close it.
Step 4: Optimize Images for Faster Decoding
Image optimization directly impacts LCP when the largest element is visual. Modern formats like WebP and AVIF reduce file sizes by 30-50% compared to JPEG. The Squoosh app lets you visually compare compression levels before committing.
In Next.js, the built-in Image component handles format conversion, resizing, and lazy loading automatically. But for the LCP image specifically, disable lazy loading:
import Image from 'next/image';
<Image
src="/hero.webp"
alt="Product showcase"
width={1200}
height={630}
priority // disables lazy loading, adds preload hint
/>
The priority prop is critical. Without it, Next.js lazy-loads the image by default, which delays LCP significantly. The Next.js Image optimization docs explain this in detail.
For non-Next.js projects, use the native loading="eager" attribute (the default) and decoding="async" to avoid blocking the main thread during image decode.
Step 5: Reduce JavaScript Bundle Impact
Large JavaScript bundles delay hydration, which delays interactive rendering. Even though SSR sends HTML early, the framework needs to hydrate (attach event listeners and reconcile state) before the page is fully functional. Heavy hydration can block the main thread and delay LCP in some cases.
Use webpack-bundle-analyzer or the built-in Next.js bundle analysis tool to identify oversized dependencies. Common offenders include moment.js (use date-fns instead), lodash (import individual functions), and heavy charting libraries loaded on landing pages.
"The fastest JavaScript is the JavaScript you never ship to the client. Every kilobyte you trim from your bundle is time saved on every single page load." - Dennis Traina, 137Foundry
Code splitting with React.lazy and dynamic imports ensures that only the code needed for the current route loads on initial navigation. This reduces main thread work during hydration and gives the browser more time to paint the LCP element quickly.
For teams that need help profiling and optimizing framework performance, the web performance team at 137Foundry specializes in this kind of technical deep-dive, working directly in the codebase to identify and fix the specific bottlenecks that lab tools flag but do not explain.
Photo by Negative Space on Pexels
Framework-Specific Pitfalls
React/Next.js: The App Router in Next.js 13+ defaults to Server Components, which helps LCP considerably. But mixing client and server components incorrectly can cause unnecessary client-side JavaScript. Wrap interactive elements in separate client components rather than marking entire pages as 'use client'. The React Server Components documentation explains the rendering model.
Vue/Nuxt: Nuxt 3 uses Nitro for server rendering, which is fast, but the default configuration may not preload images. Use the useHead composable to inject preload hints for LCP resources on key landing pages.
General: Avoid layout shifts during hydration. If your framework replaces server-rendered HTML with client-rendered content that has different dimensions, both CLS and LCP suffer. Keep server and client output identical.
For a broader look at all three Core Web Vitals metrics, including CLS and INP fixes, this complete guide to diagnosing Core Web Vitals covers the full diagnostic workflow from field data analysis to production monitoring.
Further Reading
- web.dev LCP optimization guide covers every technique with visual examples and before-after comparisons
- Chrome DevTools Performance panel reference documents how to read performance traces and identify bottlenecks
- HTTP Archive Web Almanac performance chapter provides industry-wide data on LCP distribution across frameworks
- Lighthouse scoring calculator shows how individual metric improvements affect your overall performance score
- web-vitals changelog tracks updates to measurement methodology that might affect your scores

