Mert Özen Her Satırda Daha İleri
Frontend Web Performansı — #08

Angular SSR, Hydration ve Performans: Prerender, Incremental Hydration, Event Replay ve TTFB Bedeli

Mert Özen 15 Kas 2026 14 dk 9 görüntülenme
Angular SSR, Hydration ve Performans: Prerender, Incremental Hydration, Event Replay ve TTFB Bedeli

SSR ve prerender, LCP içeriğini HTML'e koyarak CSR'a göre FCP'yi ve LCP'yi düşürür, ama SSR her istekte render ettiği için backend'in yavaşlığını TTFB'ye ekler; çözüm kısa süreli Output Cache. Bookstore'un kitap sayfasında FCP 1,28 sn'den 0,3 sn'ye indi; hydrate on viewport ile geç hydrate olan bir bloğun transfer cache'i kaçırıp isteği tekrarladığını ve bunun event replay'i bozduğunu da ölçtük.

Sürüm notu: Bu yazıdaki örnekler Angular 22.2 (Angular 22.2'nin desteklediği TypeScript 6.0 ile, @angular/ssr 22.2.7), Express 5.3, Node.js 24, web-vitals 6.2.3, .NET 10 (ASP.NET Core 10) ve Playwright 1.65 ile denenmiştir. Bütün sayılar tek bir makinede, yerel build üzerinde, headless Chromium 158 ile alınmış lab ölçümleridir.

Önce Tahmin Et

Sitedeki serilerin ortak örnek uygulaması Bookstore'un (bir online kitapçı) kitap detay sayfası şimdiye kadar tarayıcıda render ediliyordu. Sunucunun gönderdiği HTML'de kitap yok; tarayıcı JavaScript'i indiriyor, çalıştırıyor, API'den kitabı istiyor ve kapağı ancak ondan sonra keşfediyor. Aynı sayfayı üç şekilde sunacağız: CSR (Client-Side Rendering; sayfanın tarayıcıda oluşturulması), SSR (Server-Side Rendering; her istekte sunucuda oluşturulması) ve SSG (Static Site Generation; build sırasında bir kez oluşturulup statik dosya olarak sunulması, Angular'da prerender). Tahmin et: telefon profilinde LCP (Largest Contentful Paint; en büyük içeriğin çizildiği an) hangisinde en iyi çıkar? API her istekte 300 ms gecikirse sıralama değişir mi?

Route Başına Render Modu

Angular 19 ile gelen server route'larıyla render modu route bazında seçiliyor. app.routes.server.ts dosyası, tarayıcıdaki route tablosunun sunucu tarafındaki karşılığı. Ana sayfa üçüncü yazıdan beri prerender ediliyordu; kitap sayfası için üç seçenek:

import { RenderMode, ServerRoute } from '@angular/ssr';

export const serverRoutes: ServerRoute[] = [
  { path: '', renderMode: RenderMode.Prerender },
  // SSR: her istek kitap sayfasını sunucuda render eder (2.000 kitap, fiyat ve stok değişiyor)
  { path: 'books/:isbn', renderMode: RenderMode.Server },
  { path: '**', renderMode: RenderMode.Client },
];
  {
    path: 'books/:isbn',
    renderMode: RenderMode.Prerender,
    // Kapağı olan 12 kitap build sırasında render edilir; diğer ISBN'ler istek anında render edilir
    async getPrerenderParams() {
      const api = process.env['BOOKSTORE_API'] ?? 'http://127.0.0.1:5300';
      const books: { isbn: string }[] = await fetch(`${api}/api/books?take=12`).then((r) => r.json());
      return books.map((b) => ({ isbn: b.isbn }));
    },
    fallback: PrerenderFallback.Server,
  },

RenderMode.Client boş bir HTML kabuğu gönderiyor. RenderMode.Server her istekte uygulamayı Node.js'te çalıştırıyor, API'yi sunucudan çağırıyor ve dolu HTML'i gönderiyor. RenderMode.Prerender build sırasında render ediyor; parametreli bir route için getPrerenderParams hangi ISBN'lerin (kitabın uluslararası standart numarası) üretileceğini söylüyor, listede olmayanlar için fallback: PrerenderFallback.Server istek anında SSR'a düşüyor. 2.000 kitabın hepsini her build'de üretmek, fiyat ve stok değiştikçe yeniden build almak demek; bu yüzden SSG burada yalnız en çok ziyaret edilen 12 kitap için mantıklı.

Kurulum: Node.js SSR Sunucusu, Önünde ASP.NET Core

SSR için angular.json'da outputMode'u "static"'ten "server"'a çevirdik. Angular CLI'ın şablonundaki Express sunucusunu iki değişiklikle kullanıyoruz: yalnız loopback arayüzünü dinliyor, çünkü dışarıya Bookstore.Api açık:

// Her sayfa isteği: app.routes.server.ts'teki RenderMode'a göre prerender HTML, sunucuda render ya da CSR kabuğu
app.use((req, res, next) => {
  angularApp
    .handle(req)
    .then((response) => (response ? writeResponseToNodeResponse(response, res) : next()))
    .catch(next);
});

if (isMainModule(import.meta.url) || process.env['pm_id']) {
  const port = Number(process.env['PORT'] || 4000);
  // Bu sunucuyla yalnız Bookstore.Api konuşur: yalnız loopback arayüzünü dinle
  app.listen(port, '127.0.0.1', (error) => {
    if (error) {
      throw error;
    }
    console.log(`Node Express server listening on http://127.0.0.1:${port}`);
  });
}

export const reqHandler = createNodeRequestHandler(app);
            "server": "src/main.server.ts",
            "outputMode": "server",
            "ssr": {
              "entry": "src/server.ts"
            },
            "security": {
              "allowedHosts": ["127.0.0.1", "localhost"]
            }

security.allowedHosts zorunlu: Angular SSR, SSRF'e (Server-Side Request Forgery; sunucuyu saldırganın seçtiği adrese istek atmaya zorlamak) karşı isteğin host'unu bu listeyle karşılaştırıyor ve listede olmayan host'a 400 dönüyor. Üretimde buraya sitenin gerçek domain'i yazılır.

ASP.NET Core tarafı tek origin olarak kalıyor: statik dosyaları, API'yi ve görselleri kendisi sunuyor, sayfa isteklerini Node.js sunucusuna iletiyor. Böylece tarayıcının ve SSR'ın API'ye attığı istekler aynı URL'i kullanıyor ve HTTP transfer cache'i (sunucuda alınan cevapların HTML'e gömülüp tarayıcıda yeniden kullanılması) eşleşiyor:

namespace Bookstore.Api.Ssr;

// Sayfaları Angular SSR sunucusu (Node.js, yalnız loopback) render eder; tek origin bu API olarak kalır
public static class SsrProxy
{
    public static async Task ForwardAsync(HttpContext context, IHttpClientFactory httpClientFactory)
    {
        if (!HttpMethods.IsGet(context.Request.Method) && !HttpMethods.IsHead(context.Request.Method))
        {
            context.Response.StatusCode = StatusCodes.Status405MethodNotAllowed;
            return;
        }

        HttpClient client = httpClientFactory.CreateClient("ssr");
        using HttpRequestMessage request = new(HttpMethod.Get, context.Request.Path + context.Request.QueryString);
        // Angular host'u security.allowedHosts ile karşılaştırır (SSRF koruması)
        request.Headers.Host = context.Request.Host.Value;
        request.Headers.TryAddWithoutValidation("Accept", context.Request.Headers.Accept.ToString());
        request.Headers.TryAddWithoutValidation("Accept-Language", context.Request.Headers.AcceptLanguage.ToString());
        // Cookie iletilmez: render edilen sayfa herkes için aynıdır ve cache'lenebilir

        using HttpResponseMessage response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, context.RequestAborted);
        context.Response.StatusCode = (int)response.StatusCode;
        context.Response.ContentType = response.Content.Headers.ContentType?.ToString();
        if (response.Headers.CacheControl is { } cacheControl)
        {
            context.Response.Headers.CacheControl = cacheControl.ToString();
        }
        await response.Content.CopyToAsync(context.Response.Body, context.RequestAborted);
    }
}
// Sayfaları render eden Angular SSR sunucusu (Node.js)
builder.Services.AddHttpClient("ssr", client =>
    client.BaseAddress = new Uri(builder.Configuration["Ssr:Origin"] ?? "http://127.0.0.1:5301"));
// Render edilen sayfalar herkes için aynı (cookie iletilmiyor): kısa bir süre sakla
int pageCacheSeconds = builder.Configuration.GetValue("Ssr:CacheSeconds", 60);
builder.Services.AddOutputCache(options =>
    options.AddPolicy("pages", policy => policy.Expire(TimeSpan.FromSeconds(pageCacheSeconds))));
app.UseRouting();
app.UseOutputCache();

app.MapCatalogEndpoints();
app.MapRumEndpoints();
app.MapCoverImageEndpoints();
// Diğer bütün sayfalar Angular SSR sunucusuna gider: route'a göre prerender, sunucuda render ya da CSR kabuğu
RouteHandlerBuilder pages = app.MapFallback(SsrProxy.ForwardAsync);
if (pageCacheSeconds > 0)
{
    pages.CacheOutput("pages");
}

Ölçüm için API'ye yalnız lab'da kullanılan bir gecikme ayarı da ekledik (Lab:ApiDelayMs): yavaş bir veritabanını ya da başka bir servisi bekleyen bir backend'i taklit ediyor.

Ölçüm: Üç Mod, İki Backend Hızı

Telefon profili (4x CPU, 150 ms gecikme, 1,6 Mbps), 5 koşunun ortancası, ms. TTFB (Time to First Byte; sunucudan ilk byte'ın gelme süresi) ve FCP (First Contentful Paint; ilk içeriğin çizildiği an) LCP'nin yanında:

ModAPI gecikmesiTTFBFCPLCP
CSR015112841880
CSR30015112842200
SSR01513001328
SSR3003114561476
SSR + Output Cache3001513001328
SSG (prerender)01513001324
SSG (prerender)3001513001320

FCP CSR'da 1,28 sn, SSR ve SSG'de 0,3 sn. LCP farkı daha küçük, çünkü LCP element'i 1200 px'lik kapak ve 1,6 Mbps'lik hatta inmesi yaklaşık 1,1 sn sürüyor; SSR yalnız kapağın keşfedilme anını öne çekiyor (load delay CSR'da 1321 ms, SSR'da 29 ms). Görselin kendisini küçültmek görsel ve font yazısının konusuydu.

CSR satırında bir sürpriz var: bir önceki yazıda lazy route yüzünden 2220 ms'ye çıkan LCP burada 1880 ms. Kitap sayfası hâlâ RenderMode.Client, ama HTML kabuğunu artık statik index.csr.html değil Angular'ın sunucu motoru veriyor ve route'un chunk'ını modulepreload olarak ekliyor:

resource                                                  start    end  render-blocking
/books/9781000000009                                          0    164  non-blocking
/chunk-CPg4KbJp.js                                          174    568  non-blocking
/chunk-JOppahxN.js                                          174    382  non-blocking
/main-CVA4GSYC.js                                           174   1033  non-blocking
/chunk-DLdUTTQE.js                                          174   1230  non-blocking
/styles-CYMCJ46A.css                                        177    337  non-blocking
/fonts/inter-latin-400-normal.woff2                         189    771  non-blocking
/fonts/inter-latin-700-normal.woff2                        1282   1591  non-blocking
/api/books/9781000000009                                   1306   1471  non-blocking
/api/books/9781000000009/similar?count=8                   1307   1476  non-blocking
/img/covers/dune?w=1200                                    1482   2227  non-blocking
analytics.thirdparty.test:5302/analytics.js                2229   2857  non-blocking
analytics.thirdparty.test:5302/collect                     2921   3180  non-blocking
FCP 1292 ms, LCP 2244 ms

chunk-JOppahxN.js kitap sayfasının chunk'ı; main.js ile aynı anda, 174. ms'de istenmiş. Bir önceki yazıdaki ekstra round trip böylece kapandı.

SSG'nin ilk ölçümü 312 ms TTFB ile SSR'dan kötü çıktı. Sebep ASP.NET Core'un UseDefaultFiles'ı: /books/9781000000009 bir klasöre denk geliyor ve middleware varsayılan olarak /books/9781000000009/'e 301 ile yönlendiriyor; telefon profilinde bu bir round trip daha. RedirectToAppendTrailingSlash = false ile klasördeki index.html doğrudan sunuluyor ve tablodaki 151 ms'ye iniyor:

// "/" -> index.html, build sırasında prerender edilen ana sayfa; "/books/{isbn}" -> books/{isbn}/index.html
// prerender edilen kitap sayfaları için, "/books/{isbn}/"e 301 yönlendirmesi olmadan (bir round trip eksik)
app.UseDefaultFiles(new DefaultFilesOptions { RedirectToAppendTrailingSlash = false });

SSR'ın TTFB Bedeli ve Output Cache

Backend 300 ms geciktiğinde CSR'da sayfanın kabuğu yine 151 ms'de geliyor, ama kitap verisi geç geldiği için LCP 320 ms kötüleşiyor. SSR'da ise bekleme HTML'in kendisine biniyor: sunucu iki API cevabını bekleyip render ediyor, TTFB ve FCP birlikte kayıyor. Bir lab detayına dikkat: curl ile ölçtüğümüzde SSR'ın sunucu süresi gecikmeyle 311 ms, ama telefon profilinde TTFB yine 311 ms. Chromium'un ağ emülasyonu 150 ms'lik gecikmeyi bu ölçümde sunucunun süresinin içinde saydı; gerçek bir ağda iki süre toplanır ve TTFB 460 ms civarında olurdu. SSR'ı lab'da değerlendirirken sunucu süresini ayrıca ölç.

SSR'ın TTFB bedelini düşürmenin en etkili yolu render edilmiş HTML'i cache'lemek. Kitap sayfası her kullanıcı için aynı (SsrProxy cookie iletmiyor), o yüzden Output Cache'te 60 saniye tutuyoruz. Tablodaki "SSR + Output Cache" satırında ilk koşu cache'i dolduruyor (TTFB 337 ms), sonraki dört koşu 151 ms; LCP, gecikmesiz SSR ile aynı. Aynı makinede, istemciyle sunucu yan yana koşarken, 8 paralel istekle 400 isteklik küçük bir yük testi:

http://127.0.0.1:5300/books/9781000000009  400 requests, 8 parallel
4025 req/s  p50 0.8 ms  p95 2.3 ms  p99 40.0 ms
http://127.0.0.1:5300/books/9781000000009  400 requests, 8 parallel
592 req/s  p50 12.6 ms  p95 17.8 ms  p99 33.9 ms
http://127.0.0.1:5301/books/9781000000009  400 requests, 8 parallel
572 req/s  p50 12.6 ms  p95 20.0 ms  p99 30.3 ms

İlk satır Output Cache açıkken ASP.NET üzerinden, ikincisi cache yokken ASP.NET üzerinden, üçüncüsü Node.js sunucusuna doğrudan. Node.js tek thread'de saniyede 570-590 sayfa render ediyor (sayfa başına yaklaşık 1,7 ms CPU); 8 paralel istekte bir isteğin ortanca süresi 12,6 ms, çünkü istekler sırayla bekliyor. Cache'ten saniyede 4.000 sayfa çıkıyor. Rakamlar tek makineli lab ölçümü, kapasite planı için değil, oran için anlamlı: SSR her istekte uygulamayı çalıştırıyor ve sunucuya istemcinin işini yüklüyor. Kullanıcıya özel bir sayfayı (sepet, siparişlerim) bu şekilde cache'leme; Output Cache'in varsayılan policy'si yalnız GET/HEAD ve 200 cevaplarını saklıyor, cookie set eden ya da authenticate olmuş isteklere verilen cevapları saklamıyor. Bu tür sayfalar CSR kalabilir ya da kullanıcıya özel kısım istemcide yüklenir. CDN (içeriği kullanıcıya yakın sunuculardan dağıtan ağ) ile cache'lemek ilerideki Domain, DNS ve CDN serisinde.

Hydration ve hydrate on viewport

SSR'ın HTML'i görünür ama tıklanamaz; Angular tarayıcıda uygulamayı başlatıp mevcut DOM'u (sayfanın tarayıcıdaki ağaç yapısı) yeniden kurmadan sahiplenene kadar event'ler çalışmıyor. Buna hydration deniyor. Angular 22'de provideClientHydration() incremental hydration'ı varsayılan olarak açıyor: @angular/platform-browser typing'lerinde withIncrementalHydration() "v22'den beri varsayılan" notuyla deprecated, kapatmak için withNoIncrementalHydration() var. Incremental hydration, @defer bloklarına bir hydrate trigger'ı ekleyince devreye giriyor.

CLS (beklenmedik layout kaymalarının toplam puanı) yazısındaki "Similar books" bloğu @defer (on viewport) idi. Angular sunucuda @defer bloklarının yalnız placeholder'ını render ediyor; SSR'ın HTML'inde blok hiç yok:

$ curl -s http://127.0.0.1:5300/books/9781000000009 | grep -c 'Similar books'
0

hydrate on viewport eklenince sunucu bloğun içeriğini de render ediyor, tarayıcı ise bloğun JavaScript'ini blok görünür olana kadar indirmiyor ve o ana kadar blok dehydrated (sunucudan geldiği gibi, Angular'a bağlanmamış) kalıyor:

        <!-- SSR bloğu HTML'e render eder; JavaScript'i blok görünür olunca yüklenir ve hydrate edilir -->
        @defer (on viewport; hydrate on viewport) {
          <app-similar-books [books]="similar.value()" />
        } @placeholder {
          <div></div>
        }
$ curl -s http://127.0.0.1:5300/books/9781000000009 | grep -c 'Similar books'
1

Transfer Cache, Uygulama Stable Olunca Kapanıyor

İlk denemede blok kendi verisini yine kendisi istiyordu (httpResource, CLS yazısındaki gibi). Telefon profilinde sayfayı açıp scroll ettiğimizde inen dosyalar ve API istekleri:

   659 ms  /chunk-CPg4KbJp.js                            11.6 kB
  1059 ms  /chunk-CzgvOFue.js                             1.5 kB
  1370 ms  /main-LUWISOPX.js                             50.2 kB
  1615 ms  /chunk-DLdUTTQE.js                            86.9 kB
  2314 ms  /analytics.js                                 76.1 kB
  3135 ms  -- load finished, waiting 3 s (browser idle)
  6135 ms  -- scroll 400 px
  6326 ms  /chunk-k-4-XG7_.js                             1.4 kB
  6510 ms  /api/books/9781000000009/similar?count=8       0.5 kB
  7641 ms  -- scroll 400 px
  9144 ms  -- scroll 400 px

Kitabın verisi için tarayıcıdan hiç istek yok, transfer cache çalışıyor. Ama blok scroll'da hydrate olurken /similar isteği bir kez daha API'ye gidiyor, oysa aynı veri sunucuda alınıp HTML'e gömülmüştü. Sebep Angular'ın kaynağında: HTTP transfer cache'i yalnız uygulama stable olana kadar cevap veriyor, scroll'da hydrate olan blok ise çok sonra çalışıyor. Angular 22'nin resource()'una gelen id seçeneğini (TransferState'e kendi anahtarıyla yazıyor) da denedik; değer HTML'e yazıldı, ama @angular/core'daki okuma da aynı "cache aktif mi" kontrolüne bağlı ve istek yine gitti. Çözüm, veriyi ilk render sırasında, eager olan kitap sayfasında almak ve bloğa input olarak vermek:

export class BookPage {
  readonly isbn = input.required<string>();
  protected readonly book = httpResource<Book>(() => `/api/books/${this.isbn()}`);
  // Kitapla birlikte, ilk render sırasında istenir: sunucuda, tarayıcıda ise
  // HTTP transfer cache'ten
  protected readonly similar = httpResource<Book[]>(() => `/api/books/${this.isbn()}/similar?count=8`);
}
export class SimilarBooks {
  // Kitap sayfası yükler: blok, uygulama stable olduktan sonra hydrate olur; o anda HTTP transfer cache
  // artık cevap vermez, burada atılan bir istek API'ye ikinci kez gider
  readonly books = input<Book[] | undefined>();
  // Önce 4 kart, kalan 4'ü "Show more" arkasında
  protected readonly showAll = signal(false);
  protected readonly skeleton = [1, 2, 3, 4];
  protected readonly visible = computed(() => (this.books() ?? []).slice(0, this.showAll() ? 8 : 4));
}
   664 ms  /chunk-CPg4KbJp.js                            11.6 kB
   903 ms  /chunk-JOppahxN.js                             1.5 kB
  1443 ms  /main-CVA4GSYC.js                             50.2 kB
  1601 ms  /chunk-DLdUTTQE.js                            86.9 kB
  2284 ms  /analytics.js                                 76.1 kB
  3106 ms  -- load finished, waiting 3 s (browser idle)
  6106 ms  -- scroll 400 px
  6302 ms  /chunk-DPGMTcbN.js                             1.4 kB
  7608 ms  -- scroll 400 px
  9110 ms  -- scroll 400 px

Scroll'da yalnız bloğun 1,4 KB'lık chunk'ı iniyor, API isteği yok.

Event Replay: Hydration'dan Önceki Tıklama

Kullanıcı, JavaScript inmeden önce sunucudan gelen "Show more" düğmesine tıklarsa ne olur? Angular SSR'ın HTML'ine küçük bir inline script koyuyor (event dispatch contract); bu script tıklamaları kaydediyor ve hydration bitince ilgili handler'a yeniden veriyor (event replay). Incremental hydration açıkken event replay de açık. Düşük seviye telefon profilinde, düğme HTML'de belirir belirmez tıklayan bir script ile iki sürümü denedik:

click on "Show more" at 429 ms (main.js downloaded: no)
after 15 s still 4 cards: the click was not replayed; JavaScript files finished at 1408, 1908, 3175, 3537, 4856, 4016 ms
click on "Show more" at 418 ms (main.js downloaded: no)
8 cards visible at 4061 ms; JavaScript files finished at 1459, 1959, 3176, 3552, 4034 ms

İlk sürümde tıklama kayboldu. Blok hydrate olurken kendi isteğini yeniden attığı için bir an verisiz kaldı, @if düğmeyi DOM'dan kaldırdı ve kaydedilen tıklamanın hedefi yok oldu. Son sürümde aynı tıklama 418. ms'de, main.js daha inmeden yapıldı; blok hydrate olunca (4034. ms'de inen chunk'tan hemen sonra) Angular tıklamayı yeniden oynattı ve sekiz kart göründü. Hydration'da sunucuyla tarayıcının aynı veriyle aynı DOM'u üretmesi, yalnız performans için değil doğruluk için de şart.

Üretimde Dikkat

SSR'ı açmak, sunucuda bir Node.js süreci daha çalıştırmak demek: süreç yönetimi, log, bellek sınırı ve sağlık kontrolü gerekiyor. Render süresini izle; ölçtüğümüz 1,7 ms'lik render, sayfa büyüdükçe ve API çağrıları arttıkça büyür ve doğrudan TTFB'ye eklenir. SSR'ın API çağrıları için bir timeout koy, yoksa yavaş bir servis bütün sayfaları bekletir. Output Cache'i deploy sırasında temizlemeyi de unutma: cache'teki HTML önceki build'in hash'li dosya adlarını içerir ve o dosyalar yeni deploy'da yoksa sayfa JavaScript'siz kalır. Ya cache'i deploy'da boşalt (tag ile EvictByTagAsync) ya da eski build'in dosyalarını bir süre daha sun.

Render modunu RUM (gerçek kullanıcı ölçümü) verisine bakarak seç. Ölçümde SSG ile SSR'ın cache'li hâli aynı sonucu verdi; aralarındaki fark build süresi ve verinin tazeliği. Sık değişen, çok sayfalı içerik için SSR ve kısa süreli cache, az sayıda ve nadiren değişen sayfa için SSG, kullanıcıya özel sayfa için CSR makul bir başlangıç. SSR'ın arama motorları tarafı (render edilmiş HTML, meta etiketleri, canonical) Teknik SEO (arama motoru optimizasyonu) serisinin 08. ve 09. yazılarında, güvenlik tarafı Frontend Güvenliği serisinin 07. yazısında.

Sık Düşülen Tuzaklar

  • SSR'da window, document ya da localStorage'a doğrudan erişmek. Sunucuda bu object'ler yok; üçüncü yazıda web-vitals'ın prerender sırasında verdiği "document is not defined" hatası buydu. isPlatformBrowser ya da afterNextRender kullan.
  • Hydration mismatch. Sunucuyla tarayıcının farklı DOM üretmesi (DOM'u elle değiştiren bir library, geçersiz HTML iç içeliği, tarayıcıda farklı veri) Angular'da NG0500 (hydration uyuşmazlığı hata kodu) serisi hatalara ve içeriğin yeniden kurulmasına yol açar.
  • Deferred bir bloğun kendi verisini kendisinin istemesi. Bloğun hydrate olduğu an transfer cache kapalıysa istek ikinci kez gider ve hydration sırasında içerik bir an boşalır.
  • Prerender edilen parametreli route'larda trailing slash yönlendirmesi; her ziyarette bir round trip.
  • SSR'ı her şey için açıp kullanıcıya özel sayfaları cache'lemek ya da hiçbir şeyi cache'lemeyip her isteği render etmek.
  • SSR'ı yalnız lab'da, emüle edilmiş ağla değerlendirmek. Sunucu süresini ayrıca ölç; emülasyon gecikmeyle sunucu süresini çakıştırabiliyor.

Küçük Bir Deneme

★☆☆ ssr-bench.mjs'i Lab:ApiDelayMs=300 ile, Output Cache kapalı ve açıkken çalıştır. Saniyedeki istek sayısı neden bu kadar düşüyor, sunucu CPU'su mu meşgul yoksa beklemede mi?

★★☆ Kitap sayfasındaki "Details" bölümünü @defer (on idle; hydrate never) içine al. Bu bölümde hiç etkileşim yok; inen JavaScript ve hydration süresi değişiyor mu? hydrate never bloğuna sonradan bir düğme eklersen ne olur?

★★★ app.routes.server.ts'te kitap sayfasına headers: { 'Cache-Control': 'public, max-age=60' } ver ve ASP.NET Core'daki Output Cache yerine tarayıcı cache'iyle aynı sayfayı ikinci kez ziyaret ederek ölç. Hangi durumda tarayıcı sunucuya hiç gitmiyor, hangi durumda 304 alıyor?

Kitap sayfasında FCP 1,28 sn'den 0,3 sn'ye, LCP 1,88 sn'den 1,33 sn'ye indi ve backend yavaşken bile cache'li SSR aynı kaldı. Bir sonraki yazıda bir sayfadan ötekine geçişe bakıyoruz: bfcache ile geri tuşunun anında çalışması, Speculation Rules ile bir sonraki sayfayı önceden hazırlamak, Angular Router'ın preloading'i ve Brotli seviyeleri, önceden sıkıştırılmış dosyalar ve zstd.