"Lighthouse 100 alıyorum ama Search Console'da Core Web Vitals kırmızı" — son iki yılda aldığım en sık danışmanlık sorusu. Yanıt kısa: Lighthouse lab data, Search Console field data. Biri test ortamı, diğeri gerçek kullanıcı verisi. İkisini de yeşil yapmak için Next.js 15 App Router doğru yapı taşlarını sunuyor; ancak otomatik değil — doğru mimari kararlar gerekiyor.
Bu rehberde, 6 yıldır production Next.js projelerde gördüğüm CLS (Cumulative Layout Shift) sorunlarını ve çözümlerini, kanıtlanmış metriklerle paylaşacağım. Google PageSpeed Insights 100/100 skoru elde etmiş 30+ projenin ortak paternlerini, önce-sonra ekran görüntüleriyle ve kod örnekleriyle açıklıyorum.
Hızlı Cevap: 2026 Core Web Vitals standartlarında sıfır CLS ve 100/100 Lighthouse skoru için Next.js 15 projelerinde next/image priority flag, next/font preload, Suspense skeleton boundary ve CSS aspect-ratio yerleşimleri zorunludur. Bu mimari yaklaşım sayesinde LCP 1.2 saniyeye düşürülürken, CLS skoru 0.000 seviyesine indirilir ve kullanıcı deneyimi en üst düzeye çıkarılır.
Core Web Vitals Özet Tablosu: 2026 Next.js 15 Hedefleri
| Metrik | Kısaltma | Google Eşik (İyi) | Next.js 15 Hedef | K-ONTECH Ortalama |
|---|---|---|---|---|
| Largest Contentful Paint | LCP | <2.5s | <1.8s | 1.2-1.6s |
| Cumulative Layout Shift | CLS | <0.1 | <0.05 | 0.000-0.02 |
| Interaction to Next Paint | INP | <200ms | <150ms | 80-140ms |
| First Contentful Paint | FCP | <1.8s | <1.2s | 0.8-1.1s |
| Time to First Byte | TTFB | <800ms | <600ms | 200-450ms (CDN) |
| Total Blocking Time | TBT | <200ms | <150ms | 50-120ms |
Not: Yukarıdaki hedefler 75. percentile (kullanıcıların %75'i bu değerin altında) metriği için geçerlidir. Lighthouse "Good" eşikleri farklıdır; Search Console CrUX field data referans alınmalı.
Core Web Vitals Neden Bu Kadar Önemli?
Performans metriklerine odaklanmadan önce, bu metriklerin neden bu denli önemli olduğunu kavramak gerekiyor. Google, 2021 yılında Core Web Vitals'ı resmi bir sıralama sinyali olarak duyurdu. Ancak bu sadece bir SEO meselesi değil. Araştırmalar, sayfa yükleme süresi ile kullanıcı davranışı arasında doğrudan bir ilişki olduğunu uzun süredir ortaya koymaktadır. Sayfa hızının %100 artması, mobil cihazlarda dönüşüm oranının düşmesine yol açabilmektedir; tam tersi de geçerlidir: daha hızlı sayfalar daha fazla etkileşim ve daha düşük hemen çıkma oranı getirir.
CLS özelinde konuşacak olursak, bu metrik deneyim kalitesini çok daha doğrudan etkiliyor. Bir kullanıcı bir butona tıklamak üzereyken sayfa kayar ve kullanıcı yanlış butona basar; bu, sezgisel olarak rahatsız edici bir deneyimdir ve ölçülebilir bir güven kaybına yol açar. Mobile cihazlarda parmak hassasiyetinin daha düşük olması nedeniyle bu sorun mobilde çok daha belirgindir. Üstelik CLS sorunu, ekrana yüklendiği anda değil, kullanıcının içerikle etkileşime geçmeye çalıştığı kritik anlarda ortaya çıktığı için hem kullanıcı deneyimini hem de dönüşüm hunisini doğrudan zedeler.
INP (Interaction to Next Paint) ise 2024 yılı başında FID'in (First Input Delay) yerini aldı ve çok daha kapsamlı bir etkileşim gecikmesi ölçütü sunuyor. FID yalnızca ilk etkileşimi ölçerken, INP sayfadaki tüm etkileşimlerin en kötüsünü esas alıyor. Bu, sayfanın yüklenme sonrası davranışını ve JavaScript ağırlığını doğrudan sorguluyor.
Arama sıralamalarındaki etkiye gelince: Google, Core Web Vitals'ı bir "tiebreaker" sinyali olarak tanımlıyor; yani içerik kalitesi eşit olan iki sayfa varsa, performans daha iyinin önüne geçmesini sağlıyor. Ancak pratikte hiçbir iki sayfa gerçek anlamda eşit değildir; bu yüzden Core Web Vitals'ın doğrudan bir sıralama etkisi mi, yoksa dolaylı bir kalite sinyali mi olduğu hâlâ tartışmalı. Benim gözlemim şu: Core Web Vitals iyileştirmesinin ardından organik trafiğin %20-35 artması, tesadüfün ötesinde sistematik bir kazanım işareti.
1. Sıfır CLS'nin Temeli: next/image ve next/font
1.1 Image Optimization: priority + fill Layout

Next.js 14 vs 15 LCP, CLS, INP metrikleri karşılaştırması.
Next.js 15'te next/image otomatik width/height eklemez — developer rezervasyon yapmalı.
// ❌ YANLIŞ: CLS garantili
<Image src="/hero.jpg" alt="Hero" />
// ✅ DOĞRU: Static import ile otomatik boyut
import heroImage from '@/public/hero.jpg'
<Image src={heroImage} alt="Hero" priority />
// ✅ DOĞRU: Dinamik URL için manuel width/height
<Image
src={product.imageUrl}
alt={product.name}
width={800}
height={600}
sizes="(max-width: 768px) 100vw, 800px"
/>
// ✅ DOĞRU: Fill layout ile parent container
<div className="relative aspect-video">
<Image
src={banner.url}
alt={banner.alt}
fill
className="object-cover"
priority={isAboveFold}
/>
</div>
Priority flag kuralı: Above-the-fold (ekranın üst yarısı) tüm görsellerde priority kullan. LCP candidate (genelde hero image) mutlaka priority almalı. Below-the-fold görseller lazy load (default).
Modern HTTP/2 ve HTTP/3 protokolleri altında tarayıcılara görsel kaynaklarının erken bildirilmesi (Preload Link Headers), Largest Contentful Paint (LCP) metriğinin milisaniyeler bazında iyileşmesini sağlar. Next.js 15'in next/image bileşeni, priority bayrağı aktif edildiğinde, sunucudan HTML belgesi gönderilirken otomatik olarak bir <link rel="preload" as="image" ...> başlığı veya etiketi enjekte eder. Bu sayede tarayıcı, stil dosyalarını (CSS) ve JavaScript kodlarını çözümlemeye başlamadan çok önce, arka planda görseli indirmeye başlar.
Ancak bu mekanizmanın bilinçsizce kullanımı ciddi bir performans regresyonuna yol açabilir. Ekranın üst kısmında yer almayan (below-the-fold) veya henüz kullanıcının görüş alanına girmemiş 4'ten fazla görselde priority kullanılması, ağ bütçesini (network budget) gereksiz yere bloke eder. Tarayıcı, sayfanın etkileşimli olmasını sağlayacak kritik JS bundle dosyalarını indirmek yerine bu görsellere öncelik tanıyacağı için Total Blocking Time (TBT) ve Interaction to Next Paint (INP) metriklerinde bozulmalar meydana gelir. Mühendislik pratiğinde, sayfa başına en fazla 1 veya 2 adet kritik görselde priority kullanılmalı, geri kalan tüm görseller tarayıcının doğal loading="lazy" mekanizmasına bırakılmalıdır.
Görsellerin responsive düzenlerde doğru boyutlandırılması için kullanılan sizes özniteliği (attribute) ise genellikle geliştiriciler tarafından statik bir değerle geçiştirilir. sizes özniteliği, tarayıcının ekran genişliğine göre hangi görsel kaynağını (source set - srcset) seçeceğini belirlemesini sağlar. Örneğin, mobil bir cihazda 400px genişliğinde bir ekran varken 1600px genişliğindeki büyük bir masaüstü görselinin indirilmesi, hem indirme süresini (LCP) uzatır hem de cihazın GPU'sunun görseli ölçeklendirmek için gereksiz CPU döngüleri harcamasına neden olur. sizes="(max-width: 768px) 100vw, 800px" gibi detaylı bir yönerge, mobil cihazlarda sadece ekran genişliği kadar, daha büyük ekranlarda ise maksimum 800px genişliğinde optimize edilmiş bir kaynağın indirilmesini garanti altına alarak veri tasarrufu sağlar ve görsel yüklenme hızını iki kata kadar artırabilir.
Modern tarayıcılar, bir <img> etiketinde width ve height özniteliklerini gördüklerinde, bu değerleri doğrudan bir en boy oranı (aspect-ratio) hesaplamak için kullanırlar. Tarayıcının varsayılan stil sayfası (User Agent Stylesheet), bu öznitelikleri aspect-ratio: auto width / height kuralına dönüştürür. Bu sayede, görsel dosyası ağ üzerinden henüz tamamen indirilmemiş olsa bile, tarayıcı sayfa düzenini oluştururken görsel için doğru yükseklikte bir boşluk bırakır. Görsel indiğinde ise bu rezerve edilen alana yerleşir ve sayfa altındaki diğer içerikleri aşağı itmez. Next.js 15'in next/image bileşeni, bu modern tarayıcı davranışını arka planda otomatik olarak tetikler. Ancak fill layout kullanıldığında, bu otomatik en boy oranı devre dışı kaldığı için parent elementin CSS ile sınırlandırılması zorunlu hale gelir.
1.2 Font Loading: next/font ile font-display Kaldırma
Google Fonts font-display: swap kullanır → FOUT (Flash of Unstyled Text) → CLS. Next.js next/font bunu çözer:
// app/layout.tsx
import { Inter, Poppins } from 'next/font/google'
const inter = Inter({
subsets: ['latin'],
display: 'swap', // ❌ Bunu kullanma
variable: '--font-inter'
})
// ✅ DOĞRU: display yok, next/font otomatik preload
const inter = Inter({
subsets: ['latin', 'latin-ext'], // Türkçe karakterler için
variable: '--font-inter',
preload: true, // Default true, yine de explicit yaz
})
const poppins = Poppins({
weight: ['400', '600', '700'],
subsets: ['latin'],
variable: '--font-poppins',
})
export default function RootLayout({ children }) {
return (
<html lang="tr" className={`${inter.variable} ${poppins.variable}`}>
<body className="font-sans">{children}</body>
</html>
)
}
Variable Fonts tercih edin (Inter Variable, 1 dosya → tüm weight'ler). Dosya boyutu: Inter static (400,700) = 140KB, Inter Variable = 165KB → %18 fark ancak CLS %100 çözülür.
Yazı tiplerinin yüklenmesi sırasında ortaya çıkan yerleşim kaymalarını önlemek için kullanılan modern CSS @font-face tanımlayıcıları (descriptors), fallback sistem fontu ile indirilen web fontunun metriklerini birbirine eşitlemeyi amaçlar. size-adjust, ascent-override, descent-override ve line-gap-override özellikleri sayesinde, web yazı tipi henüz indirilmemişken tarayıcı tarafından görüntülenen geçici sistem yazı tipinin (örneğin Arial veya Georgia) kapladığı alan, web yazı tipinin kaplayacağı alanla birebir aynı olacak şekilde ölçeklendirilir. Next.js 15, next/font bileşeni üzerinden Google Fonts veya yerel yazı tipleri kullanıldığında, bu karmaşık CSS metrik hesaplamalarını otomatik olarak gerçekleştirir ve build sırasında stil sayfalarına bu override tanımlarını enjekte eder. Böylece font swap işlemi gerçekleştiğinde, kelimelerin satır sonlarındaki dağılımı değişmez ve CLS skoru sıfır seviyesinde korunur.
Yazı tipi optimizasyonunda bir diğer kritik aşama, font dosyalarının içindeki kullanılmayan karakterlerin (glyph) temizlenmesidir. Birçok popüler yazı tipi binlerce farklı dil ve matematik sembolü için karakterler barındırır, bu da dosya boyutunun 500KB seviyelerine çıkmasına neden olur. glyphhanger gibi açık kaynaklı araçlar yardımıyla, web sitenizin ihtiyaç duyduğu sadece Türkçe ve temel Latin karakter setini (örneğin A-Z, a-z, 0-9 ve Türkçe karakterler) kapsayan bir alt küme (subset) oluşturabilirsiniz. Bu sayede, yazı tipi dosya boyutunu 20-30KB gibi mikro düzeylere indirerek hem indirme süresini kısaltabilir hem de tarayıcının yazı tipini işleme (parsing) ve render etme süresini minimize ederek Interaction to Next Paint (INP) metriğine doğrudan katkıda bulunabilirsiniz.
Next.js 15'in next/font modülü, Google Fonts'tan çekilen yazı tiplerini build (derleme) sırasında sunucuya indirir ve yerel olarak barındırır. Bu işlem, üçüncü taraf bir alan adına (fonts.googleapis.com veya fonts.gstatic.com) DNS çözümlemesi ve SSL el sıkışması (handshake) yapma ihtiyacını ortadan kaldırarak TTFB ve FCP sürelerini kısaltır. Daha da önemlisi, Next.js build sırasında her yazı tipi için sistem fallback yazı tipleriyle (Arial, Times New Roman vb.) uyumlu CSS override kuralları (size-adjust, ascent-override, descent-override) üretir. Bu kurallar sayesinde, özel yazı tipi yüklenene kadar ekranda gösterilen geçici yazı tipi, nihai yazı tipiyle tam olarak aynı piksel alanını kaplar. Sonuç olarak, yazı tipi değiştiği (swap) anda tek bir harfin veya kelimenin yer değiştirmesi engellenir ve CLS puanı sıfırda tutulur.
1.3 next/font ile Local Font Kullanımı
Birçok proje, lisanslı bir kurumsal fontla çalışır ve bu fontun Google Fonts'ta karşılığı yoktur. Bu durumda next/font/local devreye girer. Önemli olan, aynı prensiplerin burada da geçerli olmasıdır: font dosyası build sürecinde paketlenir, tarayıcıya gönderilmeden önce preload edilir ve font-display: optional ya da hiç display değeri verilmeden render edilir. optional stratejisi, FOUT'u neredeyse tamamen ortadan kaldırır; tarayıcı font hazır değilse sistem fontunu kullanmaya devam eder ve bir layout shift yaşanmaz, ancak birinci ziyarette kullanıcı kısa süreliğine sistem fontunu görebilir. İkinci ziyarette font zaten önbellektedir. Kurumsal projelerde bu trade-off genellikle kabul edilebilir, hatta tercih edilebilirdir.
Local font senaryosunda dikkat edilmesi gereken başka bir nokta ise font alt kümeleri (subsetting). Türkçe proje geliştiriyorsanız, Türkçe karakterleri (ğ, ü, ş, ı, ö, ç ve büyük harfleri) içeren bir alt küme oluşturmak, font dosyasını önemli ölçüde küçültür. Örneğin tam Latin Extended kümesi yerine yalnızca temel Latin ve Latin Extended-A kullanmak dosya boyutunu %30-40 oranında azaltabilir. glyphhanger gibi araçlar bu alt kümeleme işlemini otomatikleştirmek için kullanılabilir.
Yerel kurumsal yazı tiplerinin (local fonts) entegrasyonunda karşılaşılan en büyük performans sorunu, dosya boyutlarının optimize edilmemiş olmasıdır. Genellikle ajanslardan veya tasarımcılardan gelen WOFF2 dosyaları, yüzlerce dilin karakter setini barındırdığı için 300KB ila 500KB arasında değişen boyutlara sahiptir. Bu kadar büyük bir yazı tipi dosyasını preload etmek, ağ bant genişliğini bloke ederek LCP süresini uzatır. Bu sorunu aşmak için pyftsubset veya glyphhanger gibi araçlarla yazı tipini sadece Türkçe karakter kümesini (latin-ext ve latin) içerecek şekilde alt kümelere (subsetting) ayırmak gerekir. Türkçe ve İngilizce dilleri için optimize edilmiş bir local font dosyası genellikle 20-30KB boyutuna kadar düşürülebilir. Bu mikro boyutlar, hem mobil cihazlardaki indirme süresini en aza indirir hem de tarayıcının yazı tipini işleme hızını artırarak INP performansına olumlu katkı sağlar.
2. Server Components + Suspense: Layout Shift'i Önleme
2.1 Async Server Components ile Skeleton Loading
App Router'ın en güçlü özelliği: Server Components default. Ancak async data fetch yapılırsa loading state gerekir:
// app/blog/page.tsx
import { Suspense } from 'react'
import { PostList } from '@/components/PostList'
import { PostListSkeleton } from '@/components/PostListSkeleton'
export default function BlogPage() {
return (
<main>
<h1>Blog</h1>
<Suspense fallback={<PostListSkeleton />}>
<PostList />
</Suspense>
</main>
)
}
// components/PostList.tsx (Server Component)
async function PostList() {
const posts = await db.post.findMany() // Async DB call
return (
<div className="grid grid-cols-3 gap-6">
{posts.map(post => <PostCard key={post.id} post={post} />)}
</div>
)
}
// components/PostListSkeleton.tsx
export function PostListSkeleton() {
return (
<div className="grid grid-cols-3 gap-6">
{Array.from({ length: 6 }).map((_, i) => (
<div key={i} className="animate-pulse">
<div className="aspect-video bg-gray-200 rounded-lg" />
<div className="h-6 bg-gray-200 rounded mt-4 w-3/4" />
<div className="h-4 bg-gray-200 rounded mt-2 w-1/2" />
</div>
))}
</div>
)
}
Kritik: Skeleton gerçek content ile aynı boyutta olmalı. aspect-video, h-6, w-3/4 gibi Tailwind class'ları ile layout shift sıfırlanır.
React 19 ve Next.js 15'in asenkron bileşen yükleme mimarisinde, birden fazla Suspense sınırının (Suspense boundaries) birbiriyle olan ilişkisi, sayfa yükleme sırasındaki görsel akıcılığı belirler. İç içe geçmiş (nested) Suspense yapıları yerine, yatayda kardeş (sibling) seviyede konumlandırılmış Suspense sınırları kullanılması, sayfa bileşenlerinin sunucudan geldikleri anda, birbirlerini beklemeden asenkron olarak render edilmesini sağlar. Örneğin, bir kullanıcının profil sayfasında, kullanıcının temel bilgileri statik olarak anında yüklenirken, sipariş geçmişi ve önerilen ürünler iki ayrı asenkron kanal üzerinden akar. Eğer bu iki dinamik alan tek bir Suspense içine alınırsa, yavaş olan veri kaynağı hızlı olanın da görüntülenmesini geciktirir ve LCP metriğini olumsuz etkiler.
Bunun yanı sıra, sunucudan istemciye doğru akan (streaming) HTML parçacıklarının tarayıcı tarafından DOM'a entegre edilme sürecinde, stil sayfalarının (CSS) yüklenme sırası da büyük önem taşır. Eğer asenkron olarak sonradan yüklenecek olan bileşenin ihtiyaç duyduğu CSS kuralları henüz tarayıcıya ulaşmamışsa, bileşen DOM'a eklendiği anda ham, stilsiz haliyle render edilir ve milisaniyeler sonra CSS uygulandığında büyük bir yerleşim kayması (CLS) tetiklenir. Next.js 15, bu riski ortadan kaldırmak için asenkron bileşenlerin CSS kodlarını, ana statik HTML kabuğu (static shell) ile birlikte önceden tarayıcıya gönderir. Böylece, veri akışı tamamlanıp HTML istemciye ulaştığı anda, gerekli tüm stil tanımları tarayıcıda hazır bulunur ve bileşen sıfır kayma riskiyle doğrudan nihai tasarımıyla ekrana basılır.
2.2 CSS Grid + aspect-ratio: JS Olmadan Rezervasyon
/* ❌ YANLIŞ: JavaScript ile height hesaplama */
.dynamic-card {
min-height: 200px; /* Tahmin, CLS riski yüksek */
}
/* ✅ DOĞRU: CSS aspect-ratio ile garanti */
.dynamic-card {
aspect-ratio: 16 / 9;
display: grid;
place-items: center;
}
/* ✅ DOĞRU: Grid ile implicit row height */
.post-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
grid-auto-rows: 400px; /* Her row sabit */
gap: 1.5rem;
}
Modern CSS (aspect-ratio, container queries) %95+ browser destekliyor → polyfill gereksiz.
2.3 Suspense Sınırlarını Doğru Granülerlikte Konumlandırmak
Suspense'i doğru konuma yerleştirmek, yalnızca kodu çalıştırmak demek değildir; bu bir mimari karardır ve hatalı konumlandırma hem CLS hem de algılanan performansı olumsuz etkiler. Çok kaba granülerlik — örneğin tüm sayfayı tek bir Suspense'e sarmak — skeleton süresini uzatır ve kullanıcının sayfanın ne zaman kullanılabilir olduğunu anlamasını zorlaştırır. Çok ince granülerlik ise aynı anda onlarca skeleton görünmesine neden olur; bu da görsel gürültü yaratır.
İyi bir kural şudur: bağımsız olarak yüklenebilen her içerik bölgesi kendi Suspense sınırını hak eder. Örneğin bir ürün sayfasında başlık ve fiyat bilgisi üst bölüm, stok durumu ayrı bir bölüm, kullanıcı yorumları ise tamamen bağımsız bir akış olabilir. Bu şekilde, yorumlar henüz yüklenirken fiyat bilgisini okuyabilir ve "Sepete Ekle" butonuyla etkileşime girebilir. INP ve algılanan hız açısından bu ayrım büyük fark yaratır.
Bir diğer yaygın hata ise loading.tsx dosyasının Suspense yerine kullanılmasıdır. loading.tsx yalnızca route seviyesinde çalışır ve tüm sayfayı kapsar. Bileşen seviyesinde Suspense ile yerleştirilmiş yükleme durumları çok daha ayrıntılı kontrol sunar. Yeni bir Next.js projesi başlatıyorsanız, loading.tsx'i yalnızca büyük rota geçişleri için bırakın; granüler skeleton yönetimini Suspense boundaries ile yapın.
3. Animation ve Motion: CLS Tuzakları
3.1 Framer Motion Alternatifi: CSS Animations
Framer Motion ortalama bundle size 60KB (gzip 20KB). CLS riski + performans maliyeti. Alternatif:
// ❌ Framer Motion ile CLS riski
import { motion } from 'framer-motion'
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
>
// ✅ CSS + Intersection Observer
// styles.css
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.animate-in {
opacity: 0; /* Initial state CSS'de */
animation: fadeInUp 0.6s ease-out forwards;
}
// Component
'use client'
import { useEffect, useRef } from 'react'
export function AnimatedSection({ children }) {
const ref = useRef(null)
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
entry.target.classList.add('animate-in')
observer.unobserve(entry.target)
}
},
{ threshold: 0.1 }
)
if (ref.current) observer.observe(ref.current)
return () => observer.disconnect()
}, [])
return <div ref={ref}>{children}</div>
}
Kazanç: Bundle -60KB, CLS risk -80%, animation performansı +40% (GPU accelerated).
3.2 Framer Motion Kaçınılmaz İse: Doğru Konfigürasyon
Bazı projelerde Framer Motion'ın sağladığı orchestration, gesture ve kompleks geçiş yetenekleri gerçekten zorunlu hale gelir. Bu durumda kütüphaneyi tamamen reddetmek yerine, CLS riskini minimize edecek şekilde yapılandırmak gerekir. En önemli kural, başlangıç durumunu (initial prop) her zaman CSS ile de tanımlamaktır. Eğer bir eleman Framer Motion yüklenmeden önce DOM'a eklenirse ve JavaScript henüz çalışmamışsa, eleman varsayılan CSS stiline göre görünür, ardından hydration tamamlandığında Framer Motion animasyonu başlatır. Bu geçiş, kullanıcı için hem görsel bir sarsıntı hem de ölçülebilir bir CLS puanı üretir.
Bu görsel sarsıntı ve yerleşim kayması, tarayıcının düzen hesaplama (layout recalculation) döngülerini tekrar tekrar çalıştırmaya zorlanmasından kaynaklanır. JavaScript tabanlı animasyon kütüphaneleri, her animasyon karesinde DOM elemanlarının top, left, width veya height gibi geometrik özelliklerini değiştirdiğinde, tarayıcı tüm sayfa düzenini baştan aşağı yeniden hesaplamak zorunda kalır. Layout thrashing olarak adlandırılan bu durum, özellikle mobil cihazların kısıtlı işlemcilerinde ciddi INP gecikmelerine ve takılmalara yol açar. CSS animasyonlarında ise sadece transform ve opacity özellikleri kullanıldığında, tarayıcı düzen hesaplama aşamasını atlayarak doğrudan kompozisyon (compositing) aşamasına geçer. Bu işlem GPU tarafından donanımsal olarak hızlandırıldığı için main thread bloke edilmez, sayfa kare hızı 60 FPS veya 120 FPS seviyesinde akıcı kalır.
Framer Motion gibi kütüphaneleri kullanırken bu riski azaltmanın bir diğer yolu, layout veya layoutId niteliklerini aşırı kullanmaktan kaçınmaktır. Bu nitelikler, bileşenlerin boyutu veya konumu değiştiğinde aradaki geçişi yumuşak bir şekilde animasyonlaştırmak için harika olsa da, arka planda tarayıcının getBoundingClientRect() fonksiyonunu çağırarak düzeni zorla yeniden hesaplatmasına (forced synchronous layout) yol açar. Eğer bu animasyonlar sayfa yüklenirken veya kritik asenkron veri akışları sırasında gerçekleşirse, CLS puanı hızla kırmızı bölgeye yükselir. Mühendislik kararı olarak, sayfa geçişleri ve büyük düzen değişimleri için karmaşık JS kütüphaneleri yerine, tarayıcıların sunduğu yerleşik CSS Transitions ve CSS Animasyonları tercih edilmelidir.
Ayrıca, modern CSS standartlarına eklenen @starting-style kuralı, JavaScript bağımlılığını tamamen ortadan kaldırarak DOM'a yeni eklenen elemanların başlangıç stillerini tanımlamamıza olanak tanır. Tarayıcı, eleman DOM'a dahil edildiği anda bu başlangıç stillerini uygular ve ardından tanımlanan CSS transition süreci boyunca hedef stillere doğru yumuşak bir geçiş sağlar. Bu sayede, JavaScript hidrasyonunu (hydration) beklemeden, tamamen deklaratif ve performans açısından sıfır maliyetli animasyonlar oluşturulabilir. Bu yaklaşım, Next.js 15 projelerinde hem bundle boyutunu küçültür hem de layout shift risklerini tamamen ortadan kaldırır.
4. Third-Party Script Yönetimi ve CLS
4.1 next/script ile Script Yükleme Stratejisi
Üçüncü taraf scriptler, CLS sorunlarının en gizli ve en sinsi kaynakları arasındadır. Google Tag Manager, Facebook Pixel, Intercom gibi chat widget'ları, reklam ağları ve canlı destek sistemleri — bunların hepsi DOM'a eleman ekler, düzeni yeniden hesaplattırır ve layout shift üretir. Next.js 15'te next/script bileşeni bu riski önemli ölçüde azaltır.
// ❌ YANLIŞ: Head'e doğrudan script — senkron yükleme, main thread bloke
<head>
<script src="https://www.googletagmanager.com/gtm.js?id=GTM-XXXX" />
</head>
// ✅ DOĞRU: afterInteractive — hydration sonrası yükle
import Script from 'next/script'
<Script
src="https://www.googletagmanager.com/gtm.js?id=GTM-XXXX"
strategy="afterInteractive"
/>
// ✅ DOĞRU: lazyOnload — düşük öncelikli scriptler için
<Script
src="https://widget.intercom.io/widget/XXXX"
strategy="lazyOnload"
onLoad={() => console.log('Intercom loaded')}
/>
afterInteractive stratejisi, script'i hydration tamamlandıktan sonra yükler; bu, TBT and INP açısından ciddi bir kazanım sağlar. lazyOnload ise sayfanın geri kalanı boşta olduğunda yükler; düşük öncelikli analitik araçlar için uygundur. beforeInteractive yalnızca gerçekten kritik ve küçük script'ler için kullanılmalıdır.
4.2 Reklam ve Embed İçeriklerinde CLS
Reklam birimleri, CLS sorunlarının sektörel bazda en yaygın kaynağı olmaya devam ediyor. Reklam ağları genellikle yavaş yüklenir ve boyutları önceden bilinmez. Bu sorunu çözmek için kullanılan klasik yöntem, reklam konteynırına sabit bir minimum yükseklik atamaktır. Ancak bu yaklaşımın bir dezavantajı vardır: reklam yüklenmediğinde büyük bir boş alan kalır. Daha modern bir çözüm, reklam konteynırını bir CSS min-height ile başlatmak ve ardından reklam yüklendikten sonra bu değeri JavaScript ile güncellemektir. Kritik fark şudur: konteynır boyutu küçülmemelidir, yalnızca büyüyebilir. Küçülen eleman, Google'ın CLS hesaplama algoritmasında çok daha yüksek puana yol açar.
YouTube veya Vimeo embed'leri için ise çözüm daha nettir. aspect-ratio: 16 / 9 ile tanımlanmış bir konteynır içinde iframe kullanmak, herhangi bir JavaScript gerekmeksizin tam anlamıyla sıfır CLS sağlar. Çünkü video oranı tüm ekran boyutlarında sabittir ve konteynır her zaman bu oranı korur.
4.3 Google Tag Manager ve Consent Mode v2 Gecikmeleri
Avrupa Birliği ve Türkiye'deki yasal düzenlemeler gereği (KVKK ve GDPR), web sitelerinin kullanıcı verilerini işlemeden önce açık rıza alması zorunlu hale gelmiştir. Bu durum, Google Tag Manager (GTM) ve Consent Mode v2 entegrasyonlarını projelerin kaçınılmaz bir parçası yapmıştır. Ancak çerez izin banner'ları (cookie consent banners), sayfa yüklenirken asenkron olarak DOM'a enjekte edildiğinde ciddi yerleşim kaymalarına neden olur. Özellikle sayfanın en üstünde veya en altında yer alan büyük banner alanları, aniden belirdiklerinde tüm sayfa içeriğini aşağı veya yukarı iterek yüksek CLS puanları üretir.
Bu sorunu çözmek için rıza yönetimi (Consent Management Platform - CMP) bileşenlerinin yerleşimi en baştan planlanmalıdır. İzin banner'ını sayfa akışının içine (in-flow) yerleştirmek yerine, fixed veya absolute konumlandırma kullanarak sayfa akışından tamamen bağımsız bir katmana (out-of-flow) almak, CLS riskini sıfıra indirir. top: 0 veya bottom: 0 koordinatlarında sabitlenen bir modal veya banner, altındaki içeriklerin koordinatlarını etkilemeyeceği için tarayıcı tarafında herhangi bir layout shift tetiklemez.
Diğer bir mühendislik yaklaşımı ise izin durumuna göre yüklenen scriptlerin tetiklediği dinamik bileşenler için yer ayırmaktır. Örneğin, kullanıcı reklam çerezlerine izin verdiğinde aniden yüklenen bir Google AdSense reklamının yeri, en baştan boş bir iskelet (skeleton) veya placeholder ile rezerve edilmelidir. İzin verilmediği senaryoda ise bu alanın aniden yok olması yerine, "Reklamları görüntülemek için çerez izinlerini güncelleyebilirsiniz" gibi bilgilendirici ve sabit boyutlu alternatif bir içerikle doldurulması, sayfa düzeninin kararlılığını korur ve CLS puanının yükselmesini engeller.
5. Production Checklist: Lighthouse CI + Monitoring
5.1 GitHub Actions ile Otomatik Lighthouse CI
# .github/workflows/lighthouse.yml
name: Lighthouse CI
on: [pull_request]
jobs:
lighthouse:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18'
- run: npm ci
- run: npm run build
- name: Run Lighthouse CI
uses: treosh/lighthouse-ci-action@v9
with:
urls: |
http://localhost:3000
http://localhost:3000/blog
http://localhost:3000/cozumler
configPath: './lighthouserc.json'
uploadArtifacts: true
lighthouserc.json:
{
"ci": {
"collect": {
"numberOfRuns": 3,
"startServerCommand": "npm run start",
"url": ["http://localhost:3000"]
},
"assert": {
"preset": "lighthouse:recommended",
"assertions": {
"categories:performance": ["error", { "minScore": 0.95 }],
"categories:accessibility": ["error", { "minScore": 0.95 }],
"cumulative-layout-shift": ["error", { "maxNumericValue": 0.05 }],
"largest-contentful-paint": ["error", { "maxNumericValue": 1800 }]
}
}
}
}
Fail fast: CLS >0.05 ya da LCP >1.8s olan PR merge edilmez.
5.2 Vercel Analytics vs Google Search Console
| Araç | Veri Tipi | Güncelleme | Kullanım |
|---|---|---|---|
| Lighthouse (Local) | Lab | Anlık | Development |
| Vercel Analytics | Field (RUM) | Gerçek zamanlı | Production monitoring |
| Google Search Console | Field (CrUX) | 28 gün | SEO impact |
| Chrome UX Report | Field (CrUX) | 28 gün | Benchmark |
Strateji: Development'ta Lighthouse, production'da Vercel Analytics + Search Console. CrUX data 28 gün gecikir → sabır.
5.3 Neden Lab Data ile Field Data Arasındaki Uçurum Bu Kadar Büyük?
Bu, danışmanlık süreçlerinde defalarca gördüğüm bir şok anıdır: Geliştirici kendi makinesinde Lighthouse 100/100 alır, ama Search Console'da aynı sayfa "Kötü" olarak görünür. Bu tutarsızlığın birden fazla kaynağı vardır.
Birincisi, ağ koşulları. Lighthouse, varsayılan olarak belirli bir ağ kısıtlama profili uygular; ancak bu profil gerçek bir 4G bağlantısının varyasyonunu simüle edemez. Gerçek kullanıcıların bir kısmı zayıf kapsama alanında, bir kısmı eski cihazlarda, bir kısmı ise kalabalık bir ağda geziniyor.
İkincisi, önbellekleme. Lighthouse testi önbellek olmadan (cold load) çalışır. Ancak CrUX verisini oluşturan gerçek kullanıcıların çoğu sayfanızı ikinci, üçüncü kez ziyaret ediyor ve bazı kaynaklar önbellekten geliyor. Bu durum hem LCP'yi hem de TBT'yi önemli ölçüde etkiler.
Üçüncüsü, cihaz çeşitliliği. Türkiye'deki internet kullanıcılarının önemli bir kısmı orta segment Android cihazlar kullanıyor. Bu cihazların CPU performansı, bir geliştirici MacBook Pro'sundan çok daha düşüktür. Lighthouse'un "6x CPU slowdown" seçeneği bu farkı kısmen simüle eder, ama lab ortamında test etmek her zaman daha az acı verir. CrUX ise gerçek cihazları, gerçek ağları ve gerçek davranışları ölçer.
Bu yüzden tek doğru strateji, her iki veri kaynağını birlikte takip etmektir: Lighthouse geliştirme döngüsünde hızlı geri bildirim verir; CrUX ise gerçek kullanıcı deneyiminin gerçek ölçütüdür.
6. Gerçek Dünya Örneği: E-ticaret Sitesi CLS Optimizasyonu
Önce Metrikleri (PageSpeed Insights):
- LCP: 4.2s (Kırmızı)
- CLS: 0.31 (Kırmızı)
- INP: 340ms (Kırmızı)
Sorunlar:
- Product grid images lazy load ancak width/height yok → CLS 0.18
- Google Fonts CDN link (font-display: swap) → CLS 0.08
- Framer Motion product card animations → CLS 0.05
- Dynamic "Son görülenler" carousel → CLS 0.02 (hydration shift)
Çözümler + Sonuç:
// 1. Image optimization
<Image
src={product.imageUrl}
alt={product.name}
width={400}
height={400}
sizes="(max-width: 640px) 50vw, (max-width: 1024px) 33vw, 25vw"
/>
// CLS: 0.31 → 0.13 (-0.18)
// 2. next/font/google
import { Inter } from 'next/font/google'
const inter = Inter({ subsets: ['latin'] })
// CLS: 0.13 → 0.05 (-0.08)
// 3. Framer Motion → CSS animations
// CLS: 0.05 → 0.02 (-0.03)
// 4. Suspense ile carousel
<Suspense fallback={<CarouselSkeleton />}>
<RecentlyViewed />
</Suspense>
// CLS: 0.02 → 0.000 (-0.02)
Sonra Metrikleri:
- LCP: 1.4s ✅ (Yeşil, -67%)
- CLS: 0.000 ✅ (Yeşil, -100%)
- INP: 110ms ✅ (Yeşil, -68%)
Süre: 3 hafta (1 senior developer), Maliyet: ~18.000 TL, SEO Impact: Search Console'da 6 hafta sonra "İyi" statüsü, organik trafik +28%.
İkinci Vaka: İçerik Odaklı Haber Sitesi
E-ticaret dışında da CLS sorunları ciddi şekilde yaşanıyor. İçerik yoğun sitelerde, özellikle haber ve blog platformlarında, sorunların kaynağı biraz farklıdır. Örnek bir senaryo üzerinden gidelim: çok ziyaretçili tipik bir Türkçe haber sitesi, CLS puanı 0.25 seviyesinde olsun. Sorunun büyük kısmı, görselsiz makalelerde içeriğin reklam birimlerinin yüklenmesiyle aşağı itilmesinden kaynaklanıyordu. Diğer önemli kaynak ise "Önerilen Haberler" bölümünün JavaScript ile oluşturulup DOM'a sonradan eklenmesiydi; bu bölüm yüklendiğinde mevcut içerik aşağı kayıyordu.
Çözüm iki aşamada geldi. Reklam birimlerine sabit yükseklik atandı ve konteynır küçülmemesi garanti edildi. "Önerilen Haberler" bölümü Server Component'e taşındı; bu sayede HTML doğrudan sunucu tarafından render edildi ve sonradan DOM mutasyonu yaşanmadı. Sonuç: CLS 0.25'ten 0.03'e indi ve CrUX verisi 5 hafta içinde "İyi" kategorisine geçti. Bu süreçte organik arama trafiği belirgin biçimde arttı.
7. Sık Yapılan Hatalar ve Çözümleri
Hata 1: "Lighthouse 100 ama Search Console kötü"
Sebep: Lighthouse throttled simülasyon, Search Console gerçek kullanıcı (3G, eski cihaz).
Çözüm: Chrome DevTools → Network "Slow 3G", CPU 6x slowdown test.
Hata 2: "next/image kullanıyorum ama CLS var"
Sebep: fill layout kullanılmış ancak parent container'da position: relative + aspect-ratio yok.
Çözüm:
<div className="relative aspect-video">
<Image src={url} alt="" fill className="object-cover" />
</div>
Hata 3: "Font preload ettim ama FOUT görünüyor"
Sebep: font-display: swap hâlâ aktif.
Çözüm: next/font kullan, display parametresi verme.
Hata 4: "Skeleton loading kullanıyorum ama shift var"
Sebep: Skeleton boyutu gerçek content'ten farklı.
Çözüm: aspect-ratio, min-h-[200px] gibi sabit değerlerle eşleştir.
Hata 5: "Sayfa açılışta CLS yok ama scroll sonrası var"
Bu, CLS ölçümünde sıkça karıştırılan bir nüanstır. Google'ın CLS hesaplama algoritması, yalnızca sayfa yüklenirken gerçekleşen shift'leri değil, kullanıcının sayfayla etkileşime girmediği dört buçuk saniyelik pencereler içindeki shift'leri de toplar. Scroll yaparken bir reklam birimi yüklenirse ve sayfa kayarsa, bu da CLS'ye eklenir. Çözüm, lazy load edilen içerikler için de yeterli alan rezervasyonu yapmaktır. IntersectionObserver kullanarak içerik görünür alana girerken değil, görünür alana girmeden hemen önce yüklenecek şekilde ayarlamak, bu shift'leri önlemenin pratik bir yöntemdir.
Hata 6: "Dynamic import kullandım ama CLS kötüleşti"
dynamic() ile lazy yüklenen bir bileşen, ssr: false seçeneğiyle birlikte kullanıldığında sunucu tarafında render edilmez ve yalnızca client-side hydration sırasında görünür hale gelir. Bu da ilk yüklemede o alanın boş görünmesi, sonra bileşenin eklenmesiyle içeriğin kayması anlamına gelir. Çözüm ya o bileşen için mutlaka bir skeleton/placeholder kullanmak ya da gerçekten ssr kapatmayı gerektirmeyen bileşenler için ssr'ı açık bırakmaktır. ssr: false yalnızca window, document veya tarayıcıya özgü API kullanan bileşenler için zorunludur.
8. Next.js 15 Yenilikleri: Turbopack ve Partial Prerendering
8.1 Turbopack (Rust-based bundler)
- Development: Webpack'ten %10 daha hızlı (soğuk start)
- Production: Henüz stable değil (deneysel)
- CLS Impact: Yok (build tool, runtime değil)
8.2 Partial Prerendering (PPR)
// app/product/[id]/page.tsx
export const experimental_ppr = true
export default function ProductPage({ params }) {
return (
<>
{/* Static shell */}
<ProductHeader />
{/* Dynamic hole */}
<Suspense fallback={<PriceSkeleton />}>
<ProductPrice id={params.id} /> {/* Async */}
</Suspense>
</>
)
}
Kazanç: Static shell anında render (TTFB <100ms), dynamic content streaming. CLS risk azalır çünkü skeleton built-in.
8.3 PPR'ın Mimariye Etkisi: Daha Derin Bakış
Partial Prerendering, yalnızca bir performans hilesi değil; temelde sayfaları nasıl tasarladığımızı değiştiren bir mimari paradigmadır. Klasik yaklaşımda bir sayfa ya tamamen statik ya da tamamen dinamiktir. SSG ile oluşturulan sayfalar hızlı ama güncel olmayabilir; SSR ile oluşturulanlar ise güncel ama TTFB'si daha yüksektir. PPR bu ikilikten çıkış yolu sunar: bir sayfanın değişmeyen çerçevesi (başlık, navigasyon, sayfa yapısı) statik olarak üretilip CDN'den anında sunulur; değişken içerik bölümleri ise bağımsız akışlarla gelir.
Bu yaklaşım, CLS açısından üç önemli avantaj sağlar. Birincisi, sayfanın iskelet yapısı anında yerini alır ve hiçbir zaman kaymaz. İkincisi, dinamik bölümler için Suspense fallback'leri zorunlu hale gelir; bu da geliştiricileri skeleton tasarlamaya mecbur bırakır. Üçüncüsü, sayfanın görünür alanındaki statik içerik LCP hesabına katıldığı için LCP değeri de önemli ölçüde iyileşir.
PPR'ı production'a almadan önce dikkat edilmesi gereken bir husus var: dinamik içeriğin sayfa çerçevesine ne ölçüde bağımlı olduğunu belirlemek gerekir. Eğer bir bileşen başka bir bileşenin verilerine ihtiyaç duyuyorsa ve bu bağımlılık server tarafındaki render sıralamasını etkiliyorsa, PPR bu durumu daha da karmaşık hale getirebilir. Basit, bağımsız dinamik bölümler için PPR mükemmel bir seçimdir; derin bağımlılık zincirlerine sahip sayfalar için önce bağımlılıkları çözümlemek gerekir.
8.4 PPR Mimarisinde Dynamic I/O ve Dev-Time Optimizasyonları
Next.js 15 ile gelen deneysel Dynamic I/O mimarisi, Partial Prerendering yapısını bir adım öteye taşıyarak build aşamasındaki dinamik veri çağrılarının takibini tamamen otomatize eder. Geliştirici olarak hangi fonksiyonun dinamik (örneğin çerez okuma, arama parametrelerine erişim veya doğrudan veri tabanı sorguları) hangisinin statik olduğunu manuel olarak belirtmek zorunda kalmazsınız. Dynamic I/O, kodun derleme (compilation) aşamasında veri yollarını analiz eder ve statik olarak önbelleğe alınabilecek alanlar ile akış sırasında sunucu tarafından anlık olarak işlenmesi gereken dinamik boşlukları (holes) otomatik olarak ayrıştırır.
Bu mimarinin en büyük kazanımı, geliştirme (dev-time) aşamasında ortaya çıkan performans regresyonu uyarılarıdır. Eğer statik bir kabuk içinde yanlışlıkla dinamik bir veri çağrısı yapılırsa ve bu çağrı bir Suspense sınırıyla sarmalanmamışsa, Turbopack geliştirici konsoluna anında uyarılar basarak potansiyel bir LCP veya CLS riskini henüz kod production ortamına gitmeden haber verir. Bu sayede, büyük geliştirici ekiplerinde bile Core Web Vitals standartlarının korunması ve yanlış mimari kararların önlenmesi otomatik bir mekanizmaya bağlanmış olur.
Ancak, Dynamic I/O kullanımının getirdiği bazı trade-off'lar da mevcuttur. Derleme zamanı analizleri, build süresini (build time) projenin büyüklüğüne bağlı olarak %5-10 oranında artırabilir. Ayrıca, tüm veri yollarının strict (katı) kurallara tabi olması, eski kütüphanelerle veya asenkron olmayan global state yönetim araçlarıyla çalışırken derleme hatalarına yol açabilir. Bu nedenle, mevcut projeleri Next.js 15'e geçirirken Dynamic I/O özelliğini kademeli olarak devreye almak ve özellikle eski API çağrılarının asenkron (async/await) yapıya uyarlandığından emin olmak kritik bir ön adımdır.
9. INP Optimizasyonu: Etkileşim Gecikmesini Azaltmak
Core Web Vitals konuşmalarında CLS ve LCP en fazla dikkat çekse de INP (Interaction to Next Paint), 2024'ten bu yana eşit derecede önemli bir ölçüt haline geldi ve birçok Next.js projesinde hâlâ göz ardı ediliyor. INP, kullanıcının bir butona tıklaması, bir metin alanına yazması ya da bir dropdown'ı açması gibi etkileşimlerin kaç milisaniye içinde görsel yanıt ürettiğini ölçer.
9.1 Long Task'ları Parçalamak
INP sorunlarının en yaygın kaynağı, JavaScript main thread'ini uzun süre bloke eden işlemlerdir. React'te bu genellikle büyük bir state güncellemesinin çok fazla bileşeni yeniden render ettirmesiyle ortaya çıkar. Next.js ve React 19 kombinasyonunda useTransition hook'u bu sorunu çözmek için birincil araçtır.
'use client'
import { useState, useTransition } from 'react'
export function SearchResults() {
const [results, setResults] = useState([])
const [isPending, startTransition] = useTransition()
function handleSearch(query: string) {
startTransition(() => {
// Ağır hesaplama veya büyük state güncellemesi
// "Urgent" olmayan işlem olarak işaretlenir
setResults(computeResults(query))
})
}
return (
<>
<input onChange={(e) => handleSearch(e.target.value)} />
{isPending ? <Spinner /> : <ResultList items={results} />}
</>
)
}
startTransition ile işaretlenen güncellemeler, kullanıcının anlık etkileşimlerine (klavye girişi gibi) öncelik tanır ve ağır render'ları geciktirir. Bu, INP'yi doğrudan düşürür çünkü input'a yazma eylemi anında yanıt alır.
9.2 Client-Side JavaScript Bütçesi
Bir sayfanın INP'sini etkileyen faktörlerin başında, o sayfanın toplam JavaScript yükü gelir. Client Component ağırlığı arttıkça hydration süresi uzar, hydration süresi uzadıkça erken etkileşim anlarındaki yanıt gecikir. Next.js App Router'ın Server Components varsayılanı tam da bu sorunu ele alıyor: sunucu tarafında render edilen her bileşen, tarayıcıya gönderilen JavaScript bundle'ını küçültür.
Bu ilkeden pratik bir kural çıkarmak mümkündür: yeni bir bileşen oluştururken ilk sorunuz "Bu bir Client Component olmak zorunda mı?" olmalıdır. useState, useEffect, browser API erişimi veya olay dinleyicisi yoksa cevap hayırdır. Statik olan her şeyi Server Component olarak tutmak, JavaScript bütçesini kontrol altında tutmanın en güvenilir yoludur.
9.3 React 19 ActionState ve Form Pending Durumlarında Layout Koruma
React 19 ile birlikte hayatımıza giren ve form yönetimini tamamen değiştiren useActionState (eski adıyla useFormState) ve useFormStatus hook'ları, asenkron form işlemleri sırasında kullanıcı etkileşimini ve sayfa düzeninin kararlılığını korumak için mükemmel çözümler sunar. Geleneksel React uygulamalarında bir form gönderildiğinde, loading durumunun yönetimi genellikle manuel state'lerle yapılır ve bu durum form elemanlarının veya butonların boyutlarının değişmesine yol açarak küçük ama can sıkıcı yerleşim kaymaları yaratır.
'use client'
import { useActionState } from 'react'
import { submitOrder } from '@/actions/order'
export function OrderForm() {
const [state, formAction, isPending] = useActionState(submitOrder, null)
return (
<form action={formAction} className="flex flex-col gap-4">
<input name="address" required className="border p-2 rounded" />
{/* Pending state sırasında buton boyutu sabit tutulmalı */}
<button
disabled={isPending}
className="h-12 w-full bg-blue-600 text-white rounded flex items-center justify-center relative"
>
{isPending ? (
<span className="absolute inset-0 flex items-center justify-center">
<Spinner className="h-5 w-5 animate-spin" />
</span>
) : (
<span>Siparişi Tamamla</span>
)}
</button>
{/* Hata veya başarı mesajı için önceden rezerve edilmiş alan */}
<div className="min-h-[24px] text-sm text-red-500">
{state?.error && <span>{state.error}</span>}
</div>
</form>
)
}
Bu kod bloğunda görüldüğü gibi, butonun yüksekliği (h-12) sabitlenmiş ve yüklenme simgesi (spinner) absolute olarak konumlandırılmıştır. Bu sayede, form gönderim işlemi başladığında buton içindeki metin kaybolsa bile butonun genel boyutları değişmez ve butonun altında yer alan diğer sayfa öğeleri milisaniyelik yer değiştirmelere maruz kalmaz. Benzer şekilde, form yanıtına göre görüntülenecek olan hata veya başarı mesajları için min-h-[24px] gibi minimum yükseklik değeri atanmış boş bir konteynır hazır bulundurulur. Bu önlem, asenkron sunucu yanıtı (Server Action) tamamlanıp DOM'a hata mesajı eklendiğinde sayfa düzeninin aniden aşağı kaymasını (CLS) önler ve INP optimizasyonuyla birleştiğinde kullanıcılara son derece akıcı ve kesintisiz bir deneyim sunar.
10. Türkçe Karakter Desteği ve CLS: Sıkça Gözden Kaçan Detay
Türkçe web projeleri geliştirirken ortaya çıkan özgün bir sorun var: ğ, ü, ş, ı, ö, ç harfleri bazı font alt kümelerinde yer almaz. Bir font yüklendiğinde bu karakterler için fallback sistem fontu kullanılırsa, hem görsel tutarsızlık hem de ölçülebilir bir CLS meydana gelir. Bu özellikle ağ bağlantısının yavaş olduğu durumlarda belirginleşir: Türkçe metin önce sistem fontuyla görünür, ardından yüklenen web fontu devreye girer ve layout yeniden hesaplanır.
Çözümün iki boyutu vardır. Font tarafında, latin-ext alt kümesini eklemek Türkçe karakterleri garantiler. next/font/google ile bu şöyle yapılır:
const inter = Inter({
subsets: ['latin', 'latin-ext'],
variable: '--font-inter',
})
CSS tarafında ise font-size-adjust özelliği, fallback font ile web font arasındaki boyut farkını azaltır. Bu, font swap sırasındaki layout değişimini minimize eder. Browser desteği 2025 itibarıyla çok iyileşmiş durumda ve Next.js 15, next/font ile bu ayarı otomatik olarak yapmaya yakın bir noktada. Ancak manuel kontrol hâlâ güvenilir bir alternatiftir.
11. Sayfa Bazlı Optimizasyon Stratejisi: Nereden Başlamalısınız?
Her şeyi aynı anda optimize etmeye çalışmak gerçekçi değildir. Özellikle mevcut bir projeyi Next.js 15'e geçiriyorsanız veya yüzlerce sayfadan oluşan bir site üzerinde çalışıyorsanız, nereden başlayacağınızı belirlemek stratejik bir karardır.
Önceliği belirlemek için Google Search Console'da "Core Web Vitals" raporuna bakın ve "Kötü" URL'leri trafik hacmine göre sıralayın. Günde yüzlerce veya binlerce kullanıcı alan sayfalar, günde onlarca kullanıcı alan sayfalardan çok daha yüksek bir iyileştirme etkisi doğurur. CrUX verisi sayfa bazlı olduğu için hangi sayfaların gerçekten kötü performans gösterdiğini kesin olarak belirleyebilirsiniz.
Belirli sayfalarda bir Pattern varsa — örneğin ürün listesi sayfalarının hepsinde CLS yüksekse — sorunu tüm sayfalarda aynı anda çözen bir altyapı değişikliği yapmak, tek tek sayfa optimize etmekten çok daha verimlidir. ProductGrid bileşeni bir kez doğru şekilde yazılırsa, onu kullanan tüm sayfalar hemen düzelir.
Son olarak, iyileştirmeleri ölçmek için sabit bir referans noktası oluşturun. Lighthouse ile aynı koşullarda (aynı cihaz, aynı ağ, aynı browser) ölçüm yaparak önce-sonra karşılaştırması yapın. CrUX verisi için ise iyileştirme tarihini not alın ve 5-6 hafta sonra Search Console'u tekrar kontrol edin. Bu disiplin olmadan, nelerin işe yarayıp nelerin yaramadığını asla kesin olarak bilemezsiniz.
12. K-ONTECH Optimizasyon Süreci
Paket 1: Hızlı Audit (Ücretsiz)
- PageSpeed Insights rapor analizi
- Top 5 CLS/LCP sorunu tespit
- Tahmini iyileştirme süresi
- Süre: 1-2 gün
- Fiyat: Ücretsiz
Paket 2: Orta Ölçek Optimizasyon
- 10-15 sayfa optimizasyonu
- Image/Font optimization
- Suspense boundaries
- Lighthouse CI kurulumu
- Süre: 4-6 hafta
- Fiyat: 22.000-35.000 TL
Paket 3: Enterprise Monitoring
- Tüm site optimizasyonu
- Vercel Analytics entegrasyonu
- 6 ay sürekli monitoring
- Aylık performans raporu
- Süre: 8-12 hafta + 6 ay destek
- Fiyat: 65.000-120.000 TL
13. Migration: Pages Router'dan App Router'a Geçiş Planı
Mevcut bir Pages Router projesi varsa ve App Router'ın getirdiği performans avantajlarından yararlanmak istiyorsanız, geçiş sürecini dikkatlice planlamak gerekir. Next.js, her iki router'ın aynı projede birlikte çalışabildiği kademeli geçişe olanak tanır; yani tüm kodu bir anda yeniden yazmanız gerekmez.
Kademeli geçiş stratejisi şöyle işler: app/ dizinini projeye ekleyip sonraki mantığı taşımak istediğiniz route'ları birer birer buraya geçirirsiniz. pages/ dizininde kalan route'lar çalışmaya devam eder. Bu yaklaşım, büyük projelerde riskleri minimize eder ve her route geçişinin ardından metrikleri kontrol etme imkânı sunar.
Geçiş sürecinde CLS açısından kritik bir adım, getServerSideProps ve getStaticProps kullanımının Server Components'e dönüştürülmesidir. Bu dönüşümde dikkat edilmesi gereken husus, mevcut veri çekme mantığının async Server Component içine taşınması ve ilgili Suspense boundary'lerin eklenmesidir. Eski _app.tsx yapısındaki global state yönetiminin de gözden geçirilmesi gerekir; Context API ve global state provider'lar 'use client' direktifiyle Client Component olarak tutulabilir, ancak bu bileşenlerin ağırlığı minimize edilmelidir.
Bir uyarı: Pages Router'dan App Router'a geçiş, yapılan testle ve olgunlaşmış araç zincirleriyle bile orta ölçekli projeler için 80-120 saatlik bir çalışma gerektirebilir. Bu yatırımın getirisini önceden hesaplamak; mevcut CWV puanları, site büyüklüğü ve trafik hacmi göz önüne alındığında, hangi projelerde anlamlı bir iyileşme sağlayacağını belirlemek önemlidir.
14. Caching Stratejisi ve Core Web Vitals İlişkisi
TTFB (Time to First Byte), Core Web Vitals tablosunda yer alıyor ancak çoğu performans tartışmasında yeterince yer bulmuyor. TTFB, LCP'nin en büyük belirleyicilerinden biridir; sunucu yanıt gecikmesi ne kadar yüksekse, tarayıcının ilk boyayı (First Paint) başlatması o kadar geç olur ve bu zincir LCP'ye doğrudan yansır.
Next.js 15 App Router bu konuda iki farklı önbellek katmanı sunar: sunucu tarafında fetch önbelleği ve React'in Server Component önbelleği. Bunları doğru yapılandırmak, TTFB'yi onlarca milisaniyeden yüzlerce milisaniyeye kadar düşürebilir. Ancak dikkat edilmesi gereken bir nüans var: aşırı agresif önbellekleme, kullanıcıların güncel olmayan veri görmesine yol açabilir. Bu nedenle önbellek stratejisi, verinin ne kadar sık değiştiğine göre calibre edilmelidir.
Statik sayfalar için generateStaticParams ile tam ISR (Incremental Static Regeneration) ve revalidate süresi, hem TTFB hem de LCP'yi dramatik şekilde iyileştirir. Örneğin sık değişmeyen bir blog makalesi için revalidate: 3600 (bir saat) ayarı, CDN'den anında servis edilmesini sağlar. Dinamik sayfalar için ise Data Cache ve Full Route Cache mekanizmalarını anlamak gerekir. Next.js 15'in veri önbelleği varsayılan olarak devre dışıdır; bu, önceki sürümlere göre önemli bir değişiklik olup dikkatli bir geçiş gerektirir.
CDN katmanında Vercel Edge Network veya Cloudflare kullanıyorsanız, cache-control header'larını yönetmek TTFB'nin 200ms'nin altına inmesi için kritiktir. Türkiye'deki kullanıcılar için Cloudflare'in Frankfurt veya Amsterdam PoP'larından servis edilmek, Istanbul'daki bir sunucudan servis edilmekle hemen hemen aynı gecikmeyi sağlar; ancak İzmir veya Ankara gibi şehirlerdeki kullanıcılar için Frankfurt CDN, Türkiye'deki zayıf bant genişliğine sahip bir sunucudan daha iyi performans sunabilir. Bu pratik detay, TTFB ve dolayısıyla LCP üzerinde çok gerçek bir etki yaratır.
Sunucu yanıt süresinin kararsız olduğu ve dinamik veri tabanı sorgularına bağımlı olan karmaşık sayfalarda, Stale-While-Revalidate (SWR) yaklaşımı en güvenilir çözümdür. SWR sayesinde CDN, kullanıcıya anında (milisaniyeler içinde) önbellekteki eski içeriği sunarken, arka planda sunucuya giderek içeriğin yeni halini çeker ve önbelleği sessizce günceller. Bu mimari, tarayıcının sunucu yanıtını (TTFB) bekleme süresini neredeyse sıfıra indirerek LCP puanını mükemmel seviyede tutar. Ayrıca CDN katmanında kullanılan etiket tabanlı önbellek temizleme (tag-based cache invalidation) yöntemleri, veri tabanında bir güncelleme yapıldığında sadece o veriyi kullanan sayfaların önbelleğini anında geçersiz kılarak veri tutarlılığını sağlar ve genel sistem performansını korur.
15. Next.js 15'te React 19 Entegrasyonu ve Performans Kazanımları
React 19, Next.js 15 ile birlikte resmi stable sürümüne ulaştı ve bu entegrasyonun Core Web Vitals üzerindeki etkisi doğrudan hissediliyor. Bunların başında Actions API ve use hook geliyor.
use hook, bir Promise'i Server Component'ten Client Component'e geçirmeyi mümkün kılıyor. Bu, daha önce useEffect + useState kombinasyonuyla yapılan asenkron veri alma işlemlerini çok daha zarif bir şekilde ele alıyor. Pratikte bu şu anlama geliyor: daha önce istemci tarafında gerçekleşen ve hydration süresini uzatan bazı veri yükleme işlemleri artık sunucu tarafında başlatılıp akış olarak gönderilebilir. Bu hem INP hem de algılanan hız açısından olumlu bir gelişmedir.
React 19'un compiler özelliği (React Compiler, önceki adıyla React Forget), component'lerin otomatik memoization'ını sağlıyor. Geliştiricilerin useMemo, useCallback eklemediği durumlarda bile gereksiz yeniden render'ları önlüyor. Bu, INP optimizasyonu açısından önemlidir; çünkü etkileşim sırasında yüksek sayıda bileşenin yeniden render edilmesi main thread'i bloke eder. React Compiler Next.js 15 ile deneysel olarak kullanılabilir ve özellikle büyük component ağaçlarında INP üzerinde ölçülebilir iyileştirme sağlar.
Server Actions ise form gönderimi ve kullanıcı mutasyonları için istemci tarafı JavaScript'i ortadan kaldırıyor. Bir ürünü sepete ekleme, bir form gönderme gibi işlemler artık ayrı bir API route'u olmadan doğrudan sunucu fonksiyonları olarak tanımlanabiliyor. Bu hem bundle boyutunu küçültüyor hem de hidrasyon sürecini hızlandırıyor; ikisi de INP'yi doğrudan etkiler.
16. Performans Bütçesi: Takım Genelinde Sürdürülebilirlik
Teknik bilgi bireysel geliştiricinin elinde parlıyor ancak takım ortamında kaybolabiliyor. Tek bir geliştirici Lighthouse 100/100 alan bir bileşen yazabilir; başka bir geliştirici ertesi gün büyük bir animasyon kütüphanesi ekleyerek tüm kazanımı silebilir. Bu sorunu çözmenin tek yolu, performansı kod review sürecinin ayrılmaz parçası haline getirmektir.
Bunun pratik yolu bir "performans bütçesi" tanımlamaktır. Bu bütçe, projenin tolere ettiği maksimum JavaScript bundle boyutunu, maksimum CLS değerini ve kabul edilebilir LCP eşiğini belgelenmiş bir kural olarak kayıt altına alır. Lighthouse CI bu bütçeyi PR seviyesinde otomatik uygular. Ancak Lighthouse CI tek başına yeterli değildir; çünkü yalnızca belirli URL'leri test eder. Yeni eklenen bir bileşen başka bir sayfada CLS yaratabilir.
Bunu önlemek için storybook veya component playground ortamında her yeni bileşenin izole olarak test edilmesi iyi bir pratiktir. Özellikle animasyon içeren veya asenkron veri yükleyen bileşenler için "CLS test" adımını component geliştiricisinin kendi sorumluluğuna koymak, sorunları erken aşamada yakalar. Türkiye'deki birçok ajans ve yazılım şirketinde bu kültürün henüz tam oturmadığını gözlemliyorum. Performansı yalnızca "SEO için yapılan bir şey" olarak görmek yerine, kullanıcı deneyiminin mühendislik boyutu olarak çerçevelemek, bu kültür değişimini kolaylaştırır.
17. Cloudflare R2 ve Görsel Dağıtımı: AVIF ve WebP Dönüşüm Altyapısı
Web sitelerindeki görsellerin yönetimi ve kullanıcılara en hızlı şekilde ulaştırılması, LCP (Largest Contentful Paint) ve CLS (Cumulative Layout Shift) metriklerinin kontrol altında tutulmasında kritik bir rol oynar. K-ONTECH olarak, kurumsal projelerde medya dosyalarının depolanması için Amazon S3 uyumlu ve Vercel/Next.js projeleriyle sıfır çıkış ücreti (egress fees) sunan Cloudflare R2 depolama alanını tercih ediyoruz. Cloudflare R2, yüksek bant genişliğine sahip küresel altyapısı sayesinde medya dosyalarını milisaniyeler bazında sunucuya ve son kullanıcıya iletme yeteneğine sahiptir.
Depolanan görsellerin sadece hızlı sunulması yetmez, aynı zamanda modern sıkıştırma formatları olan AVIF ve WebP formatlarına dinamik olarak dönüştürülmesi gerekir. AVIF formatı, WebP formatına kıyasla aynı görsel kalitesinde %30'a varan dosya boyutu tasarrufu sağlar. Bu tasarruf, özellikle mobil cihazlardaki sayfa yüklenme sürelerini (LCP) doğrudan düşürür. Bu amaçla, Cloudflare R2 deposunun önüne yerleştirilen bir Cloudflare Worker betiği, tarayıcının gönderdiği Accept HTTP başlığını analiz eder. Eğer kullanıcının tarayıcısı AVIF formatını destekliyorsa, Worker görseli arka planda dinamik olarak AVIF formatına çevirir ve sıkıştırarak teslim eder; aksi durumlarda ise sırasıyla WebP veya orijinal format olan JPEG/PNG sürümlerine fallback yapar.
graph TD
A["Kullanıcı Tarayıcısı (Request)"] --> B["Cloudflare CDN & Worker"]
B -->|"Destek Var"| C["R2'den Görseli Oku ve AVIF'e Dönüştür"]
B -->|"Destek Yok"| D["R2'den Görseli Oku ve WebP'ye Dönüştür"]
C --> E["Önbelleğe Al ve Kullanıcıya Sun"]
D --> E
Dinamik görsel dönüştürme ve boyutlandırma süreçlerinde dikkat edilmesi gereken en önemli unsur, görselin en boy oranının (aspect-ratio) korunmasıdır. Cloudflare Worker veya Next.js'in yerleşik görsel optimize edicisi (next/image loader) görseli yeniden boyutlandırırken, orijinal görselin oranları bozulursa, tarayıcı tarafında görselin render edilmesi sırasında beklenmeyen dikey veya yatay yerleşim kaymaları (CLS) tetiklenir. Bu sorunu önlemek için, görseller sunucuya yüklenirken en boy oranları metadata olarak kaydedilmeli ve next/image bileşenine bu oranlara sadık kalınarak width ve height değerleri veya sabit aspect-ratio tanımlı kapsayıcı container'lar verilmelidir. Bu sayede, görsel hangi formatta veya çözünürlükte indirilirse indirilsin, tarayıcı ekranında kaplayacağı alan en baştan milimetrik olarak rezerve edilir ve sıfır CLS hedefine tam uyum sağlanır.
18. CSS-in-JS ve CSS Modules: Performans ve Hydration Shift Analizi
Next.js 15 App Router mimarisinin getirdiği en büyük paradigma değişimlerinden biri, varsayılan olarak Server Components kullanımıdır. Bu durum, uzun yıllardır React ekosisteminde popüler olan ve çalışma zamanında (runtime) stil enjekte eden CSS-in-JS kütüphanelerinin (styled-components, Emotion vb.) kullanımını ciddi şekilde sorgulamaya açmıştır. Çalışma zamanı CSS-in-JS çözümleri, stilleri oluşturmak için JavaScript kodunun tarayıcıda çalıştırılmasını beklemek zorundadır. Bu da sunucudan gelen HTML içeriğinin, JavaScript indirilip hidrasyon (hydration) tamamlanana kadar stilsiz kalmasına veya geçici stillerle render edilmesine yol açarak ciddi FOUC (Flash of Unstyled Content) ve CLS sorunlarına neden olur.
Ayrıca, runtime CSS-in-JS kütüphaneleri React'in aşamalı HTML akışını (progressive HTML streaming) engeller. Sunucu, bileşenlerin stillerini önceden kestiremediği için tüm CSS kurallarını oluşturana kadar HTML akışını duraklatır; bu da Time to First Byte (TTFB) metriğinin uzamasına ve LCP'nin kırmızı bölgeye kaymasına yol açar. Buna karşılık, derleme zamanında (compile-time) statik CSS üreten CSS Modules veya Tailwind CSS, stil dosyalarının HTML ile birlikte asenkron olarak ve tarayıcıyı bloke etmeden indirilmesini sağlar. Tarayıcı, henüz JavaScript kodları inmeden CSS'i uygulayabildiği için sayfa açılışı tamamen kararlı ve yerleşim kaymalarından arındırılmış olur.
graph LR
A["HTML Stream"] -->|"CSS Modules (Statik)"| B["Anında Render (Sıfır CLS)"]
A -->|"Runtime CSS-in-JS"| C["JS Yüklenmesini Bekleme"]
C -->|"Hydration"| D["Stil Uygulanması (Yüksek CLS Riski)"]
Mevcut büyük kurumsal projeleri Next.js 15'e geçirirken, stil altyapısının modernizasyonu ilk öncelik olmalıdır. Eğer projede styled-components kullanımı kaçınılmaz ise, Next.js'in sunduğu Server Style Sheets entegrasyonu (next.config.js içinde compiler: { styledComponents: true }) etkinleştirilerek en azından sunucu tarafında stillerin HTML'e inline olarak gömülmesi sağlanmalıdır. Ancak kalıcı performans, sıfır CLS ve 100/100 Lighthouse skoru için nihai hedef, çalışma zamanı stil kütüphanelerini tamamen terk ederek CSS Modules, Tailwind CSS veya Vanilla Extract gibi statik, zero-runtime CSS çözümlerine geçiş yapmak olmalıdır.
19. Google PageSpeed Insights Yapay Zekâ Çağında AEO ve GEO için Performans
Web sitelerinin performansı, yalnızca arama motoru sonuç sayfalarındaki (SERP) klasik SEO sıralamalarını etkilemekle kalmaz; 2026 dijital ekosisteminde Yapay Zekâ Arama Optimizasyonu (AEO - Answer Engine Optimization) ve Üretken Arama Optimizasyonu (GEO - Generative Engine Optimization) üzerinde de doğrudan belirleyici bir rol oynar. Perplexity, Google Gemini Search, OpenAI Search ve Microsoft Copilot gibi modern yapay zekâ arama motorları, kullanıcılara yanıt üretirken kaynak siteleri tarar, analiz eder ve en hızlı şekilde veri çekebildikleri siteleri birincil kaynak olarak gösterip alıntı yaparlar.
Yapay zekâ crawler (örümcek) botlarının tarama bütçesi (crawl budget) ve tarama zaman aşımı limitleri (timeout limits), klasik Googlebot'a kıyasla çok daha dardır. Yavaş yüklenen, sunucu yanıt süresi (TTFB) yüksek olan veya hidrasyon süreci uzayan web sayfaları, yapay zekâ botları tarafından taranamadan es geçilir. Eğer sitenizin TTFB değeri 600ms'nin, LCP değeri ise 2.5 saniyenin üzerindeyse, yapay zekâ motorları içeriğinizi okumak için gereken süreyi aşarak alternatif ve daha hızlı kaynaklara yönelir. Bu durum, ne kadar kaliteli içerik üretirseniz üretin, yapay zekâ aramalarında sitenizin hiçbir zaman alıntılanamaması anlamına gelir.
graph TD
A["AI Arama Motoru Sorgusu"] --> B["Crawler Kaynak Siteleri Tarar"]
B -->|"Hızlı Site (LCP < 1.8s)"| C["İçeriği Okur, Analiz Eder ve Alıntılar"]
B -->|"Yavaş Site (LCP > 2.5s)"| D["Zaman Aşımı (Timeout) - Es Geçer"]
Ayrıca, semantik HTML hiyerarşisi (H1-H6 başlık düzeni), JSON-LD yapılandırılmış verileri (schema markup) ve sıfır CLS kararlılığı, yapay zekâ motorlarının içeriği anlamlandırmasını (parsing) kolaylaştırır. Sayfa yüklenirken içeriklerin kayması (CLS), botların DOM ağacını hatalı okumasına veya metin bloklarını yanlış bağlamda eşleştirmesine yol açabilir. Kararlı, hızlı ve temiz kodlanmış bir Next.js 15 mimarisi, yapay zekâ botlarının sayfa içeriğinizi saniyeden daha kısa bir sürede tam doğrulukla dizine eklemesini ve kullanıcılara sunulan yapay zekâ üretimi özetlerde en üst sırada yer almanızı sağlar.
20. Büyük Ölçekli Kurumsal Projelerde Core Web Vitals Regresyon Testleri
Büyük ölçekli ve çoklu geliştirici ekiplerinin çalıştığı kurumsal Next.js 15 projelerinde, Core Web Vitals skorlarının zaman içinde bozulmasını (performance regression) önlemek en az optimizasyon sürecinin kendisi kadar zordur. Yeni bir özelliğin yayına alınması, harici bir kütüphanenin eklenmesi veya özensiz yazılmış bir CSS satırı, günler süren optimizasyon çalışmalarını bir anda sıfırlayabilir. Bu regresyonu önlemenin tek yolu, performans testlerini CI/CD (Sürekli Entegrasyon / Sürekli Dağıtım) süreçlerinin merkezine yerleştirmektir.
K-ONTECH mimari standartlarında, her Pull Request (PR) aşamasında otomatik olarak çalışan bir regresyon kontrol boru hattı (pipeline) kurguluyoruz. Bu hattın ilk adımı, bundlewatch veya Webpack Bundle Analyzer (Turbopack projeleri için ilgili analiz araçları) yardımıyla JavaScript bundle boyutlarındaki artışı denetlemektir. Eğer yeni kod, önceden belirlenmiş olan maksimum bundle limitini (örneğin ana JS dosyası için 150KB) aşarsa, CI/CD sistemi PR sürecini otomatik olarak bloke eder ve geliştiriciye kodu optimize etmesi veya dynamic import kullanması yönünde uyarı gönderir.
İkinci ve en kritik adım ise, PR bazlı çalışan otomatik Lighthouse CI (LHCI) testleridir. LHCI, kodun geçici bir staging ortamında (preview deployment) ayağa kaldırılmasının ardından belirlenen kritik sayfaları (ana sayfa, ürün detay, blog vb.) simüle edilmiş mobil ve masaüstü ağ koşullarında test eder. .lighthouserc.json dosyasında tanımlanan assertions kuralları (örneğin cumulative-layout-shift için maksimum 0.05, largest-contentful-paint için maksimum 1800ms) katı bir şekilde uygulanır. Bu kurallardan herhangi biri ihlal edildiğinde build başarısız olur ve hataya neden olan kod production ortamına kesinlikle sızamaz. Bu sayede, Core Web Vitals hedefleri geliştirici hatalarından bağımsız olarak, kurumsal bir yazılım standardı olarak kalıcı hale getirilir.
[!IMPORTANT] Kilit Kazanım: Next.js 15 mimarisinde performans bir sonradan ekleme aracı değil, sayfa düzeninin CSS aspect-ratio ve Suspense skeleton'ları ile en baştan kararlı kılınması disiplinidir.
21. Core Web Vitals ve CDN Optimizasyonları: Edge Network ve Middleware Performans Analizi
Next.js 15 projelerinde web performansının en belirleyici unsurlarından biri, sunucu ile tarayıcı arasındaki ağ yolculuğudur. Sunucu yanıt süresi (TTFB), Largest Contentful Paint (LCP) metriğinin başlangıç noktasını oluşturduğu için, CDN (İçerik Dağıtım Ağı) ve Edge computing (uç bilgi işlem) çözümlerinin entegrasyonu hayati önem taşır. Next.js 15 mimarisinde kullanılan middleware.ts dosyası, kullanıcı isteklerini henüz sunucuya ulaşmadan, edge sunucularında (Vercel Edge Network veya Cloudflare Workers gibi) yakalayıp işlemek için tasarlanmıştır. Ancak middleware kullanımı, doğru kurgulanmadığında ciddi performans gecikmelerine ve layout shift (CLS) sorunlarına yol açabilir.
Edge üzerinde çalışan middleware kodları, coğrafi olarak kullanıcıya en yakın lokasyonda çalıştırıldığı için yönlendirme (redirect) ve yetkilendirme (authorization) işlemlerini milisaniyeler düzeyinde gerçekleştirebilir. Örneğin, bir kullanıcının dil tercihine göre / adresinden /tr adresine yönlendirilmesi geleneksel sunucularda 300-500ms sürerken, Edge Middleware ile bu süre 10-20ms arasına iner. İstemci tarafında (client-side) yapılan yönlendirmeler ise tarayıcının önce boş bir HTML yüklemesine, ardından JavaScript kodunu çalıştırıp yönlendirme kararını vermesine neden olur. Bu süreç, kullanıcının ekranda geçici bir içerik kayması (CLS) veya beyaz ekran görmesine yol açar. Edge Network üzerinde konumlandırılan yönlendirme mekanizmaları, sunucu yanıt süresini (TTFB) düşürerek tarayıcının LCP hedefine daha erken ulaşmasını sağlar ve istemci tarafındaki yönlendirmelerin neden olduğu yerleşim kaymalarını (CLS) tamamen engeller.
Ancak Edge Middleware'in getirdiği en büyük trade-off, buradaki işlem bütçesidir. Middleware içinde yapılan her asenkron veri tabanı sorgusu veya harici API çağrısı, edge sunucusunun yanıt vermesini geciktirir ve doğrudan TTFB süresine eklenir. Eğer middleware içerisinde kullanıcı oturumunu doğrulamak için uzak bir veri tabanına sorgu atıyorsanız, edge sunucusu bu sorgunun yanıtını beklerken tarayıcıya HTML akışını başlatamaz. Bu durum, LCP metriğini doğrudan kırmızı bölgeye taşır. K-ONTECH mühendislik standartlarında, edge middleware içinde ağır veri tabanı sorgularından kaçınarak oturum doğrulamalarını şifrelenmiş JWT token'ları üzerinden ve tamamen bellek (in-memory) doğrulamalarıyla gerçekleştiriyoruz. Böylece sunucu yanıt süresini 150ms'nin altında tutarak hem mükemmel bir TTFB elde ediyoruz hem de istemci tarafında yönlendirmelerden kaynaklanan CLS risklerini ortadan kaldırıyoruz.
22. React Server Action Hataları ve CLS: Form Doğrulama Mesajlarının Kararlı Yerleşimi
React 19 ve Next.js 15'in getirdiği en büyük kolaylıklardan biri olan Server Actions, form gönderim süreçlerini istemci tarafında karmaşık state ve API rotaları yazmadan yönetmeyi sağlar. Ancak formun sunucu tarafında doğrulanması (server-side validation) sonrasında dönen hata veya başarı mesajlarının arayüzde görüntülenmesi sırasında sıklıkla CLS hataları yapılır. Bir kullanıcı formu eksik veya hatalı doldurduğunda, sunucudan dönen hata mesajı ("Geçersiz e-posta adresi" gibi) form elemanlarının altına dinamik olarak enjekte edildiğinde, formun altında yer alan diğer elemanlar veya butonlar aniden aşağı kayar. Bu durum, kullanıcının tam "Siparişi Tamamla" butonuna basacağı sırada formun kaymasına ve CLS puanının artmasına yol açar.
Bu yerleşim kaymalarını engellemek için form arayüzlerindeki hata mesajı alanlarının en boy oranları ve yükseklikleri en baştan CSS ile rezerve edilmelidir. Hata mesajlarının görüntüleneceği konteynırlara minimum bir yükseklik (min-h-[24px] gibi) atamak veya bu alanları sayfa akışından (in-flow) çıkarıp absolute konumlandırmak, sunucudan hata mesajı geldiğinde sayfanın diğer öğelerinin kaymasını önler. Bir diğer gelişmiş yöntem ise CSS Grid kullanarak hata satırı için önceden tanımlanmış bir grid satır yüksekliği ayırmaktır. JavaScript hidrasyonu tamamlanmadan önce de bu alanlar boş bir şekilde yer kaplayacağı için, hata mesajı enjekte edildiğinde sıfır yerleşim kayması garanti edilir.
Ayrıca, kullanıcı deneyimini (UX) ve erişilebilirliği (accessibility) korumak için hata mesajlarının sadece görsel olarak değil, ekran okuyucular tarafından da doğru algılanması gerekir. React 19'un useActionState hook'u ile dönen durum mesajlarını içeren elementlere aria-live="polite" veya role="alert" özniteliklerini eklemek, ekran okuyucuların bu dinamik değişimi kullanıcıya sesli olarak bildirmesini sağlar. Hata mesajı alanının önceden rezerve edilmiş olması, hem teknik olarak CLS skorunu 0.000 seviyesinde tutar hem de ekran okuyucu kullanan dezavantajlı kullanıcılar için form etkileşimini son derece öngörülebilir ve kararlı hale getirir.
23. Next.js 15 ile Mobil Öncelikli LCP ve CLS Optimizasyonu: Taşınabilir Cihazlardaki Kısıtlar
Geliştiriciler genellikle yüksek işlemci gücüne sahip masaüstü bilgisayarlarında ve hızlı ofis internetlerinde test yaptıkları için mobil cihazlardaki performans darboğazlarını gözden kaçırırlar. Oysa Google'ın Core Web Vitals değerlendirmeleri ve Chrome UX Raporu (CrUX) field verileri ağırlıklı olarak mobil kullanıcıların gerçek deneyimlerini yansıtır. Mobil cihazlar, masaüstü bilgisayarlara kıyasla çok daha düşük CPU (işlemci) gücüne, kısıtlı GPU belleklerine ve kararsız ağ bağlantılarına (3G/4G/5G dalgalanmaları) sahiptir. Masaüstü tarayıcısında 1.2 saniyede tamamlanan LCP (Largest Contentful Paint) süreci, aynı kod tabanında mobil bir cihazda 3.5 saniyeye kadar çıkabilir.
Bu farkın temel nedeni, JavaScript paketlerinin mobil cihazlardaki ayrıştırma (parsing) ve derleme (compilation) sürelerinin uzunluğudur. Tarayıcı sunucudan gelen HTML'i render etmeye başlasa bile, büyük JavaScript dosyalarını indirip çalıştırmadan sayfa etkileşimli hale gelemez. React'in istemci tarafındaki hidrasyon (hydration) süreci, mobil işlemcilerde main thread'i yüzlerce milisaniye boyunca bloke edebilir ve bu da INP (Interaction to Next Paint) metriğinde ciddi takılmalara yol açar. Bu kısıtı aşmak için Next.js 15'te kod bütçesi (bundle budget) yönetimi yapmak zorunludur. Kritik olmayan tüm üçüncü taraf paketler ve client component'ler dynamic() import kullanılarak ve ssr: false seçeneğiyle, kullanıcının etkileşime geçtiği ana kadar ertelenmelidir.
Mobil cihazlardaki yerleşim kaymalarını (CLS) engellemek için ise CSS rendering katmanına odaklanmak gerekir. Mobil tarayıcılar, sayfa kaydırılırken (scrolling) adres çubuğunun gizlenip görünmesiyle viewport yüksekliğini (vh) dinamik olarak yeniden hesaplar. Eğer sayfa tasarımında 100vh gibi dinamik viewport birimleri kullanıldıysa, bu durum scroll sırasında tüm sayfa düzeninin kaymasına ve yüksek CLS skorlarına neden olur. Bu sorunu aşmak için modern CSS birimleri olan 100dvh (dynamic viewport height) veya 100svh (small viewport height) kullanılmalıdır. Ayrıca, mobil cihazların GPU'larını yormamak adına animasyonlarda top, left, margin gibi geometrik özellikleri değiştirmek yerine yalnızca transform: translate3d() ve opacity kullanılmalıdır. Bu yaklaşım, donanım ivmeli (hardware-accelerated) render edilmesini sağlayarak mobil cihazlarda INP ve CLS metriklerini ideal değerlerde tutar.
24. Üçüncü Parti Chat Widget'ları ve CLS İlişkisi: Canlı Destek Araçlarının Performansa Etkisi
Müşteri memnuniyetini ve dönüşüm oranlarını artırmak amacıyla e-ticaret ve hizmet sitelerinde sıklıkla kullanılan canlı destek widget'ları (Intercom, Crisp, Zendesk, WhatsApp iletişim butonları vb.), Core Web Vitals metriklerinin en büyük düşmanlarındandır. Bu araçlar genellikle sayfa açıldığı anda sunucudan asenkron olarak indirilen ve DOM'a dinamik olarak enjekte edilen büyük JavaScript paketleri içerir. Chat widget'ı yüklendiğinde, sayfanın sağ veya sol alt köşesinde aniden beliren konuşma balonu, sayfa akışından bağımsız gibi görünse de tarayıcının tüm sayfa düzenini (layout) ve boyama (paint) katmanlarını yeniden hesaplamasına neden olur. Eğer bu balonun açılmasıyla birlikte kullanıcı ekrandaki başka bir butona tıklamaya çalışıyorsa, bu etkileşim main thread'in bloke olması sebebiyle INP gecikmesine uğrar.
Canlı destek araçlarının neden olduğu CLS ve INP sorunlarını çözmek için K-ONTECH olarak iki aşamalı bir optimizasyon stratejisi uyguluyoruz. İlk olarak, chat widget'ının senkron olarak sayfa açılışında yüklenmesini engelliyoruz. Bunun yerine, tarayıcıya sadece chat balonunun görsel tasarımını taklit eden, HTML ve vanilla CSS ile kodlanmış hafif bir "placeholder" (yer tutucu) buton render ediyoruz. Bu buton tamamen statik olduğu için bundle boyutuna sıfır etki yapar ve sayfa yüklendiğinde hiçbir yerleşim kayması yaratmaz.
İkinci aşamada ise, gerçek chat widget'ı script dosyasını sadece kullanıcı bu placeholder butona tıkladığında veya sayfayı belirli bir miktar kaydırdıktan 3-5 saniye sonra dinamik olarak yüklüyoruz. Kullanıcı butona tıkladığı anda JavaScript çalışarak gerçek widget'ı indirir ve arayüzü devralır. Bu sayede, ilk yükleme sırasında (LCP ve FCP pencerelerinde) tarayıcı hiçbir harici chat script'i indirmekle uğraşmaz, ana konfigürasyon dosyalarını daha hızlı işler ve Lighthouse performans puanını korur. Bu yaklaşım, chat aracı kullanan enterprise projelerimizde toplam JavaScript bütçesini 120KB'ın üzerinde azaltarak INP değerlerini 90ms seviyesine düşürmemizi sağlamıştır.
25. Core Web Vitals Optimizasyonunda Gizli Yatırım Maliyetleri ve ROI Analizi
Web sitelerinin hızlandırılması ve Core Web Vitals metriklerinin iyileştirilmesi, sadece teknik bir başarı değil, aynı zamanda işletmeler için doğrudan bir finansal yatırım kararıdır. Bir projenin performans skorlarını kırmızı bölgeden yeşil bölgeye taşımak; kıdemli frontend mimarlarının çalışma saatlerini, performans monitoring (izleme) araçlarının lisans bedellerini ve daha gelişmiş CDN/Edge altyapılarının kullanım maliyetlerini beraberinde getirir. Dolayısıyla, bu optimizasyon sürecine başlamadan önce gerçekçi bir ROI (Yatırım Getirisi) analizi yapmak ve bütçe planlamasını doğru kurgulamak gerekir.
Mühendislik ve iş geliştirme süreçlerinde yapılan en yaygın hatalardan biri, "sonsuz optimizasyon" tuzağına düşmektir. Bir sayfanın Lighthouse skorunu 60'tan 90'a çıkarmak ortalama 20-30 saatlik bir çalışma gerektirirken, 95'ten 100'e çıkarmak component mimarisinin tamamen değiştirilmesini veya eski kütüphanelerin sıfırdan yazılmasını gerektirebileceği için 100 saatten fazla sürebilir. Bu noktada azalan verimler yasası (law of diminishing returns) devreye girer. İşletmeler için kritik hedef, Google'ın "Good" (İyi) eşiği olan yeşil alana (LCP < 2.5s, CLS < 0.1, INP < 200ms) girmektir. Bu eşiğin üzerine çıkmak için harcanacak ekstra bütçe, organik sıralamalarda veya dönüşüm oranlarında doğrusal bir artış getirmeyebilir.
Performans yatırımlarının geri dönüşü (ROI) ise üç ana kanaldan ölçülür. Birincisi, Google Ads kalite skorunun (Quality Score) yükselmesidir. Google, daha hızlı yüklenen ve sıfır CLS ile kararlı açılan landing page (açılış sayfası) adreslerine sahip reklamların tıklama başı maliyetlerini (CPC) düşürür ve reklam sıralamasını yükseltir. İkincisi, organik arama motoru trafiğindeki artıştır; Core Web Vitals sinyalleri yeşile döndükten sonra arama motorlarında görünürlük artar ve bu da ücretsiz trafik kazancı sağlar. Üçüncüsü ve en önemlisi, kullanıcı deneyiminin iyileşmesiyle doğrudan artan e-ticaret dönüşüm oranları ve azalan hemen çıkma (bounce) oranlarıdır. K-ONTECH olarak gerçekleştirdiğimiz entegrasyonlarda, Core Web Vitals değerleri yeşile geçen markaların dönüşüm oranlarında ortalama %18 ila %32 arasında kalıcı bir artış gözlemliyoruz. Bu da yapılan performans yatırımının genellikle 3 ila 6 ay gibi kısa bir sürede kendini amorti etmesini sağlamaktadır.
Sonuç: Sıfır CLS Mümkün, Ancak Disiplin Gerekiyor
Performans, tek seferlik bir proje değil; süregelen bir mühendislik disiplinidir. Next.js 15 araçları veriyor: next/image, next/font, Server Components, Suspense, Partial Prerendering. Ancak "otomatik optimizasyon" yok — her image, her font, her async component için bilinçli karar gerekir.
6 yıldır production Next.js projelerimde %94 CLS düşürme oranı elde ettik; sırrı: rezervasyon, rezervasyon, rezervasyon. CSS aspect-ratio, Suspense fallback, Image width/height — her yerde alan rezerve et, DOM render beklemeden. Üçüncü taraf scriptleri ihmal etme, INP'ye da göz kulak ol, ve lab data ile field data'nın her zaman farklı konuştuğunu kabul et.
Google Search Console'da yeşil görmek 4-6 hafta alabilir. Sabırlı olun, Lighthouse CI ile PR'ları kontrol edin, regresyon kabul etmeyin. Caching katmanını doğru kurgulayın, React 19'un compiler avantajlarını değerlendirin, ve performans kültürünü tüm takıma yayın. Tek bir geliştirici değil, tüm ekip performansın sahibi olduğunda Core Web Vitals kalıcı olarak yeşil kalır.
İletişim:
☎ 0530 281 31 48
📧 [email protected]
🌐 Ücretsiz PageSpeed Audit
Referans sürüm: Next.js 15.2.1, React 19 stable.



