Pages Router'dan App Router'a geçişte performans regresyonu çoğunlukla 'use client' sınırının yanlış çizilmesinden kaynaklanır. Hangi bileşenin Server Component kalması gerektiğini, hydration maliyetini nasıl ölçeceğinizi ve bundle analizi ile regresyon noktasını nasıl bulacağınızı anlatan geçiş rehberi.

Pages Router'dan App Router'a Geçişte Performans Regresyonu Nasıl Önlenir?

Pages Router'dan App Router'a geçmek, Next.js projelerinde sık yapılan ve genellikle iyi niyetle başlanan bir karardır. Yeni routing sistemi, React Server Components (RSC) desteği ve sunucu taraflı veri getirme için daha doğrudan primitifler sunar; ama geçiş süreci dikkatli yönetilmezse LCP değerlerinin yükselmesi, FCP gecikmesi ve JavaScript bundle boyutunun beklenmedik biçimde şişmesi kaçınılmaz olur.

Performans regresyonlarının büyük bölümü tek bir yanlış anlamadan kaynaklanır: App Router'ın varsayılan olarak her bileşeni sunucu taraflı çalıştırdığını bilmek, ama hangi dosyanın nerede Client Component sınırı çizdiğini tam kavrayamamak. Bu kafa karışıklığı biriktiğinde, Pages Router'daki orijinal bundle'dan daha büyük ve daha ağır bir çıktıya ulaşmak mümkündür.

Migrasyon sırasında veya hemen sonrasında regresyon, use client sınırının nereye çizildiğinden, Server Component'in istemciye kaymasından ve hydration maliyetinin nasıl büyüdüğünden okunur. Ölçüm, önlem alınacak noktayı tahmin değil somut hale getirir.

Pages Router ve App Router'ın performans modeli

Pages Router'da her sayfa istemcide tam olarak hydrate edilir; bileşen ağacı büyüdükçe JavaScript payloadu büyür. Veri getirme getServerSideProps veya getStaticProps ile yapılır ve bileşenler ayrıca client-side state tutabilir. Bu model tahmin edilebilirdir, ama sunucu taraflı renderın sağladığı avantajları sınırlı ölçüde kullanır.

App Router, React Server Components üzerine kurulur. Bir bileşen dosyasının başında "use client" direktifi yoksa, o bileşen sunucuda render edilir ve istemciye hiç JavaScript gönderilmez. Veri getirme doğrudan async bileşen gövdesinde yapılabilir; getServerSideProps kalkmıştır.

Doğru kurgulandığında interaktif olmayan içerik için sıfır JavaScript gönderilir. Ters yüzü şudur: her "use client" direktifi o bileşeni ve onun tüm alt ağacını tarayıcıya JavaScript olarak gönderir. Sınır ne kadar yukarıda çizilirse, potansiyel o kadar boşa gider. App Router ile Pages Router arasındaki gerçek fark, bu sınırı nereye koyduğunuzla belirlenir.

use client sınırını yanlış çizmek

En sık görülen hata, büyük bir layout veya wrapper bileşenine "use client" eklemektir. Geçiş sırasında "bu bileşende bir onClick var, direktif gerekiyor" denilir ve dosyanın başına eklenir. Bileşen bir wrapper ise altındaki on, yirmi bileşen de aynı anda istemciye gönderilir.

Tipik senaryo şöyle gelişir: AppShell.tsx hem navigasyon hem de bir hamburger menü tutar; hamburgerin açılıp kapanması için useState gerekir. Direktif eklenir. Altındaki <Header>, <Nav>, <Logo>, <Breadcrumb> ve diğer statik bileşenler de artık istemci taraflı JavaScript paketine girer. Pages Router'a kıyasla yüklenen JavaScript miktarı ciddi ölçüde artar.

Düzeltme nettir. useState veya useEffect gerektiren parçayı kendi küçük bileşenine çıkarın: MenuToggle.tsx kendi dosyasında "use client" direktifi taşır; AppShell sunucu taraflı kalır ve altındaki ağaç sunucuda render edilir. İstemciye gönderilen JavaScript'i bileşen ağacının yaprakları düzeyinde tutmak, App Router'ın performans modelini doğru kullanmanın özüdür.

Yaygın ikinci hata, bir Server Component'e prop olarak event handler geçmeye çalışmaktır. onClick={handleClick} gibi bir prop sunucu tarafında serialize edilemez; Next.js derleme sırasında uyarı verir. Tepki olarak geliştiriciler bazen tüm bileşeni "use client" ile işaretlerler, oysa gereken yalnızca interaktif parçanın ayrıştırılmasıdır.

Sınır kararlarını gözden geçirirken şu soruyu sorun: bu bileşen tarayıcıya özgü bir API kullanıyor mu (window, document, localStorage), bir event handler'a mı ihtiyaç duyuyor, yoksa sadece veri mi gösteriyor? Son durum için Server Component yeterlidir; direktif eklemek performansı artırmaz, düşürür.

Server Component'i Client Component'e dönüştürmenin maliyeti

Bir bileşen Server Component'ten Client Component'e geçtiğinde üç şey değişir: o bileşenin JavaScript kodu bundle'a eklenir, içindeki veri getirme mantığı tarayıcıya taşınır ve hydration sırasında çalıştırılması gereken kod artar. Bunların toplamı, kullanıcının sayfayı etkileşime hazır gördüğü anı - yani Time to Interactive (TTI) değerini - doğrudan etkiler.

Büyük veri işleme yapan bileşenler özellikle maliyetlidir. Bir blog sayfasında Markdown'ı HTML'e dönüştüren bir bileşen düşünün. Server Component olduğunda bu dönüşüm sunucuda olur, istemciye yalnızca sonuç HTML gönderilir. Client Component yapıldığında hem dönüştürücü kütüphane hem de ham Markdown verisi tarayıcıya indirilir; dönüşüm her sayfa açıkta tekrarlanır.

Kütüphane boyutu da hesaba katılmalıdır. Bir Server Component içinde kullandığınız date-fns, marked veya lodash gibi bir paketin kodu istemciye gönderilmez. Aynı paketi "use client" direktifi taşıyan bir bileşen import ederse, paket bundle analizinde görünür hale gelir. Geçiş sırasında bu farkı fark etmemek, kaynaklarda açıklanamayan büyümelere yol açar.

Somut kontrol: bir bileşeni Client Component yapma kararı alırken import listesine bakın. Ağır bir kütüphane varsa ve o kütüphane yalnızca render zamanında veri işlemek için kullanılıyorsa, bileşeni Server Component olarak tutmak ve interaktif kısmı ayrıştırmak çoğunlukla daha yerinde bir tercih olur.

Hydration maliyetini tanılamak

Hydration, sunucudan gelen HTML'e tarayıcının event listener ve state bilgisini bağladığı süreçtir. App Router'da yalnızca Client Component'ler hydrate edilir; Server Component'ler statik HTML olarak kalır. Geçiş sonrasında beklenen, daha az bileşenin hydrate edilmesi ve dolayısıyla ana iş parçacığının daha kısa süre meşgul olmasıdır.

Regresyon görmek mümkündür. Tarayıcının DevTools Performance sekmesinde kaydı başlatın, sayfayı yenileyin ve kaydı durdurun. "Main thread" satırında "Hydration" veya "React" etiketli görevlerin toplam süresine bakın. Geçiş öncesi bir karşılaştırma kaydınız varsa iki değeri karşılaştırın; regresyon varsa hangi bileşen ağacından kaynaklandığını izlemek mümkün olur.

React DevTools Profiler de kullanışlıdır. Client Component ağacının ne kadar derin olduğunu, hangi bileşenlerin render süresini artırdığını gösterir. App Router geçişi sonrasında öncekinden çok daha fazla bileşen Profiler'da görünüyorsa, "use client" sınırları büyük ihtimalle gereğinden yukarıda çizilmiştir.

Hydration hataları ayrı bir kategoridir ve doğrudan regresyon sayılmaz; ama sessizce performansı yer. Sunucuda render edilen HTML ile istemcide oluşturulan sanal DOM arasındaki fark, Next.js'in tüm ağacı yeniden render etmesine neden olur. Konsolda Hydration failed veya There was an error while hydrating uyarılarına dikkat edin; bunlar çoğu zaman sunucu-istemci farklılığından kaynaklanır ve tahmin edilenden daha uzun main thread görevlerine dönüşür.

Bundle analizi ile regresyon noktasını bulmak

Next.js projesine @next/bundle-analyzer eklemek, istemciye gönderilen JavaScript'in hangi modüllerden oluştuğunu görselleştirir. Kurulum iki adımdır: paketi yükleyin, next.config.js'de sarmalayın.

// next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
  enabled: process.env.ANALYZE === 'true',
})

module.exports = withBundleAnalyzer({
  // mevcut next config
})

ANALYZE=true next build komutu çalıştırıldığında tarayıcıda interaktif bir treemap açılır. Client bundle'ını inceleyin; Server Components'tan geldiğini sandığınız bir modül orada görünüyorsa, o bileşen zincirinde bir yerde beklenmedik bir "use client" sınırı var demektir.

Karşılaştırma yapmak için Pages Router sürümünden ve App Router migrasyonundan sonra iki ayrı build raporu alın. Toplam istemci bundle boyutunun artması tek başına sorunun işaretidir. Ağır bir kütüphane (örneğin bir Markdown işleyici veya tarihleme kütüphanesi) Pages Router'da da vardı ve şimdi Client bundle'ında görünüyorsa, o kütüphaneyi kullanan bileşen Server Component olabilecekken Client Component yapılmış demektir.

Route bazında analiz de yapılabilir. App Router, .next/analyze/ altında her route için ayrı chunk dosyaları oluşturur. Beklenmedik biçimde büyük bir route varsa, ilgili sayfanın bileşen ağacını taramak gerekir.

Geçiş sırasında metrik takibi

Geçiş tek seferlik değil, sayfa sayfa yapılan bir süreçtir. Her sayfa migrasyonunda üç metriği kayıt altına almak, regresyonların birikmesini önler: ilgili sayfanın LCP değeri, istemci JavaScript boyutu ve Lighthouse Performance skoru. Geçiş öncesi değerler referans noktası olarak saklanmalı; sonrasındaki değerler bu referanstan belirgin biçimde kötüleşiyorsa sayfanın bileşen yapısı gözden geçirilmelidir.

Lab ölçümü için Lighthouse CI kullanılabilir. CI pipeline'a entegre edildiğinde her deploy sonrası otomatik Lighthouse raporu üretir ve önceki değerlerle karşılaştırır. App Router geçişi sırasında bu otomasyonu devreye almak, hangi commit'te hangi sayfanın gerilediğini kesin olarak gösterir.

Gerçek kullanıcı verisi (RUM) için web-vitals kütüphanesi Next.js App Router ile uyumludur. app/layout.tsx içine yerleştirilen bir Client Component, onCLS, onLCP, onFCP callback'lerini dinleyerek verileri bir analytics endpoint'ine gönderebilir. Lab verisinde görünmeyen mobil cihaz veya yavaş bağlantı sorunları bu yolla yakalanır.

Geçiş tamamlandıktan sonra bile ölçümün sürekliliğini sağlamak gerekir. Yeni bir bileşen eklendiğinde veya mevcut bir bileşen refactor edildiğinde "use client" sınırı yanlışlıkla yukarı taşınabilir. Bunu fark etmenin yolu düzenli bundle analizi ve otomatik Lighthouse kontrolüdür; aksi takdirde regresyon, birkaç commit gecikmesiyle fark edilir ve kök nedeni bulmak zorlaşır.

Pages Router'dan App Router'a geçiş performans açısından fırsatlar sunar; ama bu fırsatlar kendiliğinden hayata geçmez. "use client" direktifini nereye koyduğunuz, hangi bileşeni Server Component olarak tuttuğunuz ve hydration yükünü nasıl dağıttığınız, migrasyonun sonucunu belirler. Doğru sınırlar çizildiğinde daha az JavaScript, daha kısa hydration süresi ve daha iyi LCP değerleri elde edilir; yanlış çizildiğinde Pages Router'ın ötesinde bir ağırlık oluşur.

Tanılama araçları bu süreçte rehberdir: bundle analyzer hangi kütüphanenin nereye gittiğini gösterir, DevTools Performance sekmesi hydration süresini ölçer, Lighthouse CI bir sonraki commit'in regresyon getirip getirmediğini söyler. Ölçüm olmadan yapılan migrasyon, sorunun ne zaman başladığını bilmeden çalışmak demektir.

Geçiş tamamlandıktan sonra da "use client" sınırlarını düzenli aralıklarla gözden geçirmek yararlıdır. Kod tabanı büyüdükçe yeni bileşenler eklenir, mevcut bileşenler birleşir; sınırlar sürünür. Bunu fark etmenin en pratik yolu otomatik bundle boyutu takibidir: sayfa başına istemci JavaScript'i bir önceki deploy'a kıyasla anlamlı biçimde artıyorsa, inceleme zamanı gelmiştir.