Navigasyonları Hızlandırmak: bfcache, Speculation Rules, Router Preloading ve Brotli/zstd Sıkıştırma
Sayfalar arası geçişi hızlandırmak için geri tuşunda bfcache'i engellememek, SPA içinde Router preloading, document navigation'larda Speculation Rules ve doğru sıkıştırma seviyesi gerekir. Bookstore'da uygulama içi geçiş 810 ms'den 207 ms'ye, katalogdaki liste 2,8 sn'den 1,6 sn'ye indi; Speculation Rules'un routerLink'li linklerde işe yaramadığını ve ASP.NET Core'un varsayılan Brotli seviyesinin JSON'u 160 KB gönderdiğini ö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), 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. Tarayıcı destek bilgileri MDN'in (Mozilla'nın web dokümantasyonu) browser-compat-data 8.1.9 sürümünden (2026-11-12).
Önce Tahmin Et
Şimdiye kadar hep bir sayfanın ilk yüklenmesine baktık. Kullanıcı ise sayfalar arasında dolaşıyor: sitedeki serilerin ortak örnek uygulaması Bookstore'da (bir online kitapçı) ana sayfadan bir kitaba geçiyor, geri dönüyor, kataloğa gidiyor. Ana sayfadaki "View details" linkine tıklayınca kitap başlığının ekrana gelmesi telefon profilinde 810 ms sürüyor. Tahmin et: bu süreyi en çok hangisi kısaltır: kitap sayfasını Speculation Rules ile önceden prerender etmek mi, Angular Router'ın preloading'i mi, yoksa sunucudaki sıkıştırmayı iyileştirmek mi?
bfcache: Geri Tuşu İçin Bedava Hız
bfcache (back/forward cache; geri ve ileri tuşları için tutulan sayfa cache'i), kullanıcı başka bir sayfaya gittiğinde önceki sayfayı JavaScript state'iyle birlikte bellekte dondurup geri tuşunda olduğu gibi geri getiriyor; ağ isteği, parse ve render yok. Bu yalnız document navigation'larda (tam sayfa yüklemelerinde) çalışıyor; Angular Router'ın SPA (Single Page Application; sayfayı yeniden yüklemeden route değiştiren uygulama) içi geçişleri zaten aynı document içinde kaldığı için bfcache'i kullanmıyor. SSR (sunucuda render) ile gelen kitap sayfasından katalog sayfasına tam bir navigation yapıp geri dönen bir lab script'i yazdık (bfcache.mjs). Playwright Chromium'u varsayılan olarak --disable-back-forward-cache ile başlatıyor ve varsayılan headless shell'de bfcache hiç yok; bu ölçüm için Playwright'ın tam Chromium build'ini new headless modda ve bu bayrak olmadan açıyoruz:
// bfcache/prerender lab'ları: Playwright Chromium'u --disable-back-forward-cache ile başlatır ve varsayılan
// headless shell'de bfcache yoktur; 'full' bunun yerine Playwright'ın tam Chromium build'ini new headless modda açar
export function launch({ full = false } = {}) {
return full
? chromium.launch({ channel: 'chromium', args: chromiumArgs, ignoreDefaultArgs: ['--disable-back-forward-cache'] })
: chromium.launch({ args: chromiumArgs });
}
await page.goto(url, { waitUntil: 'networkidle', timeout: 120_000 });
await page.evaluate(() => (window.__marker = 'same page object'));
await page.goto(nextUrl, { waitUntil: 'networkidle', timeout: 120_000 });
// --cookie: sonraki sayfa bir cookie değiştirir (ör. sepete kitap ekler)
if (values.cookie) await page.evaluate(() => (document.cookie = 'cart=9781000000009; path=/'));
const started = Date.now();
await page.goBack({ waitUntil: 'commit' });
await page.locator('app-book-page h1').waitFor();
const ms = Date.now() - started;
const restored = (await page.evaluate(() => window.__marker)) === 'same page object';
$ node bfcache.mjs http://127.0.0.1:5300/books/9781000000009 http://127.0.0.1:5300/books
back to /books/9781000000009: restored from bfcache, book title visible after 50 ms
RUM reports: FCP:navigate TTFB:navigate CLS:navigate LCP:navigate TTFB:back-forward-cache FCP:back-forward-cache LCP:back-forward-cache INP:back-forward-cache CLS:back-forward-cache
$ node bfcache.mjs http://127.0.0.1:5300/books/9781000000009 http://127.0.0.1:5300/books --no-store
back to /books/9781000000009: restored from bfcache, book title visible after 40 ms
RUM reports: FCP:navigate TTFB:navigate CLS:navigate LCP:navigate TTFB:back-forward-cache FCP:back-forward-cache LCP:back-forward-cache INP:back-forward-cache CLS:back-forward-cache
$ node bfcache.mjs http://127.0.0.1:5300/books/9781000000009 http://127.0.0.1:5300/books --no-store --cookie
back to /books/9781000000009: loaded again (a new page load)
not restored because (CDP): CacheControlNoStoreCookieModified
notRestoredReasons: [{"reason":"response-cache-control-no-store"}]
$ node bfcache.mjs http://127.0.0.1:5300/books/9781000000009 http://127.0.0.1:5300/books --unload
back to /books/9781000000009: restored from bfcache, book title visible after 38 ms
RUM reports: FCP:navigate TTFB:navigate CLS:navigate LCP:navigate TTFB:back-forward-cache FCP:back-forward-cache LCP:back-forward-cache INP:back-forward-cache CLS:back-forward-cache
Kitap sayfası her durumda bfcache'ten 40-50 ms'de geldi. İki sonuç önemli. Birincisi unload: sayfaya boş bir unload listener'ı eklemek eskiden bfcache'i kapatırdı; web.dev'e göre Chrome bu event'i artık çalıştırmıyor ve sayfa geri yüklendi. Firefox'ta unload listener'ı hâlâ engel, bu yüzden pagehide kullan. İkincisi Cache-Control: no-store: Chrome'un dokümanına göre Chrome, no-store sayfaları da sınırlı koşullarla bfcache'e alıyor (2025'te bütün kullanıcılara açıldı); ama arada bir cookie değişirse sayfa geri yüklenmiyor. Üçüncü koşu bunu gösteriyor: kitap sepete eklenmiş gibi bir cookie yazınca sayfa yeniden yüklendi. Diğer tarayıcılar no-store sayfaları hâlâ dışarıda bırakabilir; no-store'u yalnız gerçekten hassas sayfalarda kullan.
Sayfanın neden geri yüklenmediğini RUM (gerçek kullanıcı ölçümü) ile de öğrenebilirsin: Chromium'da yeni yüklemenin navigation entry'sindeki notRestoredReasons alanı sebebi veriyor (browser-compat-data'ya göre Chrome 125+, Firefox ve Safari'de yok). web-vitals da bfcache'ten dönen sayfa için metrikleri yeniden raporluyor; ilk yazıdaki rum.ts navigationType'ı zaten gönderdiği için geri yüklenen ziyaretler back-forward-cache olarak ayrı görünüyor. Bunları ayrı tutmak gerekiyor, yoksa bfcache'in 40 ms'lik geri dönüşleri p75'i olduğundan iyi gösterir. Geri gelen sayfanın verisi eskimiş olabilir (stok, fiyat); pageshow event'inde event.persisted doğruysa veriyi tazele.
Speculation Rules ve SPA'lar
Speculation Rules, tarayıcıya kullanıcının gideceği sayfayı önceden hazırlamasını söylüyor: prefetch yalnız HTML'i indiriyor, prerender sayfayı görünmez bir sekmede baştan sona render ediyor ve tıklamada anında gösteriyor. eagerness ne zaman başlanacağını belirliyor: immediate hemen, moderate masaüstünde link üzerinde yaklaşık 200 ms beklenince, conservative tıklama başlarken. browser-compat-data'ya göre speculationrules Chrome 109'dan (prefetch 110'dan) beri var, prerender yalnız Chromium'da; Safari 27.3 beta'sında prefetch geliyor, Firefox'ta yalnız önizleme sürümünde, kısmen. Bookstore'un HTML şablonuna kitap sayfaları için bir kural ekledik:
<!-- Kullanıcı bir kitap linkinin üzerine gelince o sayfayı prerender et (Chromium; diğerleri yok sayar) -->
<script type="speculationrules">
{
"prerender": [{ "where": { "href_matches": "/books/*" }, "eagerness": "moderate" }]
}
</script>
$ node speculation.mjs http://127.0.0.1:5300/
speculation: prefetch Running (PrefetchNotFinishedInTime), prerender Failure (PrerenderingDisabledByDevTools), prefetch Ready (PrefetchSuccessfulButNotUsed)
click -> book title after 226 ms, same document (Angular router navigation)
$ node speculation.mjs http://127.0.0.1:5300/ --plain
speculation: prefetch Running (PrefetchNotFinishedInTime), prerender Failure (PrerenderingDisabledByDevTools), prefetch Ready (PrefetchSuccessfulButNotUsed), prefetch Success (PrefetchResponseUsed)
click -> book title after 97 ms, new document: delivery navigational-prefetch, TTFB 2 ms
$ node speculation.mjs http://127.0.0.1:5300/ --plain --no-rules
speculation: none
click -> book title after 250 ms, new document: delivery (network), TTFB 152 ms
Üç sonuç var. Birincisi, Angular'ın routerLink'iyle tıklanan bir link document navigation değil: Router tıklamayı yakalayıp aynı document içinde geçiş yapıyor ve önceden hazırlanan sayfa kullanılmıyor (PrefetchSuccessfulButNotUsed). SPA'da Speculation Rules, uygulamanın dışından gelen ya da uygulamalar arası linkler (ör. pazarlama sayfasından kitap sayfasına), routerLink olmayan düz <a href>'ler ve SSR'lı, çok sayfalı (MPA; her linkin yeni bir HTML sayfası yüklediği) siteler için anlamlı. İkincisi, düz bir link için prefetch edilen HTML kullanıldı: TTFB (Time to First Byte; ilk byte'ın gelme süresi) 152 ms'den 2 ms'ye, tıklamadan kitap başlığına kadar geçen süre 250 ms'den 97 ms'ye indi. Üçüncüsü, lab'ın sınırı: Chromium, otomasyon (DevTools protokolü) bağlıyken prerender'ı kapatıyor (PrerenderingDisabledByDevTools) ve yerine prefetch edilen cevabı kullanıyor. Prerender'ın kendisini Playwright ile ölçemedik; DevTools'un Application panelinde, Speculative loads bölümünde elle denenebilir.
Prerender'ın tuzağı yan etkiler: prerender edilen sayfa JavaScript'ini çalıştırıyor ve load event'i kullanıcı sayfayı görmeden tetikleniyor. Bizim analytics yükleyicimiz load'da çalışıyordu, yani kullanıcı hiç tıklamasa da bir sayfa görüntülemesi sayardı. document.prerendering doğruysa yüklemeyi prerenderingchange event'ine bırakıyoruz:
<script>
// Analytics load event'inden sonra: artık main.js ile bant genişliği için yarışmıyor
const loadAnalytics = () => {
const script = document.createElement('script');
script.src = 'http://analytics.thirdparty.test:5302/analytics.js';
document.head.append(script);
};
// Prerender edilen sayfa da kullanıcı görmeden "load" tetikler: görüntülemeyi aktivasyonda say
if (document.prerendering) {
document.addEventListener('prerenderingchange', () => loadAnalytics(), { once: true });
} else {
addEventListener('load', loadAnalytics);
}
</script>
Prerender lab'da çalışmadığı için bu dalın gerçek bir prerender'da çalıştığını göremedik; normal yüklemedeki dalı doğruladık. web-vitals prerender edilen sayfalarda metrikleri activationStart'tan itibaren ölçüyor ve navigationType'ı prerender olarak veriyor. Sepete ekleme, oturum açma gibi bir şey değiştiren URL'leri kurallara koyma; moderate ya da conservative eagerness ile başla ve sunucu yükünü izle.
Router Preloading: SPA İçindeki Geçişler
SPA içindeki geçişleri hızlandıran araç Angular Router'ın preloading'i. JavaScript maliyeti yazısında kitap sayfasını lazy route yapmıştık; ana sayfadan bir kitaba tıklayınca önce o chunk iniyor, sonra API istekleri gidiyor. PreloadAllModules bütün lazy route'ları indirir, buna back office'in rapor sayfası da dahil. Bunun yerine yalnız işaretli route'ları yükleyen küçük bir strateji yazdık:
import { Injectable } from '@angular/core';
import { PreloadingStrategy, Route } from '@angular/router';
import { EMPTY, Observable } from 'rxjs';
// Yalnız data: { preload: true } ile işaretli lazy route'ları, ilk navigation'dan sonra önceden yükler;
// back office (admin) müşterilere hiç indirilmez
@Injectable({ providedIn: 'root' })
export class FlaggedPreloading implements PreloadingStrategy {
preload(route: Route, load: () => Observable<unknown>): Observable<unknown> {
return route.data?.['preload'] ? load() : EMPTY;
}
}
{ path: 'books', loadComponent: () => import('./catalog/catalog-page').then((m) => m.CatalogPage), title: 'Catalog - Bookstore', data: { preload: true } },
{ path: 'books/:isbn', loadComponent: () => import('./catalog/book-page').then((m) => m.BookPage), title: 'Book - Bookstore', data: { preload: true } },
provideRouter(routes, withComponentInputBinding(), withPreloading(FlaggedPreloading)),
click -> book title after 835 ms, same document (Angular router navigation)
click -> book title after 802 ms, same document (Angular router navigation)
click -> book title after 814 ms, same document (Angular router navigation)
click -> book title after 199 ms, same document (Angular router navigation)
click -> book title after 209 ms, same document (Angular router navigation)
click -> book title after 207 ms, same document (Angular router navigation)
Üç ölçümde tıklamadan kitap başlığına kadar geçen süre 802-835 ms'den 199-209 ms'ye indi. Tıklamadan sonraki istekleri nav-requests.mjs ile logladık; önce preloading olmadan, sonra preloading ile:
req 129 /chunk-JOppahxN.js
done 259 /chunk-JOppahxN.js
req 272 /api/books/9781000000009
req 272 /api/books/9781000000009/similar?count=8
done 438 /api/books/9781000000009
done 447 /api/books/9781000000009/similar?count=8
title 919
req 91 /api/books/9781000000009
req 91 /api/books/9781000000009/similar?count=8
done 250 /api/books/9781000000009
done 258 /api/books/9781000000009/similar?count=8
title 270
Preloading olmadan API istekleri ancak chunk indikten sonra, 272. ms'de başlıyor; preloading ile 91. ms'de. Bu bir round trip. Ama farkın büyüğü API cevaplarından sonra: preloading'siz sürümde başlık, cevaplar geldikten yaklaşık 470 ms sonra görünüyor, preloading'li sürümde 12 ms sonra. Bu sürenin içini bu yazıda ayrıştırmadık; ölçülen sonuç, chunk'ın tıklamadan önce hazır olmasının ilk render'ı da belirgin şekilde hızlandırdığı. Preloading ilk yüklemeyle yarışmıyor: Router chunk'ları ilk navigation bittikten sonra istiyor. Daha seçici bir strateji linkin görünür ya da üzerine gelinmiş olmasına göre yükleyebilir; Speculation Rules'un moderate mantığının SPA'daki karşılığı bu.
Sıkıştırma: Brotli Seviyeleri ve zstd
İkinci yazıda ASP.NET Core'un response compression'ını açmıştık ve şöyle demiştik: Brotli'nin varsayılan seviyesi en hızlısı. Typing'lerdeki dokümana göre BrotliCompressionProviderOptions.Level'ın varsayılanı CompressionLevel.Fastest; dotnet/runtime kaynağında bu Brotli kalitesi 1'e, Optimal 4'e, SmallestSize 11'e karşılık geliyor. Bookstore'un gerçek cevaplarıyla seviyeleri karşılaştıran küçük bir script yazdık (Node.js'in zlib'i aynı Brotli ve zstd library'lerini kullanıyor, süreler 5 koşunun ortancası):
response raw gzip 6 br 1 br 4 br 11 zstd 3 zstd 19
chunk-C01pusRY.js 205.6 kB 68.4 kB 2.9ms 78.0 kB 0.6ms 69.8 kB 1.4ms 60.2 kB 182.4ms 71.7 kB 0.6ms 63.1 kB 24.3ms
book page (SSR HTML) 12.9 kB 3.8 kB 0.1ms 4.2 kB 0.0ms 3.7 kB 0.1ms 3.1 kB 9.2ms 3.9 kB 0.0ms 3.7 kB 1.6ms
/api/books (JSON) 534.8 kB 36.6 kB 1.5ms 58.1 kB 0.4ms 36.2 kB 0.8ms 13.2 kB 561.8ms 41.2 kB 0.3ms 14.7 kB 121.8ms
Tablo iki şeyi gösteriyor. Dinamik cevaplar için kalite 4, kalite 1'den çok daha küçük ve hâlâ ucuz: 535 KB'lık kitap listesi 58 KB yerine 36 KB, 1 ms'nin altında. Kalite 11 ise statik dosyalar için: aynı JSON 13 KB'a iniyor ama yarım saniye sürüyor; her istekte değil, build sırasında bir kez yapılmalı. zstd hızlı seviyelerde Brotli 1'den küçük, Brotli 4 kadar küçük değil; en yüksek seviyede de bu dosyalarda Brotli 11'in gerisinde. zstd browser-compat-data'ya göre Chrome 123, Firefox 126 ve Safari 26.3'ten beri her motorda var. ASP.NET Core 10'un response compression'ında zstd provider'ı yok; .NET 11'in ASP.NET Core referans paketinde ZstandardCompressionProvider var.
Gerçek cevaplarda fark tablodakinden de büyük çıktı. ASP.NET Core Fastest ile /api/books'u ağda 160,6 KB gönderiyordu, tek seferde sıkıştırılan kalite 1'in (58 KB) neredeyse üç katı: JSON akış hâlinde parça parça yazılınca en hızlı seviye çok daha kötü sıkıştırıyor. Değişiklikler:
// Dinamik cevaplar (JSON, sunucuda render edilen HTML) her istekte sıkıştırılır; statik dosyalar önceden sıkıştırılmış gelir
builder.Services.AddResponseCompression(options =>
{
// HTTPS'te varsayılan olarak kapalı (BREACH): bu cevaplarda kullanıcı girdisinin yanında gizli değer yok
options.EnableForHttps = true;
options.Providers.Add<BrotliCompressionProvider>();
options.Providers.Add<GzipCompressionProvider>();
});
// Varsayılan Fastest (Brotli kalitesi 1); Optimal kalite 4: az CPU ile çok daha küçük JSON
builder.Services.Configure<BrotliCompressionProviderOptions>(options => options.Level = CompressionLevel.Optimal);
// `ng build`'den sonra çalışır: tarayıcı build'indeki her metin dosyasının yanına Brotli (kalite 11) kopyası yazar.
// Bookstore.Api her istekte sıkıştırmak yerine bunları sunar. zstd seviye 19, bu build'in her dosyasında
// Brotli 11'den büyük çıktı (lab/compress-sizes.mjs), bu yüzden .zst kopyası yok.
import { readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
import zlib from 'node:zlib';
const root = 'dist/bookstore-web/browser';
const { BROTLI_PARAM_QUALITY, BROTLI_PARAM_MODE, BROTLI_MODE_TEXT } = zlib.constants;
const files = readdirSync(root, { recursive: true }).map(String).filter((f) => /\.(js|css|html|txt|svg|json)$/.test(f));
let raw = 0;
let br = 0;
for (const file of files) {
const path = join(root, file);
const input = readFileSync(path);
const brotli = zlib.brotliCompressSync(input, { params: { [BROTLI_PARAM_QUALITY]: 11, [BROTLI_PARAM_MODE]: BROTLI_MODE_TEXT } });
// Daha küçük olmayan sıkıştırılmış kopya işe yaramaz (çok küçük dosyalar)
if (brotli.length < input.length) writeFileSync(path + '.br', brotli);
raw += statSync(path).size;
br += Math.min(brotli.length, input.length);
}
console.log(`precompressed ${files.length} files: ${(raw / 1000).toFixed(1)} kB -> br ${(br / 1000).toFixed(1)} kB`);
using System.Text.RegularExpressions;
using Microsoft.AspNetCore.StaticFiles;
using Microsoft.Extensions.FileProviders;
using Microsoft.Net.Http.Headers;
namespace Bookstore.Api.Compression;
// main-ABC123.js.br'yi sunar (build sırasında bookstore-web/tools/precompress.mjs yazar)
// aynı dosyayı her istekte yeniden sıkıştırmak yerine
public static partial class PrecompressedFiles
{
// Bu build'de Brotli 11, zstd 19'dan küçüktü; bir ("zstd", ".zst") girdisi de buraya eklenebilir
private static readonly (string Encoding, string Extension)[] Encodings = [("br", ".br")];
private static readonly FileExtensionContentTypeProvider ContentTypes = new();
// UseStaticFiles'tan önce: tarayıcının kabul ettiği önceden sıkıştırılmış kopyayı seçer
public static IApplicationBuilder UsePrecompressedFiles(this IApplicationBuilder app, IFileProvider files) =>
app.Use(async (context, next) =>
{
string path = context.Request.Path.Value ?? "";
string acceptEncoding = context.Request.Headers.AcceptEncoding.ToString();
if (HttpMethods.IsGet(context.Request.Method) && ContentTypes.TryGetContentType(path, out _))
{
foreach ((string encoding, string extension) in Encodings)
{
// Basitleştirilmiş Accept-Encoding kontrolü (q-value yok), tarayıcılar için yeterli
if (acceptEncoding.Contains(encoding, StringComparison.OrdinalIgnoreCase) && files.GetFileInfo(path + extension).Exists)
{
context.Request.Path = path + extension;
break;
}
}
}
await next();
});
public static StaticFileOptions StaticFileOptions()
{
FileExtensionContentTypeProvider contentTypes = new();
contentTypes.Mappings[".br"] = "application/octet-stream";
return new StaticFileOptions
{
ContentTypeProvider = contentTypes,
// Body yazılmadan önce çalışır; response compression Content-Encoding'i görür ve dosyayı atlar
OnPrepareResponse = context =>
{
string name = context.File.Name;
IHeaderDictionary headers = context.Context.Response.Headers;
foreach ((string encoding, string extension) in Encodings)
{
if (name.EndsWith(extension, StringComparison.Ordinal))
{
name = name[..^extension.Length];
headers.ContentEncoding = encoding;
headers.Vary = HeaderNames.AcceptEncoding;
context.Context.Response.ContentType = ContentTypes.TryGetContentType(name, out string? type) ? type : "application/octet-stream";
}
}
// main-PL3PRF3A.js: ad içerikle değişir, dosyanın hiç yeniden doğrulanması gerekmez
headers.CacheControl = HashedName().IsMatch(name)
? "public, max-age=31536000, immutable"
: name.EndsWith(".html", StringComparison.Ordinal) ? "no-cache" : headers.CacheControl;
},
};
}
[GeneratedRegex(@"-[A-Za-z0-9_-]{8}\.(js|css)$")]
private static partial Regex HashedName();
}
app.UseResponseCompression();
// "/" -> 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 });
app.UsePrecompressedFiles(app.Environment.WebRootFileProvider);
app.UseStaticFiles(PrecompressedFiles.StaticFileOptions());
package.json'daki build script'i artık ng build && node tools/precompress.mjs. Önce zstd'yi de (seviye 19) önceden sıkıştırıp tercih ettik; katalog sayfasının toplamı 210,3 KB çıktı, Brotli 11 ile 203,9 KB. Bu build'de Brotli her dosyada daha küçük olduğu için yalnız .br kopyaları bıraktık. EnableForHttps'in varsayılanı false: HTTPS üzerinde sıkıştırma, gizli bir değerle kullanıcının kontrol ettiği bir metin aynı cevapta olduğunda BREACH (sıkıştırılmış boyuttan gizli değeri tahmin eden saldırı) saldırısına kapı açıyor. Bookstore'un sayfalarında ve JSON'larında token ya da CSRF (siteler arası istek sahteciliği) anahtarı yok; senin uygulamanda varsa bu cevapları sıkıştırma ya da gizli değeri her istekte maskele.
response enc wire(kB) decoded(kB) cache-control
/books br 1.4 2.1 -
/chunk-CPg4KbJp.js br 11.9 27.8 -
/chunk-CTSCdMNo.js br 11.0 28.6 -
/main-CVA4GSYC.js br 50.5 129.9 -
/chunk-DLdUTTQE.js br 87.0 205.6 -
/styles-CYMCJ46A.css br 0.9 1.6 -
/fonts/inter-latin-400-normal.woff2 - 24.0 23.7 -
/fonts/inter-latin-700-normal.woff2 - 24.7 24.4 -
/api/books br 160.6 534.8 -
bytes on the wire: 371.8 kB in 9 requests
response enc wire(kB) decoded(kB) cache-control
/books br 1.4 2.7 -
/chunk-C-tuJ80r.js br 8.9 27.8 public, max-age=31536000, immutable
/main-PL3PRF3A.js br 36.0 131.8 public, max-age=31536000, immutable
/chunk-C01pusRY.js br 60.5 205.6 public, max-age=31536000, immutable
/chunk-DWwqIwo2.js br 8.1 28.6 public, max-age=31536000, immutable
/styles-CYMCJ46A.css br 0.7 1.6 public, max-age=31536000, immutable
/fonts/inter-latin-400-normal.woff2 - 24.0 23.7 -
/fonts/inter-latin-700-normal.woff2 - 24.7 24.4 -
/chunk-CxcuijdK.js br 1.5 2.9 public, max-age=31536000, immutable
/api/books br 38.0 534.8 -
bytes on the wire: 203.9 kB in 10 requests
| Katalog sayfası (telefon profili, ms) | Önce | Sonra |
|---|---|---|
| Ağdaki toplam (KB) | 371,8 | 203,9 |
| FCP (ilk içeriğin çizildiği an) | 1332 | 1096 |
LCP (h1) | 1380 | 1148 |
| İlk kitap satırı görünür | 2816 | 1604 |
Kitap listesinin ekrana gelmesi 1,2 sn erkene çekildi; bu sayfada LCP element'i h1 olduğu için LCP kazancı bunun yalnız bir kısmını gösteriyor. Ana sayfanın LCP'si değişmedi (1444 ve 1452 ms): orada LCP, sıkıştırılmayan bir AVIF (modern, yüksek sıkıştırmalı görsel formatı) görselinin inme süresine bağlı.
Hash'li Dosyalara immutable
Yukarıdaki OnPrepareResponse, adı içeriğiyle değişen dosyalara (main-PL3PRF3A.js) Cache-Control: public, max-age=31536000, immutable veriyor: dosya bir yıl taze. immutable ek olarak Firefox ve Safari'ye dosyayı yenile tuşunda bile doğrulamamasını söylüyor; browser-compat-data'ya göre Chrome bu direktifi desteklemiyor, orada iş max-age'te. HTML ise no-cache: her seferinde sunucuya sorulmalı, çünkü yeni build'in hash'li dosya adlarını o taşıyor. İkinci ziyarette yalnız sayfa ve API istekleri ağa çıkıyor:
response enc wire(kB) decoded(kB) cache-control
/books br 1.4 2.7 -
/chunk-C-tuJ80r.js br cache 27.8 public, max-age=31536000, immutable
/chunk-C01pusRY.js br cache 205.6 public, max-age=31536000, immutable
/main-PL3PRF3A.js br cache 131.8 public, max-age=31536000, immutable
/chunk-DWwqIwo2.js br cache 28.6 public, max-age=31536000, immutable
/styles-CYMCJ46A.css br cache 1.6 public, max-age=31536000, immutable
/fonts/inter-latin-400-normal.woff2 - cache 23.7 -
/fonts/inter-latin-700-normal.woff2 - cache 24.4 -
/chunk-CxcuijdK.js br cache 2.9 public, max-age=31536000, immutable
/api/books br 38.0 534.8 -
bytes on the wire: 39.5 kB in 2 requests
Dürüst olmak gerekirse aynı ikinci ziyaret, Cache-Control olmadan da cache'ten geldi: tarayıcı başlık yoksa Last-Modified'a bakarak tahmini bir tazelik süresi hesaplıyor. Bu tahmin dosyanın yaşına bağlı, deploy'dan hemen sonra çok kısa; açık bir immutable sonucu tahmine bırakmıyor. HTTP caching'in ayrıntıları Web Server'lar ve Hosting serisinin 06. yazısında.
Üretimde Dikkat
Ölçtüğün geçişleri RUM'da ayrı tut: navigationType değerleri navigate, reload, back-forward, back-forward-cache ve prerender farklı deneyimler; hepsini tek bir p75'te toplamak hem bfcache'in hem prerender'ın etkisini gizler. SPA'daki route geçişlerinin kendisi bugünkü stabil Core Web Vitals'ta görünmüyor; ilk yazıda konuştuğumuz soft navigation desteği bunu ölçülebilir hale getirecek.
Speculation Rules sunucuya ekstra istek demek: moderate eagerness ile her hover bir SSR render'ı tetikleyebilir. Sec-Purpose başlığı (prefetch;prerender) spekülatif istekleri loglarda ve metriklerde ayırmanı sağlıyor. Sıkıştırmada da önceden sıkıştırılmış dosyaların deploy'a girdiğini kontrol et: .br dosyası eksikse middleware sessizce ham dosyaya ve dinamik sıkıştırmaya düşüyor; ölçmeden fark etmezsin. Compression dictionary transport (önceki sürümün dosyasını sözlük olarak kullanıp yalnız farkı göndermek) şimdilik yalnız Chromium'da (browser-compat-data'ya göre Chrome 130); sık deploy edilen büyük bundle'larda ilgiye değer, bu yazıda denemedik.
Sık Düşülen Tuzaklar
- Speculation Rules'u bir Angular SPA'ya ekleyip
routerLink'li linklerin de hızlanacağını varsaymak; Router tıklamayı yakalıyor ve hazırlanan sayfa çöpe gidiyor. - Prerender edilen sayfada analytics'in ve diğer yan etkilerin (sepet, sayaç) çalışması;
document.prerenderingkontrolü yoksa görüntüleme iki kez sayılır. - bfcache'ten dönen sayfadaki eski veriyi tazelememek;
pageshowveevent.persistedkullan. unloadve gereksizCache-Control: no-store; Chrome'da artık çoğu zaman engel değil, diğer tarayıcılarda hâlâ engel.PreloadAllModulesile back office'i de her müşteriye indirmek.- Response compression'ı varsayılan
Fastestseviyede bırakmak ya da statik dosyaları her istekte yeniden sıkıştırmak. - HTTPS'te
EnableForHttps'i, cevapta gizli değer olup olmadığına bakmadan açmak.
Küçük Bir Deneme
★☆☆ bfcache.mjs'i kitap sayfasında bir WebSocket ya da açık bir fetch bırakacak şekilde değiştir (ör. sayfa kapanırken bitmeyen bir istek). CDP'nin (Chrome DevTools Protocol; tarayıcıyı programla yöneten protokol) verdiği sebep ve notRestoredReasons ne diyor?
★★☆ FlaggedPreloading'i, linki viewport'a giren route'ları yükleyecek şekilde genişlet (her routerLink için bir IntersectionObserver). Ana sayfa, katalog ve kitap sayfası arasında geçişleri speculation.mjs --no-rules ile ölç ve indirilen chunk sayısını chunks.mjs ile karşılaştır.
★★★ Bookstore.Api'yi .NET 11 SDK ile derle, ZstandardCompressionProvider'ı ekle ve /api/books için zstd ile Brotli Optimal'i ağdaki boyut ve sunucu CPU süresiyle karşılaştır.
Uygulama içi geçiş 810 ms'den 207 ms'ye, katalogdaki kitap listesi 2,8 sn'den 1,6 sn'ye indi ve geri tuşu bfcache ile 40 ms. Serinin son yazısında 2020'den bugüne Core Web Vitals'ta ve Angular'ın performans araçlarında neyin değiştiğini derliyoruz ve eski bir Angular projesinde ilk bakılacak beş kalemi çıkarıyoruz.