İLETİŞİME GEÇİN
Server Components, paralel veri yükleme, Suspense ile streaming, önbellek kararları ve Server Actions: App Router ile performans odaklı bir Next.js mimarisi nasıl kurulur?

Next.js App Router, React Server Components üzerine kurulu bir yönlendirme ve render modelidir. Pages Router'dan geçen ekipler için ilk şaşırtıcı nokta, bileşenlerin varsayılan olarak sunucuda çalışmasıdır. Bu tek fark; veri yüklemeyi, önbelleği ve tarayıcıya gönderilen JavaScript miktarını doğrudan etkiler. Bu yazıda App Router ile üretime çıkan bir uygulamada performansı belirleyen kararları sırasıyla ele alıyoruz: sunucu ve istemci bileşeni sınırı, veri yükleme, streaming, önbellek ve mutasyonlar.
app/ dizinindeki her bileşen, aksi belirtilmedikçe bir Server Component'tir. Sunucuda render edilir, kodu tarayıcıya gönderilmez ve doğrudan veritabanına ya da iç servislere erişebilir. Etkileşim gerektiren bileşenler ise dosyanın başına "use client" yazılarak Client Component olarak işaretlenir.
Client Component'e yalnız şu durumlarda ihtiyaç vardır:
useState, useEffect gibi state veya yaşam döngüsü hook'ları kullanılıyorsa,onClick, onChange gibi olay dinleyicileri gerekiyorsa,window, localStorage gibi tarayıcı API'lerine erişiliyorsa,Önemli ayrıntı şu: "use client" bir sınır tanımlar ve o dosyanın içe aktardığı her şey de istemci paketine girer. Bu yüzden sınırı ağacın olabildiğince alt yapraklarına çekin. Bütün sayfayı istemci bileşeni yapmak yerine yalnız etkileşimli butonu ayırın. Bir istemci bileşeninin içinde sunucu bileşeni göstermek gerekiyorsa onu içe aktarmak yerine children olarak geçirin:
// app/panel/layout.tsx (Server Component)
import { KenarCubugu } from "./kenar-cubugu"; // "use client"
import { MenuOgeleri } from "./menu-ogeleri"; // Server Component
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<KenarCubugu menu={<MenuOgeleri />}>
{children}
</KenarCubugu>
);
}
Sunucuya özel kodun (veritabanı istemcisi, gizli anahtar okuyan modüller) yanlışlıkla istemci paketine girmesini önlemek için bu modüllerin başına import "server-only"; eklemek, hatayı derleme anında yakalar.
Server Component'ler async olabildiği için veri, onu kullanan bileşenin içinde doğrudan beklenebilir. Bu rahatlık beraberinde bir tuzak getirir: Birbirinden bağımsız istekleri art arda await etmek, toplam süreyi isteklerin toplamına çıkarır. Bağımsız istekleri paralel başlatın:
// Art arda: süreler toplanır
const musteri = await getMusteri(id);
const siparisler = await getSiparisler(id);
// Paralel: en yavaş istek kadar sürer
const [musteri, siparisler] = await Promise.all([
getMusteri(id),
getSiparisler(id),
]);
Aynı veriye birden çok bileşende ihtiyaç duyuluyorsa, veriyi prop olarak taşımak yerine her bileşende ayrı ayrı çağırabilirsiniz. Aynı render içindeki özdeş GET fetch çağrıları otomatik olarak tekilleştirilir (request memoization). ORM veya veritabanı istemcisi gibi fetch dışı çağrılar için React'in cache() fonksiyonu aynı işi görür.
Sayfanın tamamı en yavaş veri kaynağını beklemek zorunda değildir. Bir rota klasörüne eklenen loading.tsx dosyası, o segment için otomatik bir Suspense sınırı oluşturur. Daha ince kontrol için <Suspense> doğrudan kullanılabilir: Sayfa iskeleti hemen gönderilir, yavaş bölüm hazır olduğunda aynı yanıtın içinde akarak yerine oturur.
// app/urunler/[id]/page.tsx
import { Suspense } from "react";
export default async function UrunSayfasi({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
const urun = await getUrun(id);
return (
<main>
<UrunDetay urun={urun} />
<Suspense fallback={<YorumIskeleti />}>
<Yorumlar urunId={id} />
</Suspense>
</main>
);
}
Next.js 15 ile birlikte params ve searchParams birer Promise olarak gelir; cookies() ve headers() da asenkron hâle geldi. Eski sürümlerden geçiyorsanız bu çağrıları await etmeyi unutmayın.
Suspense sınırlarını kullanıcı açısından anlamlı yerlere koyun. Her küçük parçayı ayrı ayrı sarmak, ekranın parça parça zıplayarak dolmasına yol açar; ayrıca iskelet bileşenlerinin gerçek içerikle aynı boyutta olması, yerleşim kaymasını (CLS) önler.
App Router'da önbellek davranışı sürümler arasında önemli ölçüde değişti. Next.js 14'te fetch çağrıları varsayılan olarak önbelleğe alınırken, Next.js 15 ile varsayılan önbelleksiz davranışa geçildi. Bu yüzden "bu veri ne kadar taze olmalı?" sorusunu her veri kaynağı için açıkça cevaplamak gerekir:
// Beş dakikada bir yenilenebilir, etiketle geçersiz kılınabilir
const res = await fetch("https://api.example.com/urunler", {
next: { revalidate: 300, tags: ["urunler"] },
});
// Her istekte taze veri
const stok = await fetch(url, { cache: "no-store" });
Next.js 16 ile birlikte gelen Cache Components modelinde ise önbelleğe alınacak fonksiyon veya bileşen "use cache" direktifiyle işaretlenir; süre ve etiketler cacheLife ve cacheTag ile tanımlanır. Hangi modelin projenizde etkin olduğu sürüme ve next.config ayarlarına bağlıdır; kullandığınız sürümün dokümantasyonunu esas alın.
Genel kural olarak; ürün kataloğu, blog yazısı gibi herkes için aynı olan veriler önbelleğe alınmaya ve statik üretilmeye uygundur. Kullanıcıya özel veriler ve anlık stok gibi bilgiler ise ya önbelleğe alınmamalı ya da çok kısa ömürlü tutulmalıdır.
Form gönderimleri ve veri değişiklikleri için Server Actions, ayrı bir API rotası yazmadan sunucuda çalışan fonksiyonlar tanımlamayı sağlar. Değişiklikten sonra ilgili sayfanın önbelleği revalidatePath veya revalidateTag ile geçersiz kılınır.
"use server";
import { revalidatePath } from "next/cache";
import { z } from "zod";
const Sema = z.object({ ad: z.string().min(1), fiyat: z.coerce.number().positive() });
export async function urunGuncelle(id: string, formData: FormData) {
const oturum = await oturumuGetir();
if (!oturum?.yetkiler.includes("urun:duzenle")) throw new Error("Yetkisiz");
const veri = Sema.parse(Object.fromEntries(formData));
await db.urun.update({ where: { id }, data: veri });
revalidatePath(`/urunler/${id}`);
}
Server Action'lar dışarıdan çağrılabilen uç noktalardır. Arayüzde butonun gizli olması güvenlik sağlamaz; yetki kontrolü ve girdi doğrulaması her zaman action'ın içinde yapılmalıdır.
next/image boyutlandırma ve modern formatları, next/font ise fontları kendi alan adınızdan sunmayı ve yerleşim kaymasını azaltmayı üstlenir.@next/bundle-analyzer ile istemci paketine nelerin girdiğine düzenli bakın; büyük kütüphaneleri yalnız gerektiği yerde next/dynamic ile yükleyin.generateMetadata ile sunucuda üretin.error.tsx ve not-found.tsx ekleyin.Geçiş için her şeyi bir anda taşımak gerekmez; pages/ ve app/ dizinleri aynı projede birlikte çalışabilir. Yeni sayfaları App Router'da yazıp eski sayfaları zamanla taşımak, riski küçük parçalara böler. Taşırken getServerSideProps ve getStaticProps mantığını doğrudan async Server Component'lere, API rotalarını da gerektiğinde Route Handler'lara veya Server Action'lara dönüştürebilirsiniz.
App Router'ın sunduğu performans avantajı kendiliğinden gelmez; sınırı doğru çizilmiş istemci bileşenleri, paralel veri yükleme, anlamlı Suspense sınırları ve bilinçli önbellek kararlarıyla ortaya çıkar. Server-first varsayılanı korumak ve istemciye yalnız gerçekten gerekeni göndermek, bu kararların ortak paydasıdır.
Yorumlar yakında etkinleştirilecek.

LCP, INP ve CLS neyi ölçer, eşikleri nedir ve üretimde nasıl iyileştirilir? Saha ve laboratuvar ölçümünden performans bütçesine pratik bir rehber.

Token, primitive ve composite bileşen katmanlarıyla erişilebilir, temalanabilir ve sürdürülebilir bir React tasarım sistemi nasıl kurulur?

Mobil uygulama projenize başlamadan önce verilecek en kritik kararlardan biri: native mi, cross-platform mı? İki yaklaşımı dürüstçe karşılaştırıyor, doğru seçimi nasıl yapacağınızı anlatıyoruz.