Optimasi Performance Next.js — Panduan Praktis untuk Developer Indonesia

Update 2026: Artikel ini sudah diuji di production dengan Next.js 14+ (App Router). Cocok untuk developer yang sudah deploy aplikasi dan ingin mempercepat loading.


Pendahuluan

Hai developer! Pernah nggak sih aplikasi Next.js kamu loadingnya lama? User complain "kok lemot ya?", padahal di localhost cepat banget. Atau score Lighthouse kamu merah semua di performance.

Di artikel ini, kita bahas optimasi performance Next.js dari berbagai sisi: 1. Image Optimization — Next/Image, format modern, lazy loading 2. Caching Strategy — ISR, SSR, SSG, dan CDN caching 3. Bundle Size Reduction — tree shaking, code splitting, dynamic import 4. Core Web Vitals — LCP, CLS, INP dan cara perbaikinya 5. Database & API Optimization — query optimization, connection pooling 6. Font Optimization — next/font, font-display 7. Performance Checklist — Ringkasan semua yang harus dicek


1. Image Optimization

Next/Image Component

Jangan pakai <img> biasa. Pakai next/image:

// ❌ SALAH - img biasa<img src="/hero.jpg" alt="Hero" />

// ✅ BENAR - next/image
import Image from 'next/image'

<Image  src="/hero.jpg"  alt="Hero"  width={1200}  height={630}  priority={false}  loading="lazy"/>

Konfigurasi Image di next.config.ts

// next.config.ts
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {  images: {    formats: ['image/avif', 'image/webp'], // Format modern    deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],    imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],    minimumCacheTTL: 60 * 60 * 24 * 30, // 30 hari cache    remotePatterns: [      {        protocol: 'https',        hostname: 'images.unsplash.com',      },    ],  },}

export default nextConfig

Priority Hint untuk Above-the-fold Images

// Hero image yang langsung kelihatan - pakai priority<Image  src="/hero.jpg"  alt="Hero"  width={1200}  height={630}  priority // ← Ini penting! Preload image ini/>

AVIF/WebP Conversion

Next.js otomatis convert ke AVIF/WebP. Tapi kalau mau manual:

# Install tool
npm install -g sharp

# Convert ke WebP
npx sharp input.jpg --format webp --quality 80 output.webp

2. Caching Strategy

Static Site Generation (SSG)

Untuk halaman yang nggak berubah-ubah:

// app/about/page.tsx
export const dynamic = 'force-static' // ← Paksa static

export default function AboutPage() {  return <h1>Tentang Kami</h1>}

Incremental Static Regeneration (ISR)

Untuk halaman yang jarang berubah tapi perlu update:

// app/blog/[slug]/page.tsx
export const revalidate = 3600 // ← Revalidate tiap 1 jam

export default async function BlogPost({ params }: { params: { slug: string } }) {  const post = await getPost(params.slug)  return <article>{post.content}</article>}

SSR dengan Caching

// app/api/data/route.ts
import { NextResponse } from 'next/server'

export const revalidate = 60 // Cache response 60 detik

export async function GET() {  const data = await fetchExternalData()  return NextResponse.json(data)}

CDN Caching Header

// next.config.ts
const nextConfig: NextConfig = {  async headers() {    return [      {        source: '/static/:path*',        headers: [          {            key: 'Cache-Control',            value: 'public, max-age=31536000, immutable',          },        ],      },    ]  },}

export default nextConfig

3. Bundle Size Reduction

Dynamic Import

// ❌ SALAH - import semua di awal
import HeavyChart from '@/components/HeavyChart'

// ✅ BENAR - dynamic import
import dynamic from 'next/dynamic'

const HeavyChart = dynamic(() => import('@/components/HeavyChart'), {  loading: () => <p>Loading chart...</p>,  ssr: false, // ← Optional: jangan render di server})

export default function Dashboard() {  return <HeavyChart />}

Analyze Bundle

# Install analyzer
npm install -D @next/bundle-analyzer

# next.config.ts
import withBundleAnalyzer from '@next/bundle-analyzer'

const bundleAnalyzer = withBundleAnalyzer({  enabled: process.env.ANALYZE === 'true',
})

const nextConfig: NextConfig = {}export default bundleAnalyzer(nextConfig)

# Run analyzerANALYZE=true npm run build

Tree Shaking & Unused Dependencies

# Cek unused dependencies
npx depcheck

# Cek bundle size per dependency
npm install -g size-limit
npx size-limit --why

Optimize Imports

// ❌ SALAH - import seluruh library
import _ from 'lodash'

// ✅ BENAR - import hanya yang dibutuhkan
import debounce from 'lodash/debounce'

// Atau gunakan native
const debounce = (fn, ms) => {  let timeout  return (...args) => {    clearTimeout(timeout)    timeout = setTimeout(() => fn(...args), ms)  }}

4. Core Web Vitals

Largest Contentful Paint (LCP)

Target: < 2.5 detik

// Optimasi LCP:// 1. Priority image untuk above-the-fold<Image src="/hero.jpg" priority width={1200} height={630} alt="Hero" />

// 2. Preload critical resources
export default function RootLayout({ children }) {  return (    <html>      <head>        <link rel="preload" href="/fonts/inter.woff2" as="font" type="font/woff2" crossOrigin="" />      </head>      <body>{children}</body>    </html>  )}

Cumulative Layout Shift (CLS)

Target: < 0.1

// ❌ SALAH - gambar tanpa dimensi<img src="/ad.jpg" />

// ✅ BENAR - selalu specify width & height<Image src="/ad.jpg" width={300} height={250} alt="Ad" />

// Atau reserve space<div style={{ minHeight: 250 }}>  <AdComponent /></div>

Interaction to Next Paint (INP)

Target: < 200 ms

// Optimasi INP:// 1. Jangan block main thread// 2. Gunakan Web Workers untuk heavy computation// 3. Debounce input events

'use client'import { useState, useCallback } from 'react'

export function SearchBox() {  const [results, setResults] = useState([])

  const handleSearch = useCallback(    debounce(async (query: string) => {      const res = await fetch(`/api/search?q=${query}`)      setResults(await res.json())    }, 300),    []  )

  return <input onChange={(e) => handleSearch(e.target.value)} />}

5. Database & API Optimization

Query Optimization dengan Prisma

// ❌ SALAH - fetch semua field
const users = await prisma.user.findMany()

// ✅ BENAR - select hanya field yang dibutuhkan
const users = await prisma.user.findMany({  select: {    id: true,    name: true,    email: true,  },  take: 10, // ← Pagination})

// ✅ BENAR - include relations dengan filter
const posts = await prisma.post.findMany({  where: { published: true },  include: {    author: { select: { name: true } }, // ← Hanya field yang perlu  },  orderBy: { createdAt: 'desc' },  take: 20,})

Connection Pooling

// lib/prisma.ts
import { PrismaClient } from '@prisma/client'

const globalForPrisma = globalThis as unknown as {  prisma: PrismaClient | undefined}

export const prisma =  globalForPrisma.prisma ??  new PrismaClient({    log: ['query', 'error', 'warn'],  })

if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma

API Response Caching

// app/api/popular/route.ts
import { NextResponse } from 'next/server'import { redis } from '@/lib/redis'

export async function GET() {  // Cek cache dulu  const cached = await redis.get('popular_posts')  if (cached) {    return NextResponse.json(JSON.parse(cached))  }

  // Kalau nggak ada, query database  const posts = await getPopularPosts()

  // Simpan ke cache 5 menit  await redis.setex('popular_posts', 300, JSON.stringify(posts))

  return NextResponse.json(posts)}

6. Font Optimization

next/font (Recommended)

// app/layout.tsx
import { Inter } from 'next/font/google'

const inter = Inter({  subsets: ['latin'],  display: 'swap', // ← Hindari FOIT  variable: '--font-inter',})

export default function RootLayout({ children }) {  return (    <html lang="id" className={inter.variable}>      <body>{children}</body>    </html>  )}

Self-host Fonts (Zero Layout Shift)

// app/layout.tsx
import localFont from 'next/font/local'

const inter = localFont({  src: './fonts/Inter-Variable.woff2',  display: 'swap',  variable: '--font-inter',})

7. Performance Checklist

✅ Images

  • [ ] Pakai next/image bukan <img>

  • [ ] Format AVIF/WebP enabled

  • [ ] Priority hint untuk above-the-fold images

  • [ ] Lazy loading untuk below-the-fold images

  • [ ] Width & height selalu dispesifikkan (CEGEnvironment CLS)

✅ Caching

  • [ ] SSG untuk halaman static

  • [ ] ISR untuk halaman yang jarang berubah

  • [ ] CDN caching header untuk static assets

  • [ ] API response caching (Redis/In-Memory)

✅ Bundle

  • [ ] Dynamic import untuk heavy components

  • [ ] Bundle analyzer dijalankan rutin

  • [ ] Tree shaking optimal

  • [ ] Unused dependencies di-remove

✅ Core Web Vitals

  • [ ] LCP < 2.5s

  • [ ] CLS < 0.1

  • [ ] INP < 200ms

  • [ ] Lighthouse performance score > 90

✅ Database

  • [ ] Query select hanya field yang perlu

  • [ ] Pagination di semua list endpoints

  • [ ] Connection pooling configured

  • [ ] Index pada kolom yang sering di-query

✅ Fonts

  • [ ] next/font digunakan

  • [ ] display: 'swap' enabled

  • [ ] Self-hosted untuk zero CLS

  • [ ] Font subsetting


8. Troubleshooting

LCP Terlalu Lambat

# Cek apa yang di-load di initial render# 1. Buka DevTools > Performance# 2. Record load# 3. Lihat apa yang block LCP

# Solusi umum:# - Priority image untuk hero# - Preload critical CSS/JS# - Remove render-blocking resources

Bundle Terlalu Besar

# Analyze bundleANALYZE=true npm run build

# Cek dependencies yang besar
npx size-limit --why

# Solusi:# - Dynamic import# - Replace heavy lib dengan lighter alternative# - Tree shake unused code

CLS Tinggi

// Selalu specify dimensi<Image width={300} height={200} src="..." alt="..." />

// Atau reserve space<div style={{ aspectRatio: '16/9' }}>  <Image fill src="..." alt="..." /></div>

API Lambat

# Cek query time# 1. Enable Prisma query log# 2. Cek slow query di database# 3. Add indexes

# Solusi:# - Add database indexes# - Cache API response# - Optimize query (select only needed fields)

9. Kesimpulan

Nah itu dia panduan optimasi performance Next.js. Simpel kan?

Kesimpulan utama: 1. Image optimization — next/image + AVIF/WebP = loading 50% lebih cepat 2. Caching strategy — SSG/ISR/CDN = response time turun drastis 3. Bundle reduction — dynamic import + tree shaking = First Load JS lebih kecil 4. Core Web Vitals — LCP, CLS, INP = UX yang smooth 5. Database optimization — select field + pagination + pooling = query cepat 6. Font optimization — next/font = zero CLS

Best practices: - Measure dulu (Lighthouse, WebPageTest) sebelum optimize - Optimasi images adalah low-hanging fruit paling besar - Gunakan CDN untuk static assets - Monitor Core Web Vitals di production - Test di device low-end, bukan cuma di laptop kencang

Tools yang berguna: - Lighthouse — Audit performance - WebPageTest — Detailed waterfalls - Bundle Analyzer — Visualize bundle - PageSpeed Insights — Google's tool

Semoga artikel ini membantu kamu bikin aplikasi yang ngebut!


FAQ

Q: Berapa target Lighthouse score yang realistis? A: 90+ untuk performance di desktop, 75+ di mobile (mobile lebih berat karena hardware terbatas).

Q: next/image ribet, apa bedanya sama img biasa? A: next/image auto-resize, auto-format (AVIF/WebP), lazy load, dan priority hint. Savings: 30-50% bandwidth.

Q: ISR vs SSG vs SSR - kapan pakai yang mana? A: SSG untuk konten statis (about, contact). ISR untuk yang jarang berubah (blog, docs). SSR untuk yang real-time (dashboard, user-specific).

Q: Dynamic import bikin loading spinner muncul, itu bagus? A: Ya, selama spinternya di-placeholder yang benar (reserve space) supaya nggak CLS.

Q: Font self-host vs Google Fonts? A: Self-host lebih cepat (no external request) dan zero CLS. next/font handle semua otomatis.


Artikel ini ditulis oleh developer Indonesia, untuk developer Indonesia. Kalau berguna, jangan lupa share ke teman-teman!

Artikel ini ditulis oleh developer Indonesia, untuk developer Indonesia.