
Why Most Next.js Portfolios Are Poorly Optimized (And How to Fix Yours)
Your Next.js 16 portfolio might score 100 on Lighthouse and still be slow. Here is how to actually optimize for the metrics that matter in 2026.
Your Next.js 16 portfolio might score 100 on Lighthouse and still be slow. Here is how to actually optimize for the metrics that matter in 2026.
I have audited dozens of developer portfolios over the past year, many built with Next.js, and the same optimization mistakes keep appearing. The painful part? Most developers think their site is fast because Lighthouse gives them a green score.
Lighthouse is lying to you. Or more accurately, it is telling you a very incomplete story.
The Lighthouse Trap
Here is the dirty secret about Lighthouse scores: a 100/100 Lighthouse score in a controlled lab environment tells you almost nothing about how your site performs for real users on real devices.
Common Mistake: Optimizing for Lighthouse instead of optimizing for actual user experience. These are not the same thing. A site can score 100 on Lighthouse while feeling sluggish on a mid-range Android phone over 4G.
The metrics that actually matter in 2026 are:
- INP (Interaction to Next Paint): How fast does the page respond when someone clicks something?
- LCP (Largest Contentful Paint): How quickly does the main content appear?
- CLS (Cumulative Layout Shift): Does the page jump around while loading?
- TTFB (Time to First Byte): How fast does the server respond?
Most Next.js portfolios nail LCP (server-rendered content loads fast) but completely fail on INP because they ship too much client-side JavaScript.
The 5 Most Common Next.js Portfolio Sins
Sin 1: Client Components Everywhere
This is the biggest offender. Next.js 16 defaults to Server Components, but most developers immediately slap "use client" on everything because their favorite animation library or state management tool requires it.
// Bad: The entire page is a client component
"use client";
import { motion } from "framer-motion";
import { useState, useEffect } from "react";
export default function ProjectsPage() {
const [projects, setProjects] = useState([]);
useEffect(() => {
fetch("/api/projects").then(r => r.json()).then(setProjects);
}, []);
return (
<motion.div animate={{ opacity: 1 }}>
{projects.map(p => <ProjectCard key={p.id} {...p} />)}
</motion.div>
);
}This code has three problems:
- The entire page is a client component, so no server-side rendering benefit
- Data fetching happens on the client with useEffect (waterfall request)
- framer-motion ships ~40KB of JS for a simple fade animation
The Fix: Push client boundaries as deep as possible. The page itself should be a Server Component. Only the specific interactive elements need to be Client Components.
// Good: Server Component page with isolated client boundaries
import { getProjects } from "@/lib/projects";
import { ProjectCard } from "./project-card"; // Server Component
import { AnimatedContainer } from "./animated-container"; // Client Component
export default async function ProjectsPage() {
const projects = await getProjects();
return (
<AnimatedContainer>
{projects.map(p => <ProjectCard key={p.id} {...p} />)}
</AnimatedContainer>
);
}The data fetches on the server. The HTML renders on the server. Only the animation wrapper is a Client Component, and it receives server-rendered children.
Sin 2: Unoptimized Images
I see this constantly: developers using the Next.js <Image> component but failing to actually optimize their images.
Common mistakes:
- Using PNG screenshots instead of WebP/AVIF
- Not setting
sizesprop, causing the browser to download full-size images - Missing
priorityon the hero/banner image (hurts LCP) - Using massive original images (3000x2000+) when the displayed size is 800x400
// Bad
<Image src="/images/project.png" width={800} height={400} alt="Project" />
// Good
<Image
src="/images/project.webp"
width={800}
height={400}
alt="Project showcase: real-time dashboard with analytics"
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 800px"
priority // Only for above-the-fold images
quality={85}
/>Convert Images to WebP or AVIF
Use tools like Sharp, Squoosh, or even a simple build script to convert all PNG/JPG images to WebP. AVIF offers even better compression but has slightly less browser support.
Set Proper Sizes
The sizes prop tells the browser which image size to download based on viewport width. Without it, Next.js generates multiple sizes but the browser might download a larger one than needed.
Prioritize Above-the-Fold Images
Add priority to your hero image, profile photo, or any image visible without scrolling. This tells Next.js to preload it, which directly improves LCP.
Audit Image Dimensions
If your image displays at 800x400, there is no reason to serve a 3000x2000 original. Resize at the source level. This reduces storage cost and speeds up the optimization pipeline.
Sin 3: Font Loading Gone Wrong
Custom fonts are the silent killer of portfolio performance. Google Fonts, self-hosted fonts, icon fonts, they all cause layout shifts and slow down rendering if loaded incorrectly.
The most common mistake I see: loading 4-5 font weights when only 2-3 are actually used.
// Bad: Loading too many weights
import { Inter } from "next/font/google";
const inter = Inter({
subsets: ["latin"],
weight: ["100", "200", "300", "400", "500", "600", "700", "800", "900"]
});// Good: Only load what you use
import { Inter } from "next/font/google";
const inter = Inter({
subsets: ["latin"],
weight: ["400", "600", "700"],
display: "swap",
preload: true,
});Each additional font weight can add 15-30KB. If you are loading 9 weights, that is potentially 200KB+ of font files. On a 3G connection, that is seconds of delay.
Sin 4: Route-Level Code Splitting Ignored
Next.js 16 handles page-level code splitting automatically, but it cannot save you from importing heavy libraries in your layout or shared components.
A pattern I see constantly:
// app/layout.tsx
import { Analytics } from "@vercel/analytics/react";
import { SpeedInsights } from "@vercel/speed-insights/next";
import { CommandMenu } from "@/components/CommandMenu"; // Heavy component
import { MusicWidget } from "@/components/MusicWidget"; // Another heavy one
import { Notifications } from "@/components/Notifications"; // And anotherEvery component imported in layout.tsx ships JavaScript to every single page. If your CommandMenu uses a library like cmdk (10KB+), that JavaScript loads even on pages where nobody will ever open the command menu.
Performance Killer: A common pattern is to import a heavy animation library, a markdown renderer, or a rich text editor in the root layout. This JS ships to every page, even if 90% of your visitors only see the homepage.
The Fix: Lazy load heavy components that are not immediately visible.
import dynamic from "next/dynamic";
const CommandMenu = dynamic(() => import("@/components/CommandMenu"), {
ssr: false, // No need to SSR a keyboard-triggered overlay
});
const MusicWidget = dynamic(() => import("@/components/MusicWidget"), {
ssr: false,
loading: () => <MusicWidgetSkeleton />,
});Sin 5: No Caching Strategy
This blows my mind every time I see it. A portfolio site, where the content changes maybe once a week, making fresh API calls on every single request.
// Bad: No caching, fetches fresh data on every request
export default async function BlogPage() {
const posts = await fetch("https://api.example.com/posts").then(r => r.json());
return <BlogList posts={posts} />;
}// Good: Revalidate every hour
export default async function BlogPage() {
const posts = await fetch("https://api.example.com/posts", {
next: { revalidate: 3600 }
}).then(r => r.json());
return <BlogList posts={posts} />;
}Even better, for truly static content like blog posts, use generateStaticParams and let Next.js generate static pages at build time:
export async function generateStaticParams() {
const posts = await getBlogPosts();
return posts.map(post => ({ slug: post.slug }));
}Static pages are served from the CDN edge. No server computation, no database queries, no cold starts. Just HTML, instantly.
A Real-World Audit: What I Found on a Client Portfolio
I recently audited a senior developer's Next.js 16 portfolio. Lighthouse score: 97. Real-world performance? Terrible on mobile.
Here is what I found:
| Issue | Impact |
|---|---|
| 3 unused font weights loaded | +90KB |
| framer-motion imported in layout | +42KB on every page |
| 8 Client Components that could be Server Components | +120KB total JS |
| Blog images served as unoptimized PNG | LCP of 4.2s on mobile |
| No revalidation strategy on API routes | Fresh DB queries on every visit |
After applying the fixes outlined above:
- Total JS shipped: Reduced from 380KB to 145KB (62% reduction)
- LCP (mobile): Dropped from 4.2s to 1.1s
- INP: Dropped from 280ms to 65ms
- TTFB: Improved by 800ms by adding proper caching
The Lighthouse score was barely affected (went from 97 to 99). But the actual user experience went from sluggish to snappy. That is the difference between optimizing for a score and optimizing for people.
The Quick Optimization Checklist
If you want to audit your own Next.js portfolio right now, here is a practical checklist:
Run this command to see exactly how much JS your pages ship:
npx next build
# Check the "First Load JS" column in the output
# Anything over 150KB for a portfolio page is a red flagThen go through this list:
- Are you using Server Components by default? (Check for unnecessary "use client")
- Is your hero/banner image set to
priority? - Are all images in WebP or AVIF format?
- Are you loading only the font weights you actually use?
- Are heavy components (command menu, modals, widgets) lazy loaded?
- Does your build output show reasonable "First Load JS" numbers?
- Are you using
revalidateorgenerateStaticParamswhere appropriate? - Have you tested on a real mobile device, not just Chrome DevTools?
The Bottom Line
A fast portfolio is not about chasing Lighthouse scores. It is about making intentional decisions: Server Components by default, images properly optimized, fonts trimmed, heavy libraries lazy loaded, and caching in place.
Your portfolio is often the first thing a potential employer or client sees. If it takes 4 seconds to load on their phone, it does not matter how beautiful your animations are. They have already left.
Optimize for real users on real devices. Your future self will thank you.
Have you audited your portfolio recently? What performance wins did you find? Share your before/after numbers in the comments.
Author Parth Sharma
Full-Stack Developer, Freelancer, & Founder. Obsessed with crafting pixel-perfect, high-performance web experiences that feel alive.
How to Stand Out as a Developer in 2026 (Without Open Source Fame)
Next Article →The Hidden Cost of Using Too Many NPM Packages
Discussion0
Join the conversation
Sign in to leave a comment, like, or reply.
No comments yet. Start the discussion!
Read This Next
Why 90% of Developer Portfolios Look the Same (And How to Actually Stand Out)
Dark mode, a hero section with a wave emoji, a grid of project cards. Sound familiar? Here is why most developer portfolios blend together and what to do about it.
The Hidden Cost of Using Too Many NPM Packages
Every npm install adds more than a dependency. It adds risk, bloat, and maintenance debt. Here is how to audit your project before it becomes unmanageable.