İçeriğe geç
nextjs

Next.js Performance: Core Web Vitals Optimizasyonu

Tarık Tunç
Next.js Performance: Core Web Vitals Optimizasyonu

Next.js Performance: Core Web Vitals Optimizasyonu

Web performansı, kullanıcı deneyimi ve SEO sıralaması için kritik bir faktördür. Google'ın Core Web Vitals metrikleri (LCP, INP, CLS), sayfa deneyimini ölçen temel göstergelerdir. Next.js, bu metrikleri optimize etmek için güçlü yerleşik araçlar sunar. Bu yazıda Next.js uygulamalarında Core Web Vitals optimizasyonunu kapsamlı olarak inceleyeceğiz.

Core Web Vitals Nedir?

Metrik Açıklama İyi Kötü
LCP (Largest Contentful Paint) En büyük içerik elemanının yüklenme süresi < 2.5s > 4.0s
INP (Interaction to Next Paint) Kullanıcı etkileşimlerine yanıt süresi < 200ms > 500ms
CLS (Cumulative Layout Shift) Sayfadaki görsel kayma miktarı < 0.1 > 0.25

LCP Optimizasyonu

Image Optimization

import Image from 'next/image';

// LCP elemanı olan hero image'i optimize edin
export default function HeroSection() {
  return (
    <section className="hero">
      <Image
        src="/hero-banner.jpg"
        alt="Hero Banner"
        width={1200}
        height={600}
        priority          // LCP elemanı için priority ekleyin
        sizes="100vw"     // Responsive boyut bilgisi
        quality={85}      // Kalite ayarı
        placeholder="blur" // Blur placeholder
        blurDataURL="data:image/jpeg;base64,/9j/4AAQ..."
      />
      <h1>Ana Başlık</h1>
    </section>
  );
}
İpucu: priority prop'u, Next.js'e bu görselin LCP elemanı olduğunu bildirir ve <link rel="preload"> ekler. Sadece above-the-fold (ilk görünen) görsellerde kullanın. Her sayfada en fazla 1-2 görselde priority kullanmalısınız.

Font Optimization

// app/layout.js
import { Inter, Fira_Code } from 'next/font/google';

const inter = Inter({
  subsets: ['latin', 'latin-ext'],
  display: 'swap',        // FOUT yerine FOIT önleme
  preload: true,
  variable: '--font-inter',
});

const firaCode = Fira_Code({
  subsets: ['latin'],
  display: 'swap',
  variable: '--font-fira-code',
});

export default function RootLayout({ children }) {
  return (
    <html lang="tr" className={`${inter.variable} ${firaCode.variable}`}>
      <body className={inter.className}>
        {children}
      </body>
    </html>
  );
}

Critical CSS ve Bundle Optimization

// next.config.js
const nextConfig = {
  // Experimental CSS optimization
  experimental: {
    optimizeCss: true,
  },

  // Bundle analyzer
  webpack: (config, { isServer }) => {
    if (process.env.ANALYZE === 'true') {
      const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
      config.plugins.push(
        new BundleAnalyzerPlugin({
          analyzerMode: 'static',
          reportFilename: isServer ? '../analyze/server.html' : './analyze/client.html',
        })
      );
    }
    return config;
  },
};

INP Optimizasyonu

React.startTransition ile Önceliklendirme

'use client';

import { useState, useTransition } from 'react';

function SearchWithResults({ items }) {
  const [query, setQuery] = useState('');
  const [filteredItems, setFilteredItems] = useState(items);
  const [isPending, startTransition] = useTransition();

  const handleSearch = (e) => {
    const value = e.target.value;
    setQuery(value); // Yüksek öncelik - input hemen güncellenir

    startTransition(() => {
      // Düşük öncelik - filtreleme arka planda yapılır
      const filtered = items.filter((item) =>
        item.name.toLowerCase().includes(value.toLowerCase())
      );
      setFilteredItems(filtered);
    });
  };

  return (
    <div>
      <input value={query} onChange={handleSearch} placeholder="Ara..." />
      {isPending && <p>Filtreleniyor...</p>}
      <ul>
        {filteredItems.map((item) => (
          <li key={item.id}>{item.name}</li>
        ))}
      </ul>
    </div>
  );
}

Dynamic Import ile Code Splitting

import dynamic from 'next/dynamic';

// Ağır bileşenleri lazy load edin
const HeavyChart = dynamic(() => import('@/components/Chart'), {
  loading: () => <div className="chart-skeleton" />,
  ssr: false, // Sadece client'ta yükle
});

const MarkdownEditor = dynamic(
  () => import('@/components/MarkdownEditor'),
  { ssr: false }
);

// Koşullu lazy load
function ProductPage({ product }) {
  const [showReviews, setShowReviews] = useState(false);

  return (
    <div>
      <ProductDetails product={product} />
      <button onClick={() => setShowReviews(true)}>
        Yorumları Göster
      </button>
      {showReviews && <HeavyChart data={product.reviews} />}
    </div>
  );
}

Third-Party Script Yönetimi

import Script from 'next/script';

export default function Layout({ children }) {
  return (
    <html>
      <body>
        {children}

        {/* Analytics - sayfa yüklendikten sonra */}
        <Script
          src="https://www.googletagmanager.com/gtag/js?id=G-XXXXX"
          strategy="afterInteractive"
        />

        {/* Chat widget - idle durumda */}
        <Script
          src="https://widget.intercom.io/widget/xxx"
          strategy="lazyOnload"
        />

        {/* Kritik script - head'de */}
        <Script
          src="/critical-polyfill.js"
          strategy="beforeInteractive"
        />
      </body>
    </html>
  );
}

CLS Optimizasyonu

Image Boyut Belirtme

// Her zaman width ve height belirtin
<Image src="/photo.jpg" width={800} height={600} alt="Foto" />

// Responsive görsellerde aspect-ratio kullanın
<div style={{ position: 'relative', aspectRatio: '16/9' }}>
  <Image src="/banner.jpg" fill alt="Banner"
    style={{ objectFit: 'cover' }} />
</div>

Font Display Swap

// next/font otomatik olarak font-display: swap ekler
// Manuel font yükleme yapıyorsanız:
const myFont = localFont({
  src: './fonts/MyFont.woff2',
  display: 'swap',
  fallback: ['system-ui', 'arial'],
  adjustFontFallback: 'Arial', // Fallback font metrikleri ayarlama
});

Skeleton ve Placeholder

// loading.js - Layout shift'i önleyen skeleton
export default function Loading() {
  return (
    <div className="page-skeleton">
      {/* Gerçek içerikle aynı boyutlarda skeleton */}
      <div className="skeleton-header" style={{ height: 60 }} />
      <div className="skeleton-hero" style={{ aspectRatio: '16/9' }} />
      <div className="skeleton-content">
        <div className="skeleton-line" style={{ width: '80%', height: 24 }} />
        <div className="skeleton-line" style={{ width: '60%', height: 16 }} />
        <div className="skeleton-line" style={{ width: '70%', height: 16 }} />
      </div>
    </div>
  );
}

Caching Stratejileri

// Server Component'lerde fetch caching
async function ProductList() {
  // 1 saat boyunca cache'le
  const products = await fetch('https://api.example.com/products', {
    next: { revalidate: 3600 },
  }).then((res) => res.json());

  return products.map((p) => <ProductCard key={p.id} product={p} />);
}

// Statik sayfalar için generateStaticParams
export async function generateStaticParams() {
  const products = await getProducts();
  return products.map((p) => ({ slug: p.slug }));
}

// Route segment config
export const revalidate = 3600; // 1 saat
export const dynamic = 'force-static'; // Statik olarak oluştur

Performans Ölçüm Araçları

// Web Vitals raporlama
// app/layout.js
import { SpeedInsights } from '@vercel/speed-insights/next';

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        {children}
        <SpeedInsights />
      </body>
    </html>
  );
}

// Custom Web Vitals raporlama
// app/web-vitals.js
'use client';

import { useReportWebVitals } from 'next/web-vitals';

export function WebVitalsReporter() {
  useReportWebVitals((metric) => {
    // Analytics servisine gönder
    console.log(metric.name, metric.value);

    // Google Analytics 4'e gönder
    window.gtag?.('event', metric.name, {
      value: Math.round(metric.name === 'CLS' ? metric.value * 1000 : metric.value),
      event_label: metric.id,
      non_interaction: true,
    });
  });

  return null;
}

Performance Checklist

  1. LCP: Hero image'de priority kullanın, font'larda display: swap ekleyin
  2. INP: Ağır hesaplamaları startTransition ile sarın, code splitting uygulayın
  3. CLS: Tüm görsellere boyut belirtin, skeleton'ları gerçek boyutlarda tasarlayın
  4. Bundle: @next/bundle-analyzer ile analiz edin, gereksiz paketleri kaldırın
  5. Caching: Fetch caching, ISR ve static generation kullanın
  6. Scripts: 3. parti scriptleri afterInteractive veya lazyOnload ile yükleyin
  7. Fonts: next/font kullanın, subset ve preload ayarlayın
  8. Images: next/image ile otomatik optimizasyon, WebP/AVIF format
Uyarı: Performans optimizasyonunu ölçüme dayalı yapın. Lighthouse, PageSpeed Insights ve Chrome DevTools Performance paneli ile darboğazları tespit edin. Erken optimizasyon yerine, gerçek metrik verileriyle önceliklendirme yapın.

Sonuç

Next.js, Core Web Vitals optimizasyonu için güçlü yerleşik araçlar sunar. next/image ile otomatik görsel optimizasyonu, next/font ile font performansı, next/script ile third-party script yönetimi ve Streaming SSR ile aşamalı render bu araçların başlıcalarıdır. LCP için priority image ve font swap, INP için code splitting ve useTransition, CLS için boyut belirtme ve skeleton tasarımı temel stratejilerdir. Vercel Speed Insights ve web-vitals kütüphanesi ile metrikleri sürekli takip ederek performansı koruyun.

Kaynaklar

İlgili Yazılar