Next.js Performance Optimization - A Practical Checklist for 2026
Next.js Performance Optimization - A Practical Checklist for 2026
After building Next.js apps at Sampingan, eDOT, Orbit-3, and HeyPico, I’ve built up a repeatable checklist for squeezing out performance. Here’s exactly what I do on every project.
1. Use React Query (TanStack Query) Correctly
The biggest wins come from caching and deduplication. But most teams misuse it.
// ❌ Bad - refetches on every mount, no stale time
const { data } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
});
// ✅ Good - cache for 5 minutes, background refetch after 1 min
const { data } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
staleTime: 1000 * 60, // 1 minute
gcTime: 1000 * 60 * 5, // 5 minutes
refetchOnWindowFocus: false, // don't refetch on tab switch
});
For list pages, use initialData from server components to eliminate the loading state entirely:
// app/users/page.tsx (Server Component)
export default async function UsersPage() {
const users = await fetchUsers();
return <UserList initialData={users} />;
}
// components/UserList.tsx (Client Component)
'use client';
export function UserList({ initialData }: { initialData: User[] }) {
const { data } = useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
initialData, // no loading spinner on first render
staleTime: 30_000,
});
return <>{data.map(u => <UserCard key={u.id} user={u} />)}</>;
}
2. Dynamic Imports for Heavy Components
Don’t bundle everything eagerly. Lazy-load modals, charts, and rich editors.
import dynamic from 'next/dynamic';
// Chart library - 80KB+ saved on initial load
const RevenueChart = dynamic(() => import('@/components/RevenueChart'), {
loading: () => <ChartSkeleton />,
ssr: false, // charts need browser APIs
});
// Modal only loaded when opened
const EditUserModal = dynamic(() => import('@/components/EditUserModal'), {
loading: () => null,
});
3. Image Optimization
Always use next/image. But use it right:
import Image from 'next/image';
// ❌ No size hints - causes layout shift
<Image src={user.avatar} alt="User avatar" />
// ✅ Fixed dimensions + priority for above-fold
<Image
src={user.avatar}
alt="User avatar"
width={48}
height={48}
priority // only for above-the-fold images
className="rounded-full"
/>
// ✅ Fill + sizes for responsive images
<div className="relative aspect-video">
<Image
src={post.cover}
alt={post.title}
fill
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 800px"
className="object-cover rounded-xl"
/>
</div>
4. Server Components First, Client Components When Needed
In Next.js App Router, components are Server Components by default. Keep them that way unless you need interactivity.
// ✅ Server Component - no JS sent to client
export default async function BlogPost({ params }: { params: { slug: string } }) {
const post = await db.post.findUnique({ where: { slug: params.slug } });
return (
<article>
<h1>{post.title}</h1>
<PostContent content={post.content} />
<LikeButton postId={post.id} /> {/* only this is a Client Component */}
</article>
);
}
5. Bundle Analysis - Always Run This Before Shipping
# Install analyzer
npm install @next/bundle-analyzer
# Add to next.config.mjs
const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true',
});
# Run
ANALYZE=true npm run build
The output always surprises you. Common offenders I find: moment.js (use date-fns), lodash (use lodash-es or native), and full chart libraries when only 2 chart types are used.
6. Prefetch Critical Routes
import Link from 'next/link';
// Next.js prefetches this route on hover automatically
<Link href={`/posts/${post.slug}`} prefetch>
{post.title}
</Link>
// Programmatic prefetch
const router = useRouter();
useEffect(() => {
router.prefetch('/dashboard');
}, []);
Quick Wins Checklist
-
staleTimeset on alluseQuerycalls - Dynamic imports for modals, charts, editors
-
next/imagewith explicitwidth/heightorfill+sizes - Server Components for static/data-fetching sections
- Bundle analyzer run before deploy
-
<Link prefetch>on navigation items - Fonts loaded via
next/font(not Google Fonts<link>) -
loading.tsxfiles for route-level Suspense
Following this checklist consistently gets my projects to Lighthouse 95+ on mobile. Performance isn’t magic - it’s discipline.
Share this article