I ran my own blog through PageSpeed Insights and the mobile score wasn’t where I wanted it — the culprit was a slow First Contentful Paint. FCP is the moment a user first sees *anything* render, and on mobile it’s often the difference between someone reading your post and someone bouncing. Here’s exactly how I improved FCP in my Next.js (React) app, with the real changes that moved the needle — no generic advice.
What is First Contentful Paint (FCP)?
First Contentful Paint (FCP) is a Core Web Vital that measures how long it takes — from the moment a page starts loading — for the browser to render the first piece of content on screen, whether that’s text, an image, or any non-blank element. In short, it’s when the user first sees the page respond. A good FCP is 1.8 seconds or less.
It’s easy to confuse FCP with Largest Contentful Paint (LCP), so here’s the difference: FCP is "something appeared," while LCP is "the biggest above-the-fold thing appeared." Both are Core Web Vitals, and on a text-heavy page they’re usually driven by the same factors — how fast your HTML, CSS, and fonts arrive and render.
Rule zero: measure before you touch anything
I don’t guess at performance — I measure, change one thing, measure again. Use PageSpeed Insights or Chrome DevTools Lighthouse (mobile profile, since mobile is almost always the bottleneck), and watch the real Web Vitals in Search Console over time.
The mistake I see most often: optimizing the thing that *feels* slow instead of the thing the trace says is slow. Let the Lighthouse "Opportunities" and "Diagnostics" sections point you.
Ship less JavaScript
React apps get slow when they ship too much JS to the client — the browser has to download, parse, and execute it before the page is interactive, and that competes with your first paint. In the Next.js App Router, components are Server Components by default. Keep it that way, and push "use client" down to the leaves that actually need interactivity.
// ❌ whole page becomes client JS just for one button
'use client';
export default function Page() { /* ...lots of static markup... */ }
// ✅ page stays a Server Component; only the button ships JS
export default function Page() {
return <><StaticContent /><LikeButton /></>; // LikeButton is 'use client'
}For anything below the fold or rarely used, load it lazily so it isn’t in the critical path:
import dynamic from 'next/dynamic';
const CommentsWidget = dynamic(() => import('./CommentsWidget')); // not in first paintTame your fonts — the quiet FCP killer
This is the one that surprised me most on my own site. Web fonts block text from painting, and I was loading more fonts than I actually used. Two fixes made a real difference.
1. Delete fonts you don’t use. I had a decorative font declared that no CSS actually referenced — a pure wasted download on every page. Audit what’s really used and remove the rest.
2. Only preload the font on the critical path. Your heading font should load first; a code/label font can wait. With next/font, self-host, subset, and set preload: false on non-critical fonts so they don’t compete with the one your first paint needs.
import { Plus_Jakarta_Sans, Geist_Mono } from 'next/font/google';
// heading/body font — on the critical path, preloaded by default
const sans = Plus_Jakarta_Sans({ subsets: ['latin'], display: 'swap' });
// code font — not needed for first paint, so don't preload it
const mono = Geist_Mono({ subsets: ['latin'], display: 'swap', preload: false });display: "swap" shows your text immediately in a fallback font and swaps when the web font arrives — so text paints fast instead of waiting. next/font also self-hosts the files (no extra connection to Google) and reserves space to avoid layout shift.
Kill render-blocking resources
Anything the browser must fetch and process before it can paint delays FCP. The usual offenders:
- Third-party scripts — load analytics and tags with
next/scriptandstrategy="afterInteractive"(orlazyOnload) so they never block the first paint. - Big CSS — ship only what the page needs; CSS Modules keep styles scoped and tree-shakeable.
- Expensive paint effects — heavy
backdrop-filter: blur()and largefilter: blur()backgrounds are surprisingly costly on mobile GPUs. Use them sparingly above the fold.
import Script from 'next/script';
// deferred — doesn't block first paint
<Script src="https://www.googletagmanager.com/gtag/js?id=..." strategy="afterInteractive" />Optimize the above-the-fold path
FCP and LCP both live above the fold, so make that region cheap to render:
- Use
next/imageand addpriorityto your one above-the-fold image so it isn’t lazy-loaded. - Give images explicit dimensions (or an aspect-ratio) so they don’t shift layout as they load.
- Keep the hero simple — text paints faster than images, and a lighter hero means a faster FCP.
Serve it static, from the edge
The fastest response is one that’s already rendered. Use static generation (SSG) or incremental static regeneration (ISR) for content pages so the HTML — and your first paint — arrives instantly from a CDN instead of being built per request.
export const revalidate = 60; // ISR: serve static HTML, refresh in the backgroundI go deeper on the caching side of this in Next.js App Router caching, finally explained, and on the broader ranking picture in the technical SEO checklist.
The results, and the takeaway
None of these were heroic rewrites — they were small, measurable changes: fewer fonts, less client JS, deferred scripts, static HTML. That’s the pattern with FCP. You don’t fix it with one silver bullet; you remove a handful of things standing between the user and the first paint, measuring each time.
Further reading: Abhi Venkata’s Improving First Contentful Paint (FCP) in React apps is a good companion read on the React side.
Performance isn’t a feature you add at the end. It’s the sum of small decisions about what you make the browser do before it can show the user anything.