Back to Blog

Next.js Performance Optimization - A Practical Checklist for 2026

R
Randy Vianda Putra
· · 4 min read
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

  • staleTime set on all useQuery calls
  • Dynamic imports for modals, charts, editors
  • next/image with explicit width/height or fill + 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.tsx files for route-level Suspense

Following this checklist consistently gets my projects to Lighthouse 95+ on mobile. Performance isn’t magic - it’s discipline.