React Performance Optimization: From 47 to 91 PageSpeed

The exact changes I made — self-hosted fonts, image optimization, CLS fixes, and lazy loading — that pushed my portfolio from red to green on PageSpeed Insights.

Suyog Bhise
Full Stack Developer · Pune, India

Starting Point: PageSpeed 47

After deploying my portfolio, I ran PageSpeed Insights for the first time. Mobile score: 47. Desktop: 71. SEO: 100. The SEO was solid (thanks to structured data and proper metadata), but performance was red — embarrassing for a developer portfolio.

📊

The three main culprits: CLS of 0.872, render-blocking Google Fonts (104KB), and S3 project images served as full-resolution PNGs.

Fix 1: Self-Host Fonts with next/font

Google Fonts was my biggest render blocker. Every page load waited for fonts.googleapis.com to respond before rendering text. The fix was switching to next/font:

// BEFORE — in layout.tsx <head>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap" rel="stylesheet"/>

// AFTER — at the top of layout.tsx
import { Inter, Space_Grotesk } from 'next/font/google'

const inter = Inter({
  subsets: ['latin'],
  display: 'swap',
  variable: '--font-inter',
  weight: ['400', '500', '600', '700'], // removed unused weights
})

This eliminated the external request entirely. Next.js downloads the font at build time, self-hosts it, and inlines a font-face declaration. FCP improved by ~800ms.

Fix 2: CLS from GSAP Animations

My about section had a CLS of 0.872 — nearly 9x the "good" threshold of 0.1. The cause: GSAP was setting opacity: 0 on elements before animating them in. When these elements had no reserved dimensions, the browser didn't allocate space for them — then they popped in and shifted everything.

/* Add to your critical inline CSS in layout.tsx */
.about-section {
  min-height: 100vh; /* reserves space before GSAP runs */
}

.about-content,
.about-visual {
  min-height: 400px; /* prevents collapse to 0 */
}

/* Prevent text spans from collapsing */
.reveal-word {
  display: inline-block;
  min-width: 1ch;
}

Fix 3: Image Optimization

Project screenshots from S3 were being served as full 2560px PNGs. I made two changes:

// next.config.js
const nextConfig = {
  images: {
    formats: ['image/avif', 'image/webp'], // auto-convert
    minimumCacheTTL: 31536000, // 1 year cache
  },
}

// In Hero.tsx — proper sizes and priority
<Image
  src={item.src}
  alt={item.title}
  fill
  priority={index < 3}
  fetchPriority={index === 0 ? 'high' : undefined}
  loading={index < 3 ? 'eager' : 'lazy'}
  sizes="(max-width: 768px) 50vw, 20vw"
/>

Results

The best optimization is the one you actually ship. Pick the 3 highest-impact items and do those first.

Back to all posts