selupucin
SEO ve Performans

Next.js'te Core Web Vitals'ı Yeşile Çekmek: Pratik Rehber

next/image, next/font, next/script, sunucu bileşenleri ve streaming ile LCP, INP ve CLS'i App Router projelerinde gerçekten yeşile çeken adımlar.

Deniz Selupuçin8 dk okuma

Ölçümü kurduysanız, sıra uygulamada

Core Web Vitals'ta zor kısım metrikleri anlamak değil; hangi kod kararının hangi metriği bozduğunu görmek. Bu yazı bilinçli olarak Next.js App Router'a özgü: hangi API'yi nasıl kullandığımda LCP, INP ve CLS'in gerçekten yerinden oynadığını anlatıyorum.

Metriklerin ne ölçtüğü, eşiklerin nasıl okunduğu, saha verisiyle laboratuvar verisi arasındaki fark ve neyi önce düzeltmeniz gerektiği gibi ölçüm sorularını burada tekrarlamıyorum; hepsini Core Web Vitals için pratik performans rehberi yazısında topladım. Ölçümü kurmadan aşağıdaki adımları uygularsanız, neyin işe yaradığını asla bilemezsiniz.

App Router'da saha verisini toplamak birkaç satır:

// app/web-vitals.tsx
"use client";
import { useReportWebVitals } from "next/web-vitals";

export function WebVitals() {
  useReportWebVitals((metric) => {
    // Kişisel veri yok: yalnızca metrik ve sayfa yolu
    navigator.sendBeacon(
      "/api/vitals",
      JSON.stringify({ name: metric.name, value: metric.value, path: location.pathname }),
    );
  });
  return null;
}

LCP: zinciri sunucudan boyamaya kadar kısaltın

LCP tek bir bayrakla düzelmez. Sunucu yanıtı, kaynağın keşfedilmesi, indirilmesi ve boyanması — zincirin tamamı işin içindedir. Next.js'te her halkaya ayrı bir müdahale var.

Hero görselini next/image ile doğru kurun

Görsel, kurumsal sitelerin yüzde doksanında LCP adayıdır. Üç ayrıntı fark yaratır:

  1. priority yalnız tek görselde. Görünür alandaki en büyük görsele verin. Her görsele verirseniz tarayıcı öncelik sırasını kaybeder ve hiçbiri hızlanmaz.
  2. sizes gerçek düzeni anlatsın. Yanlış sizes, mobil kullanıcıya masaüstü boyutunda dosya indirtir. Bu tek satır, çoğu projede indirilen bayt miktarını yarıya düşürür.
  3. Format dönüşümü açık olsun. next.config içinde AVIF ve WebP etkinleştirildiğinde aynı görsel çok daha küçük iner.
import Image from "next/image";

export function Hero() {
  return (
    <Image
      src="/hero.jpg"
      alt="Ürün arayüzü"
      width={1600}
      height={900}
      priority
      sizes="(max-width: 768px) 100vw, (max-width: 1280px) 80vw, 1200px"
    />
  );
}
// next.config.ts
const nextConfig = {
  images: {
    formats: ["image/avif", "image/webp"],
    remotePatterns: [{ protocol: "https", hostname: "res.cloudinary.com" }],
  },
};

Gerçek bir kurumsal projede hero görseli 1,4 MB'lık bir PNG'ydi ve tembel yükleniyordu. AVIF dönüşümü, doğru sizes ve tek bir priority bayrağıyla LCP 3,4 saniyeden 1,8 saniyeye indi. Tek satırlık değişikliklerin en yüksek getirili olduğu yer burasıdır.

Fontu next/font ile derlemeye bağlayın

Metin bloğu LCP adayıysa, fontun geç gelmesi doğrudan LCP gecikmesidir. next/font fontu derleme zamanında indirip kendi alan adınızdan servis eder; üçüncü taraf bir alan adına bağlanma, DNS çözümleme ve yönlendirme maliyeti ortadan kalkar.

// app/fonts.ts
import { Inter } from "next/font/google";

export const inter = Inter({
  subsets: ["latin", "latin-ext"],
  display: "swap",
  variable: "--font-sans",
  preload: true,
});

Türkçe içerikte latin-ext alt kümesini eklemeyi unutmayın; yoksa özel karakterler yedek fontla çizilir ve hem görünüm hem kayma bozulur. Ağırlık sayısını da kısıtlayın: her ek kalınlık ayrı bir dosyadır.

TTFB: dinamik render kaçaklarını kapatın

Sunucu yanıtı geç geliyorsa hiçbir istemci optimizasyonu kurtarmaz. App Router'da en sık gördüğüm sorun, sayfanın farkında olmadan tamamen dinamik hâle gelmesidir: derinlerde okunan bir çerez, bir istek başlığı ya da arama parametresi bütün rotayı istek zamanına taşır.

  • Rota segmentinin statik mi dinamik mi render edildiğini derleme çıktısından doğrulayın. Beklediğinizden farklıysa nedeni bulun.
  • İçerik yönetiminden gelen sayfalarda ISR kullanın; her istekte veritabanına gitmeyin.
  • Önbelleği süreyle değil, yayımlama akışına bağlı etiketlerle temizleyin. Böylece hem taze içerik hem düşük TTFB elde edersiniz.
  • Kişiselleştirmeyi tüm sayfaya değil, küçük bir istemci adasına ya da akışa alınan bir parçaya hapsedin.

Aynı konuyu metadata ve kanonik kurgusu tarafıyla birlikte Next.js App Router ile güçlü SEO kurulumu yazısında da ele almıştım.

Kaynak keşfini erkene alın

Tarayıcı bir kaynağı ancak keşfettiği anda indirmeye başlar. LCP adayınız bir stil dosyasının içinden ya da bir istemci bileşeni render edildikten sonra geliyorsa, keşif geç olur ve hiçbir sıkıştırma bunu telafi etmez.

  • LCP adayını sunucudan gelen HTML'e basın; arka plan görseli olarak stil dosyasına gömmeyin.
  • Görsel uzak bir alan adından geliyorsa bağlantıyı erkenden açın; el sıkışma maliyeti mobil bağlantıda azımsanmayacak kadar yüksektir.
  • Görünür alanı çerez bildirimi ya da bir modal kaplıyorsa LCP adayı artık odur; dünyanın en hızlı görseli sizi kurtarmaz.
  • Görünür alanın altındaki hiçbir görsele öncelik vermeyin; orada tembel yükleme varsayılan kalsın.

Bu dört maddeyi atlayan projelerde, görsel boyutunu yarıya indirmek bile LCP'de beklenen kazancı vermez; çünkü darboğaz indirme değil, keşiftir.

INP: istemciye giden JavaScript'i azaltın

INP'i bozan şey, ana iş parçacığını kilitleyen uzun görevlerdir. Next.js'te bunun kaynağı neredeyse her zaman gereğinden fazla istemci bileşenidir.

Sunucu bileşeni varsayılan, "use client" yaprakta

Sunucu bileşenleri varsayılan olmalı. Kritik ayrıntı şu: bir dosyaya "use client" yazdığınızda yalnız o bileşen değil, altındaki tüm ağaç istemciye taşınır. Bu yüzden sınırı olabildiğince aşağı, yaprağa itin. Etkileşimli düğmeyi istemci bileşeni yapın, düğmeyi barındıran sayfayı değil.

Sık gördüğüm ikinci hata, tarih biçimlendirme, metin dönüştürme veya markdown render gibi ağır işlerin istemciye kaçmasıdır. Bunlar sunucuda bir kez yapılır, sonuç HTML olarak gider ve kullanıcı cihazında hiç maliyeti olmaz. Hidrasyon süresi de aynı oranda kısalır.

Uzun görevleri bölün

İstemci JavaScript'ini azalttıktan sonra geriye kalan INP sorunları genelde tek bir uzun görevden çıkar: her tuş vuruşunda çalışan ağır bir filtre, yüzlerce satırlık bir listenin baştan render edilmesi ya da tıklama anında senkron yapılan bir hesaplama.

  • Uzun listelerde sanallaştırma kullanın; ekranda olmayan satırı hiç render etmeyin.
  • Arama ve filtre alanlarında girdiyi anında gösterip pahalı sonucu erteleyin; React'in ertelenmiş değer ve geçiş API'leri tam bunun için var.
  • Tıklama işleyicisinde ağır iş yapmayın. Önce görsel geri bildirimi verin, hesabı sonraya bırakın: INP bir sonraki boyamaya kadar geçen süreyi ölçer, kullanıcı da hesabın bittiğini değil ekranın tepki verdiğini görür.
  • Gerçekten pahalı hesaplamayı sunucuya ya da bir web worker'a taşıyın.

Bu dört düzeltme, kullanıcı sayısı arttıkça değil, veri büyüdükçe önem kazanır; küçük veriyle test edilen bir arayüz production'da bambaşka davranır.

Ağır parçaları akışa alın

Sayfanın tamamını en yavaş veriye bağlamak yerine, yavaş parçayı Suspense sınırına alıp gerisini hemen gönderin. Kullanıcı içeriği daha erken görür, kabuk daha erken etkileşime hazır olur.

import { Suspense } from "react";

export default function Page() {
  return (
    <>
      <Hero />
      <Suspense fallback={<YorumIskeleti />}>
        <Yorumlar />
      </Suspense>
    </>
  );
}

Yedek içeriğin yüksekliği gerçek içerikle aynı olsun; aksi halde INP'i düzeltirken CLS'i bozarsınız.

Dinamik import ve paket analizi

Grafik kütüphaneleri, zengin metin editörleri, harita ve takvim bileşenleri ilk yüklemede gerekmez.

import dynamic from "next/dynamic";

const Grafik = dynamic(() => import("@/components/grafik"), {
  loading: () => <GrafikIskeleti />,
  ssr: false,
});

Paket analizini düzenli çalıştırın. Sürprizlerin çoğu aynı yerden çıkar: tek bir yardımcı için çekilen dev bir kütüphane, ikon setinin tamamının içeri alınması, sunucuda kalması gereken bir bağımlılığın istemci paketine sızması.

Üçüncü taraf betikleri next/script ile hizaya sokun

Çoğu sitede en büyük tek kazanç, gerçekten gerekmeyen bir betiği kaldırmaktır. Kalanları da doğru stratejiye bağlayın:

  • afterInteractive: etiket yöneticisi ve ölçüm gibi, sayfa etkileşime hazır olduktan hemen sonra gereken betikler.
  • lazyOnload: sohbet penceresi, ısı haritası, sosyal gömme — geciktirilebilen her şey.
  • beforeInteractive: neredeyse hiçbir zaman. Yalnızca içerik boyanmadan çalışması zorunlu olan onay betikleri.
import Script from "next/script";

<Script src="https://widget.example.com/chat.js" strategy="lazyOnload" />;

Bir betiği eklemeden önce tek bir soru sorun: bu olmasa ne kaybederiz? Cevap net değilse eklemeyin.

CLS: yeri önceden ayırın

CLS, düzeltmesi en ucuz ama en çok ihmal edilen metriktir. Üç kaynak neredeyse tüm vakaları açıklar.

  • Boyutsuz görsel ve gömülü içerik. next/image ile her zaman genişlik ve yükseklik verin ya da fill kullanıp konteynere sabit bir en boy oranı tanımlayın.
  • Font kaynaklı kayma. next/font yedek font ölçülerini otomatik hizalar; font değiştiğinde satır yüksekliği zıplamaz.
  • Sonradan gelen arayüz. Çerez bildirimi, duyuru çubuğu, kişiselleştirilmiş blok. Alanı baştan ayırın; iskelet yüksekliği gerçek içerikle aynı olsun.

Yakın zamanda bir projede CLS'in tek kaynağı, sepet sayısını istemcide hesaplayıp sonradan ekleyen bir başlık bileşeniydi. Rozet alanına sabit genişlik vermek metriği tek hamlede yeşile taşıdı.

Regresyonu CI'da kilitleyin

Performans tek seferlik bir iş değildir; her yeni özellik biraz geri çeker. Bu yüzden kazandığınız yeri kilitleyin:

  • Performans bütçesi tanımlayın: rota başına istemci paketi boyutu ve laboratuvar LCP değeri için üst sınır koyun.
  • Lighthouse CI'ı hattınıza ekleyin; eşiğin altına düşen değişikliği birleştirmeyin.
  • Paket boyutunu her değişiklikte raporlayın. Sayı görünür olduğunda kimse fark etmeden büyüyemez.
  • Saha verisini şablon bazında izleyin. Laboratuvar yeşil, saha kırmızıysa sorun gerçek cihazlarda ve üçüncü taraf betiklerdedir.

Bu ritmi süreklileştirmenin nasıl işlediğini kurumsal sitelerde sürekli teknik SEO bakımı yazısında ayrıntılandırdım.

Özet kontrol listesi

  • Hero görseli next/image ile: tek priority, doğru sizes, AVIF ve WebP açık.
  • Font next/font ile, latin-ext dahil, ağırlık sayısı sınırlı.
  • Rota gerçekten statik ya da ISR ile önbellekli; dinamik render kaçağı yok.
  • Sunucu bileşeni varsayılan; "use client" yaprakta ve dar kapsamda.
  • Yavaş parçalar Suspense ile akışta; iskelet yükseklikleri gerçekle aynı.
  • Ağır bileşenler dinamik import ile; paket analizi düzenli çalışıyor.
  • Üçüncü taraf betikler lazyOnload veya afterInteractive; gereksizler silinmiş.
  • Tüm görsel ve gömülü alanların yeri baştan ayrılmış.
  • Saha verisi toplanıyor, CI'da performans bütçesi kilitli.

Core Web Vitals'ı yeşile çekmek sihir değil, disiplin işidir: doğru API'yi doğru yerde kullanmak ve kazandığınız yeri geri vermemek. Projenizin performans tarafını birlikte ele almak isterseniz SEO ve performans hizmetime göz atabilir ya da doğrudan bana yazabilirsiniz.

Devamı

İlgili yazılar

SEO ve Performans
8 dk okuma

Lansman Öncesi Teknik SEO Kontrol Listesi

Bir siteyi yayına almadan hemen önce ve yayından sonraki ilk 48 saatte tek tek geçtiğim, lansman anına özgü teknik SEO kontrolleri.

SEO ve Performans
9 dk okuma

Kurumsal Sitelerde Sürekli Teknik SEO Bakımı

Yayına aldıktan sonra teknik SEO'yu ayakta tutan bakım ritmi: haftalık alarmlar, aylık indeks kapsamı, çeyreklik log analizi ve yıllık denetim.

SEO ve Performans
7 dk okuma

Core Web Vitals İçin Pratik Performans Rehberi

LCP, INP ve CLS gerçekte neyi ölçüyor, saha verisi ile lab verisi neden farklı ve hangi metriği önce düzeltmelisin? Ölçüm odaklı bir rehber.

Sırada ne var?

Bir fikriniz mi var? Hayata geçirelim.

merhaba@selupucin.com

Genelde 24 saat içinde dönüş yapıyorum.