kodingup.camp

Beranda/Next.js Mastery: Full-Stack, Auth & Deploy

Performa & Optimasi

Modul 8 dari 116 menit bacaPerformance

Isi modul

    Kenapa ini penting

    Pengunjung tidak sabar. Halaman yang lama kosong, tombol yang tidak merespons saat ditekan, atau teks yang tiba-tiba melompat ke bawah karena gambar baru termuat, semuanya membuat orang menutup tab. Google juga mengukur hal-hal ini lewat Core Web Vitals dan memakainya sebagai salah satu sinyal peringkat pencarian. Jadi performa bukan sekadar "bonus", melainkan bagian dari pengalaman pengguna dan SEO.

    Next.js sudah melakukan banyak optimasi otomatis: Server Component tidak mengirim JavaScript, kode dipecah per route, gambar dan font dioptimalkan. Tapi aplikasi tetap bisa lambat kalau kamu, misalnya, meng-import library chart 300 KB di halaman yang jarang dipakai, atau membuat seluruh halaman menunggu satu query database yang lambat. Modul ini membekalimu cara mengukur dulu, lalu memperbaiki bagian yang benar-benar bermasalah.

    Analogi: optimasi tanpa pengukuran itu seperti diet tanpa timbangan. Kamu bisa saja berhenti makan nasi, padahal penyebab berat badan sebenarnya adalah minuman manis tiga gelas sehari. Ukur dulu (bundle analyzer, Web Vitals), baru tentukan apa yang dipangkas.

    Konsep inti

    Core Web Vitals

    Tiga metrik utama yang diukur dari pengalaman pengguna sungguhan:

    Metrik Mengukur Target "good" (versi web.dev)
    LCP (Largest Contentful Paint) Kapan elemen terbesar (gambar hero, judul) tampil ≤ 2,5 detik
    INP (Interaction to Next Paint) Seberapa cepat halaman merespons klik/ketik ≤ 200 ms
    CLS (Cumulative Layout Shift) Seberapa banyak tata letak "melompat" ≤ 0,1

    Penyebab umum dan obatnya di Next.js: LCP lambat → gambar hero tidak diprioritaskan atau data utama menunggu lama (pakai next/image dengan preload, prerender, streaming). INP buruk → terlalu banyak JavaScript di main thread (kurangi Client Component, lazy load). CLS tinggi → gambar tanpa ukuran, font yang berganti mendadak (pakai next/image dengan width/height dan next/font).

    Next.js menyediakan hook useReportWebVitals dari next/web-vitals untuk mengirim angka ini ke log atau layanan analitik. Setiap metrik punya name, value, dan rating ("good", "needs-improvement", atau "poor"). Catatan Next.js 16 untuk next/image: prop priority sudah deprecated dan diganti preload; dokumentasinya juga menyebut bahwa dalam banyak kasus loading="eager" atau fetchPriority="high" lebih tepat.

    Menganalisis bundle

    Sebelum memangkas, lihat apa isi JavaScript-mu. Next.js 16 menyediakan dua alat:

    Sejak Next.js 16, next build juga tidak lagi menampilkan ukuran bundle JS di ringkasan route, jadi analyzer inilah sumber angkanya.

    Obat yang paling sering dipakai setelah menemukan modul besar: pindahkan logika ke Server Component (library hanya jalan di server, tidak terkirim), lazy load komponen yang tidak langsung terlihat, atau pakai opsi optimizePackageImports untuk library ikon/utilitas yang meng-export ratusan modul.

    Dynamic import dengan next/dynamic

    Lazy loading berarti menunda pemuatan kode sampai benar-benar dibutuhkan. next/dynamic adalah gabungan React.lazy() dan Suspense:

    const Chart = dynamic(() => import('./Chart'), { loading: () => <p>Memuat grafik...</p> })
    

    Hal yang perlu diingat (dari dokumentasi Next.js 16):

    Streaming dengan Suspense

    Tanpa streaming, server menunggu semua data selesai baru mengirim HTML. Dengan <Suspense>, server langsung mengirim bagian yang sudah siap beserta fallback (misalnya skeleton) untuk bagian yang masih menunggu, lalu "mengalirkan" sisanya begitu siap. File loading.js di sebuah folder route adalah cara pintas: Next.js otomatis membungkus page.js di folder itu dengan <Suspense fallback={<Loading />}>.

    Prinsipnya: taruh pekerjaan async sedalam mungkin di pohon komponen. Kalau await lambat ada di page paling atas, seluruh halaman menunggu. Kalau dipindah ke komponen kecil di dalam <Suspense>, sisa halaman bisa tampil duluan.

    Cache Components dan Partial Prerendering

    Ini perubahan terbesar Next.js 16 soal performa, dan proyek baru sudah menyalakannya (cacheComponents: true). Dengan Cache Components:

    Hasilnya terlihat di output npm run build: ○ (Static) untuk route yang sepenuhnya prerender, ◐ (Partial Prerender) untuk route yang punya static shell plus bagian yang di-stream, dan ƒ (Dynamic) untuk route yang dirender per request.

    Kalau kamu bekerja di proyek lama tanpa cacheComponents, model caching sebelumnya (dynamic, revalidate, fetch dengan next: { revalidate }) masih didokumentasikan di panduan "Caching and Revalidating (Previous Model)".

    Contoh kode

    Satu halaman yang menggabungkan tiga jenis konten: statis, di-cache, dan di-stream per request.

    // app/dashboard-demo/page.js
    import { Suspense } from 'react'
    import { cookies } from 'next/headers'
    import { connection } from 'next/server'
    import { cacheLife, cacheTag } from 'next/cache'
    
    // 1. Data publik di-cache: masuk static shell
    async function PopularPosts() {
      'use cache'
      cacheLife('hours')
      cacheTag('posts')
      const res = await fetch('https://jsonplaceholder.typicode.com/posts?_limit=3')
      const posts = await res.json()
      return (
        <ul>
          {posts.map((p) => <li key={p.id}>{p.title}</li>)}
        </ul>
      )
    }
    
    // 2. Data per pengguna (cookie): di-stream saat request
    async function Greeting() {
      const theme = (await cookies()).get('theme')?.value ?? 'terang'
      return <p>Tema pilihanmu: {theme}</p>
    }
    
    // 3. Nilai yang harus unik per request: connection() dulu
    async function ServerTime() {
      await connection()
      return <p>Dirender pada {new Date().toLocaleTimeString('id-ID')}</p>
    }
    
    export default function Page() {
      return (
        <main>
          <h1>Dashboard</h1>                {/* statis → static shell */}
          <PopularPosts />                  {/* di-cache → static shell */}
          <Suspense fallback={<p>Memuat preferensi...</p>}>
            <Greeting />                    {/* di-stream */}
          </Suspense>
          <Suspense fallback={<p>...</p>}>
            <ServerTime />                  {/* di-stream */}
          </Suspense>
        </main>
      )
    }
    // Hasil build: ◐ /dashboard-demo  (Partial Prerender)
    

    Lazy loading komponen berat yang hanya muncul setelah tombol diklik:

    // app/statistik/StatsPanel.jsx
    'use client'
    
    import { useState } from 'react'
    import dynamic from 'next/dynamic'
    
    // Kode HeavyChart baru diunduh ketika pertama kali dirender
    const HeavyChart = dynamic(() => import('./HeavyChart'), {
      loading: () => <p>Memuat grafik...</p>,
      ssr: false, // boleh di sini karena file ini Client Component
    })
    
    export function StatsPanel({ data }) {
      const [show, setShow] = useState(false)
      return (
        <div>
          <button onClick={() => setShow(true)}>Tampilkan grafik</button>
          {show && <HeavyChart data={data} />}
        </div>
      )
    }
    

    Skeleton otomatis untuk satu folder route:

    // app/blog/loading.js
    export default function Loading() {
      return (
        <div aria-busy="true">
          <div style={{ height: 32, width: '60%', background: '#eee', borderRadius: 4 }} />
          <div style={{ height: 16, width: '90%', background: '#eee', marginTop: 12 }} />
        </div>
      )
    }
    

    Mengukur Web Vitals di browser:

    // app/_components/WebVitals.jsx
    'use client'
    
    import { useReportWebVitals } from 'next/web-vitals'
    
    export function WebVitals() {
      useReportWebVitals((metric) => {
        // → { name: 'LCP', value: 1234.5, rating: 'good', ... }
        console.log(metric.name, Math.round(metric.value), metric.rating)
      })
      return null
    }
    
    // app/layout.js → taruh <WebVitals /> di dalam <body>
    

    Menganalisis bundle dengan Turbopack:

    npx next analyze            # buka UI interaktif di browser
    npx next analyze --output   # simpan ke .next/diagnostics/analyze
    cp -r .next/diagnostics/analyze ./analyze-sebelum-refactor
    

    Gambar hero yang diprioritaskan dan font tanpa layout shift:

    // app/page.js (potongan)
    import Image from 'next/image'
    
    export default function Home() {
      return (
        <Image
          src="/hero.jpg"
          alt="Ilustrasi blog"
          width={1200}
          height={600}   // ukuran eksplisit → browser menyisakan ruang, CLS aman
          preload        // Next.js 16: pengganti prop priority yang deprecated
        />
      )
    }
    

    Kesalahan umum

    1. Mengoptimasi tanpa mengukur. Jalankan next analyze dan lihat Web Vitals dulu. Sering kali satu library besar adalah 80% masalahnya.
    2. Membawa kebiasaan Next.js 14 ke proyek Cache Components. export const dynamic = 'force-dynamic' dan export const revalidate = 60 kini memicu error. Pakai 'use cache' + cacheLife, atau biarkan dinamis di dalam <Suspense>.
    3. await lambat di komponen paling atas. Seluruh halaman jadi menunggu. Pindahkan ke komponen anak di dalam <Suspense> agar sisanya tampil duluan.
    4. Memakai ssr: false di Server Component. Next.js akan error. Pindahkan dynamic(..., { ssr: false }) ke file 'use client'.
    5. Menjadikan seluruh halaman Client Component hanya karena butuh satu tombol interaktif. Semua JavaScript-nya ikut terkirim. Jadikan hanya tombolnya Client Component, sisanya tetap Server Component.

    Latihan

    1. Mudah, Tambahkan loading.js di folder app/blog/ proyek akhir dengan skeleton sederhana, lalu buka halaman artikel di mode development dan amati fallback-nya.
    2. Sedang, Pasang komponen WebVitals di layout, buka beranda di browser, dan catat nilai LCP, CLS, serta INP (klik sesuatu untuk memicu INP). Mana yang ratingnya bukan good?
    3. Menantang, Buat halaman /acak yang menampilkan angka acak. Pertama tulis Math.random() langsung di page lalu jalankan npm run build dan baca errornya. Perbaiki dengan dua cara: (a) angka berbeda setiap request, (b) angka sama untuk semua pengunjung selama 1 menit.
    Lihat solusi
    // 1. app/blog/loading.js
    export default function Loading() {
      return <p aria-busy="true">Memuat artikel...</p>
    }
    // Catatan: halaman artikel proyek akhir sudah punya <Suspense> sendiri,
    // jadi loading.js terutama terlihat saat navigasi antar halaman.
    
    // 2. Contoh hasil di console (angka akan berbeda di mesinmu):
    // FCP 180 good
    // LCP 420 good
    // CLS 0 good
    // INP 56 good   (muncul setelah ada interaksi)
    // Ukur juga dengan throttling "Slow 4G" di DevTools untuk melihat kondisi pengguna nyata.
    
    // 3a. Berbeda setiap request
    import { Suspense } from 'react'
    import { connection } from 'next/server'
    
    async function RandomNumber() {
      await connection() // tunda ke waktu request
      return <p>Angka: {Math.random()}</p>
    }
    
    export default function Page() {
      return (
        <Suspense fallback={<p>...</p>}>
          <RandomNumber />
        </Suspense>
      )
    }
    
    // 3b. Sama untuk semua pengunjung selama ±1 menit
    import { cacheLife } from 'next/cache'
    
    async function CachedRandom() {
      'use cache'
      cacheLife('minutes') // profil minutes: revalidate 1 menit
      return <p>Angka: {Math.random()}</p>
    }
    
    export default function Page() {
      return <CachedRandom />
    }
    

    Ringkasan

    Video pendamping

    Kalau penjelasan di atas belum masuk, tonton ini. Video dari kanal YouTube pihak lain.

    The Right way to Optimize Next.js to Score 100 in lighthouseCoderOne · Bahasa InggrisBuka di YouTube

    Kuis pemahaman

    Pilih satu jawaban per soal. Penjelasan muncul setelah kamu menjawab.