Pages Router'dan App Router'a geçiş, Next.js'in getirdiği en büyük mimari dönüşümlerden biridir. Pek çok ekip bu geçişi tamamladıktan sonra ilk bakışta her şeyin yolunda göründüğünü, ancak zamanla ilk yüklenme sürelerinin ve JavaScript bundle boyutlarının beklenmedik biçimde arttığını fark ediyor. Sayfa sayısına göre bu artış onlarca kilobayt olabiliyor; Core Web Vitals değerleri düşüyor, kullanıcıya ulaşan kod giderek şişiyor.

Sorunun kökü çoğu durumda tek bir yerde: "use client" direktifinin bileşen ağacı içinde çok yukarı yerleştirilmesi. App Router, ağacın büyük bölümünü sunucuda render edip istemciye yalnızca gerçekten etkileşim gerektiren parçaları göndermek üzerine kurulu. Bu sınır bir kez yanlış çizildi mi, React Server Component felsefesinin tamamı işe yaramaz hale gelir ve migrasyon performansı iyileştirmek yerine kötüleştirir.

O sınır neden bu kadar kritik, nerede yanlış çizilir ve geçiş sonrasında bundle boyutu nasıl geri alınır: üçü de aynı mekanizmaya bağlanır.

Server Component avantajı nasıl anlık olarak yok olur?

App Router'ın temel vaadi basittir: bir bileşen sunucuda render ediliyorsa, o bileşenin içe aktardığı kütüphaneler istemci bundle'ına girmez. Bir grafik kütüphanesi, bir tarih formatleyici, hatta birkaç yüz kilobaytlık bir markdown parser - bunların hiçbiri tarayıcıya ulaşmaz.

"use client" direktifi bir bileşene eklenir eklenmez, o bileşen ve alt ağacındaki her şey istemci moduna geçer. Mesele şu: aynı direktif, o bileşenin tüm import'larını da istemci bundle'ına çeker. Bir sayfa bileşenine direktif koydunuz, o sayfanın içe aktardığı header, footer, sidebar - hepsi birlikte gelir. Header'ın da kendi import'ları vardır. Zincir bu şekilde büyür.

Pages Router'da zaten böyle çalışıyordu; her şey istemciye gidiyordu. App Router'a geçince ekipler genellikle aynı alışkanlıkla hareket ediyor: state veya event handler kullanan bir bileşen görünce hemen direktifi yazıyorlar. Geçişin hızını artırıyor ama avantajı da tamamen siliyor. Hız için alınan bu kararlar birikinceye kadar kimse bundle boyutuna bakmıyor.

"use client" direktifi nerede durmalı?

Kural şudur: direktif, etkileşim gerektiren en küçük birimin üstüne gelir; o birimin içinde yer aldığı sayfa veya layout'a değil.

Bir ürün detay sayfasını düşünün. Sayfada fiyat bilgisi, açıklama, görseller ve bir "sepete ekle" düğmesi var. Düğme tıklamaya tepki verdiği için istemci bileşeni olmak zorunda. Ama sayfanın geri kalanı sunucudan statik olarak gelebilir. Düğmeyi ayrı bir bileşene taşıyıp yalnızca ona direktifi eklerseniz, diğer her şey sunucuda kalır.

Pratikte şu soruyu sorun: "Bu bileşen useState, useEffect veya bir event listener kullanıyor mu?" Yanıt hayırsa, o bileşen büyük olasılıkla Server Component olabilir. Pages Router'daki getServerSideProps veya getStaticProps mantığı yerine artık veri doğrudan Server Component içinde çekiliyor; bu fark, sınırın ne kadar küçük tutulduğuyla doğrudan bağlantılıdır.

Ağaç tepesindeki tek direktif tüm alt dalı istemciye gönderir

Bu, geçiş sırasında en sık yapılan hatanın mekanizmasıdır.

Bir layout bileşenine direktif eklediğinizi varsayalım; belki layout içindeki bir navigasyon elemanında useState kullanmanız gerekiyor. Direktif layout'a konuldu. O layout altında render edilen veri tabloları, içerik bölümleri, hatta ayrı Server Component'ler - hepsi artık istemci tarafına geçer ve bundle'a girer.

Çözüm sınırı aşağı çekmektir. Navigasyonu ayrı bir bileşen olarak ayıklayın, yalnızca ona direktifi ekleyin. Layout kendi başına Server Component olarak kalır. Bu tek değişiklik bile ortalama bir projede onlarca kilobaytlık fark yaratabilir, çünkü layout altındaki tüm sayfalarda tekrarlanan import'lar artık bundle'a girmez.

Bileşen ağacını incelemenin pratik yolu şudur: her direktifi bulun, o bileşenin doğrudan import ettiği modülleri listeleyin. O modüller ne kadar büyük? O modüllerin de import'ları var mı? Bu zinciri takip etmek, regresyonun nerede başladığını ortaya koyar.

Context provider'lar özel bir risk taşır

React Context, değerleri ağaç boyunca aşağı iletmek için kullanılır ve Context Provider bileşenleri neredeyse her zaman direktif gerektirir. Pek çok ekip bu provider'ları root layout'a koyar: tema yönetimi, auth durumu, alışveriş sepeti, hepsi orada toplanır.

Provider sayısı arttıkça ve bunlar iç içe geçmeye başladıkça, layout'un fiilen istemci moduna geçme olasılığı da artar. Context API'nin render üzerindeki etkisi bu yapıyı ayrıntılı ele alıyor.

App Router'da bu sorunu çözmenin yolu provider sarmalama bileşenleri oluşturmaktır. Tüm provider'ları içeren küçük bir istemci bileşeni yazarsınız, onu layout içinde çocuklarla birlikte kullanırsınız. Layout kendisi Server Component olarak kalır; çocukları da sunucu tarafında render edilen içerikle beslenebilir. Sarmalayıcı direktife sahiptir, ama yalnızca o - layout değil. Bu ayrım, root layout'un altındaki tüm sayfaların sunucu tarafında işlenmeye devam etmesini sağlar.

Üçüncü taraf kütüphaneler ve istemci uyumluluğu

Pages Router'dan taşınan projeler genellikle window, document veya tarayıcıya özgü API'ler kullanan kütüphaneler içerir. Bu kütüphaneler Server Component içinde import edilirse build hatası alırsınız. Hatayı gidermek için bileşeni istemci bileşeni olarak işaretlemek cazip ve hızlı bir çözüm gibi görünür.

Ancak o bileşen bir layout veya pek çok sayfada paylaşılan bir yapıysa, bu karar bundle boyutunu önemli ölçüde artırabilir. Doğru yaklaşım: kütüphaneyi gerçekten ihtiyaç duyulan en küçük bileşen içinde kullanmak veya dinamik import ile yalnızca istemci tarafında yüklemek.

next/dynamic burada işe yarar. ssr: false seçeneğiyle içe aktarılan bir bileşen sunucu render'ında hiç işlenmez, istemcide lazy olarak yüklenir. Bundle analizi yaptığınızda bu bileşenlerin ayrı chunk'lara düştüğünü görürsünüz. Kod bölme ile sayfa sadeleştirme arasındaki karar bu pattern'in ne zaman tercih edileceğini açıklar.

Ağır istemci kütüphanelerini next/dynamic ile dinamik olarak yüklemek, ilk bundle boyutunu küçültür; bileşen yalnızca kullanıcı o bölüme ulaştığında ya da belirli bir koşul sağlandığında indirilir. Ancak bu yöntem her bileşen için uygun değildir. Sayfanın görünen alanına hemen giren bileşenlere dinamik import uygulamak, bağlantısı yavaş kullanıcılarda boş alan görünmesine yol açabilir; LCP değerini olumlu değil olumsuz etkiler. Dinamik import en çok sayfanın alt bölümlerindeki ağır bileşenler ile yalnızca bir kullanıcı eylemine bağlı açılır pencereler veya modal'lar için işe yarar. Bunun dışında kullanıldığında yükleme deneyimi bozulabilir; dolayısıyla her next/dynamic kararı için sayfadaki konumu ve ilk görünürlük olasılığı ayrı ayrı değerlendirilmelidir.

Bir kütüphane Server Component'a doğrudan import edilemez; ancak o kütüphaneyi kullanan bir Client Component'ın doğrudan import edilmesi, kütüphaneyi de bundle'a çeker. Import zincirini adım adım takip etmek, gereksiz şişkinliğin kaynağını bulmak için en etkili yöntemdir. Zincirleri kırmak için genellikle tek bir ara bileşen yeterlidir: kütüphane yalnızca bu bileşende import edilir ve dışarıya yalnızca arayüz dışa aktarılır.

Bundle analizi olmadan geçiş tamamlanmış sayılmaz

Geçiş sırasında elle yapılan incelemelerin bir sınırı vardır. Bir bileşeni Server Component olarak bıraktığınızı düşünürsünüz, ama başka bir istemci bileşen onu import ediyordur ve tüm ağaç istemciye kayar.

Next.js, next build çıktısında her sayfa için JavaScript boyutunu gösterir. Bu değerleri Pages Router'daki eski yapıyla karşılaştırın. Bir sayfanın First Load JS değeri beklenenden büyükse, o sayfada direktif sınırını araştırmanın zamanı gelmiştir.

Daha ayrıntılı analiz için @next/bundle-analyzer paketi kullanılabilir. Bu paket, webpack bundle'ınızı görsel ağaç haritasıyla gösterir; hangi modülün ne kadar yer kapladığını, hangi chunk içinde yer aldığını görebilirsiniz. Yayına çıkmadan önce yapılması gereken son kontroller arasında bundle analizi ayrı bir yer tutmalıdır. Geçiş sonrası bu adımlar atlandığında regresyon sessizce büyür.

Geçişten sonra direktifler ne zaman gözden geçirilmeli?

Zaman baskısı altında yapılan migrasyonlar genellikle iki aşamada gerçekleşir: önce çalışır hale getir, sonra optimize et. İlk aşamada her yere direktif koymak bir yöntem olabilir. Sorun şu ki ikinci aşama çoğu zaman erteleniyor, ertelendikçe teknik borç performansı sessizce aşındırmaya başlıyor.

Pratik bir kural: geçiş tamamlandıktan sonra her route için bileşen ağacını bir kez daha gözden geçirin. Her direktifin gerekçesini sorun. useState veya useEffect var mı? Tarayıcı API'si mi kullanılıyor? Bir event handler mı eklenmiş? Bu üçünden hiçbiri yoksa direktif büyük olasılıkla kaldırılabilir.

Global state için seçilen kütüphanenin bundle maliyeti de bu kararı doğrudan etkiler. Minimal istemci tarafı state API'si sunan kütüphaneler, direktif sınırını daha dar tutmanıza yardımcı olur; çünkü daha az bileşen state yönetimi için direktife ihtiyaç duyar.

App Router, Server Component mimarisinin tam anlamıyla işe yaraması için bileşen ağacının dikkatli tasarlanmasını gerektiriyor. Pages Router'dan taşınan projeler bu tasarımı zaten Pages Router düşünce biçimiyle getiriyor; her bileşen istemcide render edilmek üzere yazılmış. Geçişin kendisi bundle'ı küçültmez, istemci sınırının yeniden çizilmesi küçültür.

Performans regresyonu en tehlikeli olduğu zaman sessiz kalan türdür. Bundle yavaş yavaş büyür, her küçük direktif kararı birikir, kullanıcıya giden JavaScript her geçişte biraz daha artar. Bunu fark etmek için build çıktısını düzenli olarak izlemek ve bileşen ağacındaki sınırları periyodik gözden geçirmek gerekiyor.

Geçiş sırasında güvenli tarafta kalmak için izlenecek sıra: önce veri akışı ile etkileşim gereksinimleri ayrıştırılır, ardından direktif mümkün olan en küçük birime yerleştirilir, son olarak ilk yüklenme JavaScript boyutu referans değer olarak kaydedilir. Bu üç adım, regresyonu başlamadan önce engeller.