React 18 ile gelen Server Components, yalnızca bir render optimizasyonu değil; client bundle boyutunu köklü biçimde etkileyen mimari bir karardır. Bir bileşen sunucu tarafında çalışıyorsa, o bileşenin kodu tarayıcıya gönderilmez; bağımlılıkları da dahil. Bu fark, küçük bir seçim gibi görünse de ağır kütüphaneler kullanan uygulamalarda somut bir bundle küçülmesi anlamına gelir.

"Server component" başlığı bazen yanlış beklenti yaratır. Her bileşeni sunucuya taşımak bundle'ı küçültmez; etkileşim gerektiren bileşenler zaten client'ta kalmak zorundadır. Kazanç, doğru sınırı çizmekten geçer; nerede use client yazacağınızı ve nerede yazmayacağınızı bilmekten.

React'ı daha önce istemci taraflı bir kütüphane olarak kullananlar için bu geçiş kavramsal bir sıçrama gerektirir. Bileşenler artık iki farklı ortamda yaşayabilir ve her ortamın kendine özgü kısıtları, avantajları vardır. Bu ayrımı erken anlamak, mimari kararlarınızı daha sağlam bir zemine oturtur.

Server Component nedir ve client bundle'a neden girmez?

Next.js 13+ App Router veya Server Components destekleyen başka bir framework kullandığınızda, app/ dizinindeki bileşenler varsayılan olarak Server Component'tir. Bu bileşenler sunucuda render edilir ve sonuçları - HTML değil, React'ın kendi iç iletişim formatı - istemciye gönderilir. Bileşenin kaynak kodu bu transferin içinde yer almaz.

Geleneksel bir istemci bileşeni düşünün: tarayıcı onu indirmek, parse etmek ve çalıştırmak zorundadır. Server Component'te bu adımların hiçbiri tarayıcıda gerçekleşmez. Sunucu kodu çalıştırır, çıktıyı üretir ve istemci yalnızca bu çıktıyı alır. Bundle analyzer'ı açıp baktığınızda o bileşeni göremezsiniz, çünkü orada değildir.

Bu mekanizma, büyük kütüphanelerin bundle'dan çıkarılmasının kapısını aralar. Bir bileşen yalnızca sunucuda çalışıyorsa, onun import ettiği her modül de sunucu tarafında kalır; tarayıcıya gönderilecek pakete hiç girmez.

Ağır bağımlılıklar client bundle'dan nasıl çıkar?

Diyelim ki Markdown içeriği render eden bir bileşeniniz var. remark, rehype, prism veya benzer kütüphaneleri import ediyorsunuz. Bu kütüphanelerin her biri onlarca, hatta yüzlerce kilobayt olabilir. Bileşeni client'ta tutarsanız tüm bu ağırlık kullanıcının indirmesi gereken pakete girer; bileşeni Server Component yaparsanız bu kütüphaneler sunucuda çalışır ve bundle'a hiç girmez.

Pratikte en fazla kazanç sağlayan senaryolar şunlardır: veri formatını dönüştüren kütüphaneler (CSV parser, Markdown işleyici, XML okuyucu), büyük icon setleri, statik içerik üretirken kullanılan yardımcı araçlar. Bu bileşenler etkileşim içermiyorsa sunucuya taşımak bundle boyutunu doğrudan düşürür.

Tersine, etkileşimli bir bileşenin bağımlılıklarını bundle'dan çıkarmanın yolu yoktur. useState, useEffect, event handler - bunların herhangi biri bir bileşende varsa, o bileşen client'ta kalmak zorundadır ve import ettiği her modül de onunla birlikte pakete girer. Kazanımın gerçek kaynağı, etkileşim gerektirmeyen ama ağır kütüphaneler kullanan bileşenlerdir.

Veri çekme maliyeti sunucuya taşınır

Client taraflı veri çekme uzun bir zincir izler: HTML gelir, JavaScript indirilir, parse edilir, çalıştırılır, useEffect tetiklenir, API isteği gönderilir, yanıt gelir, state güncellenir ve bileşen yeniden render edilir. Kullanıcı bu sürenin büyük bölümünde boş ekran veya iskelet görüntü görür; içerik, ağ gecikmesi ve JavaScript işleme süresi bittikten sonra belirir.

Server Component'te bu zincirleme kaybolur. Bileşen render edilmeden önce veri zaten çekilmiştir; sunucu bileşenin içinde doğrudan await fetch(...) çağrısı yapabilirsiniz ve tarayıcıya ulaşan içerik hazır verilerle birlikte gelir.

// Server Component - doğrudan async/await kullanılabilir
async function ProductList() {
  const products = await fetch('https://api.example.com/products')
    .then(r => r.json());
  return (
    <ul>
      {products.map(p => <li key={p.id}>{p.name}</li>)}
    </ul>
  );
}

Sunucu ile API arasındaki ağ gecikmesi çoğu durumda çok düşüktür; veri merkezi içi iletişimde milisaniyeler konuşulur. Kullanıcı ile API arasındaki gecikme ise coğrafi konuma, bağlantı kalitesine ve ara noktalara göre bunun birkaç katı olabilir. Veri çekmeyi sunucuya taşımak bu farktan doğrudan yararlanır.

İç içe Server Component'ler paralel veri çekebilir. Promise.all ile birden fazla kaynaktan veri almak, her isteğin tamamlanmasını bekleyen bir zincirin önüne geçer ve toplam bekleme süresini en uzun tekil istek süresine indirir.

use client sınırı nerede çizilmeli?

Sınırı yaprak bileşenlere taşıyın. use client bir bileşeni tamamen client'a taşımaz; bir sınır çizer ve o sınırdan itibaren alt ağaçtaki her bileşen client bundle'a girer. Sınırı ağacın derinliklerine, yani gerçekten etkileşim gerektiren en küçük bileşene doğru itmek, bundle'a giren kod miktarını azaltır.

Somut bir örnek: Bir sayfa bileşeniniz var; içinde veri tablosu ve bir filtreleme dropdown'u bulunuyor. Dropdown etkileşimlidir, use client gerektirir. Tablo ise statik veriyi listeler. Tüm sayfayı use client yaparsanız tablo da bundle'a girer, tüm import'larıyla birlikte. Yalnızca dropdown bileşenini use client yaparsanız tablo Server Component olarak kalır ve bundle boyutunu büyütmez.

// Geniş sınır - tablo da client'a giriyor
'use client';
export default function ProductPage({ products }) {
  const [filter, setFilter] = useState('');
  return (
    <div>
      <input value={filter} onChange={e => setFilter(e.target.value)} />
      <ProductTable products={products} />
    </div>
  );
}

// Dar sınır - yalnızca input client'ta
'use client';
export function FilterInput({ value, onChange }) {
  return <input value={value} onChange={onChange} />;
}

// ProductPage - Server Component
async function ProductPage() {
  const products = await getProducts();
  return (
    <div>
      <FilterSection />     {/* client sınırı burada */}
      <ProductTable products={products} />  {/* server'da kalır */}
    </div>
  );
}

use client sınırının nerede başladığını görmek için Next.js build çıktısındaki client reference manifest dosyasına bakabilirsiniz. Hangi modüllerin client bundle'ına girdiği orada listelenir ve beklenmedik büyük modüller bu adımda fark edilir.

Server ve Client bileşenleri birlikte kullanmak

Server Component'ler, Client bileşenlerini children veya prop olarak alabilir. Bu, bileşen ağacı içinde sınırları esnek biçimde yönetmenin temel yoludur ve çoğu gerçek uygulamada kaçınılmazdır; saf Server Component ağaçları nadiren yeterli olur.

// ServerWrapper - Server Component
import ClientButton from './ClientButton'; // 'use client' bileşeni

export default async function ServerWrapper() {
  const data = await getData();
  return (
    <div>
      <p>{data.title}</p>
      <ClientButton label="Kaydet" />
    </div>
  );
}

Ancak Client Component içinde doğrudan bir Server Component import etmek mümkün değildir. Client Component tarayıcıda çalışır; Server Component ise sunucuda. Bu durumda children prop'u devreye girer: Server Component, Client bileşenine bir alt eleman olarak başka bir Server Component geçirebilir ve React bu bağlantıyı doğru şekilde çözer.

// ClientLayout - 'use client'
export default function ClientLayout({ children }) {
  const [open, setOpen] = useState(false);
  return (
    <div className={open ? 'expanded' : 'collapsed'}>
      {children}
    </div>
  );
}

// Kullanım - Server Component içinde
<ClientLayout>
  <ServerContent />  {/* children olarak geçiyor, çalışır */}
</ClientLayout>

Bu pattern, bileşen ağacının büyük bölümünü server'da tutarken küçük etkileşimli adacıklar oluşturmanıza olanak tanır. Sayfa layout'u Server Component, içindeki açılır menü Client Component, açılır menünün içindeki içerik yeniden Server Component olabilir.

Server Component ne zaman işe yaramaz veya ters etki yapar?

Her durumda tercih etmek doğru değildir. Birkaç senaryo Server Component'in sınırlarını açıkça gösterir.

Etkileşimli içerik bu listenin başında gelir. Formlar, modallar, sürükle-bırak alanları, gerçek zamanlı güncellemeler - bunlar useState, useReducer veya event handler gerektirir. Bu API'ler Server Component'lerde yoktur ve olmayacaktır; tasarım gereği sunucu ortamıyla uyumsuzlardır.

Tarayıcı API'lerine erişim de bir sınır oluşturur. window, document, localStorage, navigator - bunlar yalnızca istemcide vardır. Bu nesnelere erişen bir bileşeni sunucuya taşırsanız build zamanında veya çalışma zamanında hata alırsınız.

Mevcut projeye eklemenin karmaşıklığı da göz ardı edilemez. App Router mimarisine geçmeden Server Component kullanmak mümkün değildir; Pages Router kullanan bir Next.js projesinde kademeli geçiş seçeneği vardır, ama tam faydayı görmek için mimariyi yeniden düzenlemek gerekir. Birkaç düzine kilobaytlık bir bundle'ı küçültmek için bu çabanın değip değmeyeceğini projenin büyüklüğü belirler.

Çok basit uygulamalarda da kazanç sınırlı kalır. Zaten minimal JavaScript içeren bir proje Server Component mimarisine geçerek önemli bir iyileşme sağlamaz; karmaşıklık kazancın önüne geçer.

Bundle farkını nasıl ölçersiniz?

Tahmin yeterli değildir. Aynı bileşeni önce use client olarak, sonra Server Component olarak build edin ve Next.js'in build çıktısında her sayfa için gösterilen client bundle boyutlarını karşılaştırın. Fark orada görünür.

@next/bundle-analyzer paketi webpack bundle'ını görsel olarak sunar. Hangi modüllerin client bundle'ına girdiğini, hangilerinin yalnızca sunucuda kaldığını görebilirsiniz. Özellikle node_modules içindeki ağır bağımlılıkların nerede çalıştığını takip etmek için kullanışlıdır; beklenmedik büyük paketler genellikle burada fark edilir.

Chrome DevTools'daki Network sekmesi, sayfa yüklenirken indirilen JavaScript dosyalarının toplam boyutunu gösterir. Coverage sekmesi ise indirilen kodun ne kadarının gerçekten çalıştırıldığını ortaya koyar; düşük kapsama oranı, bundle'da kullanılmayan kod bulunduğuna işaret eder ve Server Component geçişi için iyi bir başlangıç noktası sağlar.

Server Components, React ekosisteminde yalnızca performans optimizasyonu değil; bir tasarım kararı olarak değer kazanıyor. Hangi bileşenin nerede çalıştığı, bundle boyutunu, veri çekme modelini ve kullanıcı deneyimini birlikte şekillendiriyor.

Pratik başlangıç noktası şudur: mevcut bileşenlerinize bakın ve hangileri hiç etkileşim içermediğini sorun. Veri gösteren, içerik render eden, statik layout oluşturan bileşenler güçlü adaylardır. Etkileşimli bileşenleri use client ile en küçük yüzeye sıkıştırın; geri kalanını sunucuya taşıyın.

Bu tek seferlik bir geçiş değil, bileşen eklendikçe sorulan bir alışkanlıktır: bu bileşen tarayıcıda çalışmak zorunda mı? Cevap hayırsa sunucuda kalır.