Network Paneli: Waterfall, Timing, Throttling, HAR ve Request Override'ları
Network panelinde bir isteğin Timing sekmesi, yavaşlığın ağda mı sunucuda mı olduğunu söyler: 800 ms bekleyen endpoint'te 803,9 ms'nin 803,1'i Waiting (TTFB). Angular 22 HttpClient'ının preflight'ı, CORS hatasını okumak, filtre syntax'ı, throttling, request blocking, Copy as cURL, Edit and resend, Local Overrides ile mock, sanitized HAR ve .NET 10 Server-Sent Events akışı, Chrome 156'dan gerçek çıktılarla.
Sürüm notu: Bu yazıdaki örnekler Chrome 156 (156.0.8078.80), Angular 22.2 ve .NET 10 (ASP.NET Core 10) ile denenmiştir. Network çıktıları aynı Chrome sürümünde, headless modda Chrome DevTools Protocol (CDP) üzerinden, DevTools'un Network panelini besleyen event'lerden alındı; panel adımları Chrome DevTools dokümanından aktarıldı.
Önce Tahmin Et
Bookstore'un Angular uygulaması http://localhost:4391'de, API (Application Programming Interface) http://localhost:5391'de çalışıyor. Kitap listesi açılırken BookService tek bir GET /api/books isteği atıyor, bir interceptor da her isteğe Authorization header'ı ekliyor:
export const authInterceptor: HttpInterceptorFn = (req, next) => {
if (!req.url.includes('/api/')) {
return next(req);
}
return next(req.clone({ setHeaders: { Authorization: 'Bearer demo-token-42' } }));
};
Network panelinde /api/books filtresiyle kaç satır görürsün? Ve 800 ms bekleyen bir endpoint için Timing sekmesinde bu 800 ms hangi fazda görünür: Stalled mı, Waiting mi, Content Download mı? İkinci sorunun cevabı, "API yavaş" şikâyetinin ağdan mı sunucudan mı geldiğini söyleyen tek satır.
Kolonlar ve Filtreler: Doğru Satırı Bulmak
Network panelini açıp sayfayı yeniledik. Sayfanın yüklediği 31 isteğin tipleri:
page requests total: 31 | types: {"Document":1,"Script":24,"Stylesheet":1,"Manifest":1,"Preflight":1,"Fetch":1,"Other":2}
24 script, dev server'ın parça parça sunduğu framework ve uygulama dosyaları. Gerçek işi yapan iki satır: bir Preflight ve bir Fetch. Angular 22'de HttpClient varsayılan olarak Fetch API'siyle çalışıyor, XHR'a (XMLHttpRequest) dönmek için provideHttpClient(withXhr()) gerekiyor; bu yüzden istek Fetch/XHR filtresinde Fetch tipiyle görünür.
Kolon başlığına sağ tıklayınca açılan menüden Protocol, Priority, Initiator, Remote Address gibi kolonları eklersin. Response Headers > Manage Header Columns ile kendi header kolonunu da ekleyebilirsin; bizim API'nin döndürdüğü X-Auth-Seen için bir kolon eklemek, interceptor'ın her istekte çalıştığını tek bakışta gösterir. Chrome 155–156'dan beri bu custom header kolonları ve sıralamaları sayfa yenilense de korunuyor.
Filter kutusu düz metinden fazlasını kabul eder: method:OPTIONS, status-code:404, larger-than:100k, domain:localhost, mime-type:application/json, has-response-header:x-auth-seen, açık WebSocket'ler için is:running. Birden fazla property'yi boşlukla yazarsan hepsi birlikte aranır (mantıksal ve), "ya da" bağlacı desteklenmez; filtreyi tersine çevirmek için yanındaki Invert kutusunu işaretlersin. Kutunun altındaki tip düğmeleri (Fetch/XHR, Doc, CSS (Cascading Style Sheets), JS (JavaScript), Img…) de aynı işi tek tıkla yapar.
Kayıt Ctrl+E (Cmd+E) ile durup başlar. Preserve log, Elements'teki gibi sayfa değişse de satırları tutar; Disable cache, DevTools açıkken HTTP (HyperText Transfer Protocol) cache'ini devre dışı bırakır. Bu ikisinin açık olup olmadığı, aynı hatayı iki farklı şekilde görmenin en yaygın sebebi.
Preflight, Authorization ve Bir CORS Hatasını Okumak
Önce Tahmin Et'in ilk cevabı iki satır. Satırların gerçek değerleri:
preflight: OPTIONS /api/books status=204 protocol=http/1.1 type=Preflight priority=High bytes=0
request: GET /api/books status=200 protocol=http/1.1 type=Fetch priority=High bytes=175992
request Authorization: Bearer demo-token-42 | response X-Auth-Seen: yes
preflight request headers: {"Access-Control-Request-Headers":"authorization","Access-Control-Request-Method":"GET","Origin":"http://localhost:4391"}
Origin farklı ve istek Authorization gibi "basit olmayan" bir header taşıdığı için tarayıcı önce bir OPTIONS isteğiyle izin istiyor. Preflight satırının Headers sekmesinde Access-Control-Request-Headers: authorization görünür; yanıtında API'nin izin verdiği header'lar. Gerçek isteğin Request Headers bölümünde Authorization'ın olması interceptor'ın çalıştığının kanıtı. Burada X-Auth-Seen: yes da sunucunun header'ı aldığını söylüyor.
Şimdi bozalım. Aynı API'ye izin verilmemiş bir header'la istek atıyoruz:
fetch('http://localhost:5391/api/books/3', { headers: { 'X-Trace-Id': 'abc-123' } })
GET status=- failed=net::ERR_FAILED corsError={"corsError":"HeaderDisallowedByPreflightResponse","failedParameter":"x-trace-id"} allow-headers=-
OPTIONS status=204 failed=- corsError=null allow-headers=Authorization,Content-Type
console: [javascript/error] Access to fetch at 'http://localhost:5391/api/books/3' from origin 'http://localhost:4391' has been blocked by CORS policy: Request header field x-trace-id is not allowed by Access-Control-Allow-Headers in preflight response.
Preflight başarılı (204) ama yanıttaki Access-Control-Allow-Headers listesinde x-trace-id yok. Network panelinde gerçek GET satırının Status kolonunda, CORS (Cross-Origin Resource Sharing) kuralına takıldığını belirten "CORS error" yazar, Console'daki mesaj da sebebi açıkça söyler. JavaScript tarafına ise yalnız TypeError: Failed to fetch ulaşır; tarayıcı güvenlik gereği detayı koda vermez. Bu yüzden CORS hatasını uygulama log'unda değil, DevTools'ta okursun. Düzeltme frontend'de değil, ASP.NET Core'daki policy'de:
builder.Services.AddCors(options =>
options.AddPolicy("BookstoreWeb", policy => policy
.WithOrigins(builder.Configuration["AllowedOrigin"] ?? "http://localhost:4391")
.WithHeaders("Authorization", "Content-Type")
.WithMethods("GET", "POST")));
WithHeaders'a X-Trace-Id eklemek yeter. CORS'un kendisi JavaScript Orta serisinin "JavaScript Fetch API: GET, POST, response.ok, Timeout ve CORS'a Giriş" yazısında anlatıldı.
Waterfall ve Timing Fazları
Waterfall kolonu her isteği zaman ekseninde bir çubuk olarak çizer. Bir isteğe tıklayıp Timing sekmesini açınca çubuğun fazlarını sayılarla görürsün. API'de 800 ms bekleyen bir endpoint var:
app.MapGet("/api/books/slow", async () =>
{
await Task.Delay(800);
return Results.Ok(new { message = "slow response" });
});
Bu isteğin CDP'den okuduğumuz timing değerleri (ms):
slow: GET /api/books/slow status=200 protocol=http/1.1 type=Fetch priority=High bytes=238
timing ms {"stalled_until_dns":"0.2","dns":"0.3","connect":"0.2","ssl":"-","send":"0.0","waiting_ttfb":"803.1","content_download":"0.1","total":"803.9"}

Fazları sırayla okursak: Queueing ve Stalled, isteğin bir bağlantı ya da sıra beklediği süre; HTTP/1.1'de aynı origin'e en fazla altı bağlantı açıldığı için çok sayıda paralel istekte burası uzar. DNS (Domain Name System) Lookup host adını çözer, Initial connection TCP (Transmission Control Protocol) el sıkışmasıdır ve HTTPS'te (HTTP Secure) TLS (Transport Layer Security) de bu faza dahildir. Request sent isteğin gönderilmesi, Waiting (TTFB, Time To First Byte) ilk byte'ın gelmesine kadar geçen süre, Content Download ise yanıt gövdesinin inmesi. 803.9 ms'nin 803.1'i Waiting'de: sorun ağda değil, sunucuda. "API yavaş" diyen ekip arkadaşına göstereceğin ilk ekran bu. Bu fazların protokol düzeyindeki anlamı ilerideki "Networking ve HTTP Nasıl Çalışır" serisinin konusu.
Throttling, Offline ve Request Blocking
Network panelinin üstündeki throttling menüsünde No throttling, Fast 4G, Slow 4G, 3G ve Offline hazır gelir; Custom > Add ile kendi profilini tanımlarsın. Aynı GET /api/books isteğini (yaklaşık 176 KB) throttling'siz ve 400 ms gecikme, 100 KB/s indirme hızıyla tanımladığımız özel bir profille çalıştırdık:
throttle no throttling: fetch took 13 ms, transferred 175991 bytes, ttfb 0.6 ms, headers at 0.9 ms, finished at 2.6 ms
throttle custom 400ms latency, 100 KB/s: fetch took 2133 ms, transferred 175991 bytes, ttfb 0.6 ms, headers at 410.1 ms, finished at 2127.1 ms
Hesap tutuyor: 400 ms gecikme header'ların gelişini 410 ms'ye itiyor, 175.991 byte'ı 100 KB/s ile indirmek de yaklaşık 1,7 saniye. Localhost'ta 13 ms'de gelen liste, yavaş bir mobil bağlantıda iki saniyeyi geçiyor. 2000 kitabı tek seferde döndürmenin bedelini görmek için ölçüm bundan net olamaz; çözümü sayfalama ya da sunucu tarafında filtreleme. Not: DevTools throttling'i tarayıcı seviyesinde bir emülasyon, gerçek ağın paket kaybını ve dalgalanmasını taklit etmez.
Offline seçince istek ağa hiç çıkmaz:
offline fetch -> TypeError: Failed to fetch
loadingFailed errorText: net::ERR_INTERNET_DISCONNECTED
Request blocking ise belli bir isteği engeller: isteğe sağ tıklayıp Block request URL (Uniform Resource Locator) ya da Block request domain dersin, Drawer'daki Network request blocking sekmesinde */api/books/slow gibi pattern'ler de tanımlarsın. Engellediğimiz istek blockedReason: inspector ile düştü ve panelde kırmızı satır olarak göründü. Bir servisin yokluğunda uygulamanın nasıl davrandığını (spinner sonsuza kadar mı dönüyor, hata mesajı çıkıyor mu) test etmenin en hızlı yolu bu.
İsteği Tekrar Oynatmak ve Mock'lamak: Copy as cURL, Edit and Resend, Local Overrides
Bir isteğe sağ tıklayıp Copy altından Copy as cURL, Copy as PowerShell, Copy as fetch ya da Copy as fetch (Node.js) seçersin. Backend ekibine "bu istek 500 dönüyor" demek yerine cURL (client for URLs) komutunu vermek, onların hatayı kendi makinelerinde birebir üretmesini sağlar. Kopyalanan komut Authorization header'ını ve cookie'leri de içerir; paylaşmadan önce token'ı temizle.
Chrome 153–154 ile gelen Edit and resend as fetch, isteği Console'da düzenlenebilir bir fetch() ifadesi olarak açıyor; parametre açıklamaları yorum olarak geliyor ve orijinal isteğe bir link de duruyor. Bir query parametresini ya da body'deki bir alanı değiştirip isteği yeniden göndermek için Postman'e geçmene gerek kalmıyor.
Local Overrides ise sunucunun yanıtını yerelde ezer. Network'te isteğe sağ tıklayıp Override content ya da Override headers dersin; ilk seferde DevTools'un dosyaları yazacağı bir klasör seçip izin verirsin, düzenlediğin yanıt Sources > Overrides altında bir dosya olarak durur. Override'lı istekler panelde işaretlenir ve has-overrides:yes filtresiyle bulunur. Chrome 155–156'dan beri response body override'ları sayfa navigasyonlarında da korunuyor; 153–154 ile de data:, blob: gibi http(s) olmayan şemalarda override kapatıldı.
Aynı fikri CDP'nin Fetch domain'iyle, API'de olmayan 9999 numaralı kitap için denedik: /api/books/9999'a giden GET'i yakalayıp sahte bir JSON (JavaScript Object Notation) döndürdük, preflight'ı olduğu gibi geçirdik. Kitap detay sayfasının çıktısı:
Fetch.requestPaused OPTIONS http://localhost:5391/api/books/9999
Fetch.requestPaused GET http://localhost:5391/api/books/9999
override -> h2: Mocked Volume | p: Grace Hopper · $99.00 · seen 1s
Backend'de endpoint henüz yokken frontend'i ilerletmek, ya da "stok 0 olunca buton ne yapıyor" gibi bir kenar durumu sunucu verisini değiştirmeden denemek için birebir. Override ettiğin şeyin gerçek API sözleşmesiyle uyumlu kalmasından sen sorumlusun; override'ı kapatmayı unutursan bir saat boyunca var olmayan bir veriyi debug edersin.
HAR: Paylaşmadan Önce
HAR (HTTP Archive), Network panelindeki tüm isteklerin JSON formatında kaydı; destek ekibi ya da başka bir takımla "bende böyle oluyor" kanıtı paylaşmanın standart yolu. Panelin araç çubuğundaki indirme ikonuyla dışa, yükleme ikonuyla içe aktarırsın. Chrome 130'dan beri varsayılan export sanitized: Export HAR (sanitized) ve Copy all as HAR (sanitized) seçenekleri Cookie, Set-Cookie ve Authorization header'larını dosyaya koymaz. Hassas veriyle export için Settings > Preferences > Network altında "Allow to generate HAR with sensitive data" ayarını açman gerekir; ardından Export HAR (with sensitive data) seçeneği görünür.
Sanitized export her şeyi temizlemez. URL'deki query string'ler, response body'ler, X-Api-Key gibi özel header'lar ve form verisi dosyada kalır. HAR'ı bir ticket'a eklemeden önce bir metin editöründe aç ve token, e-posta, sipariş numarası gibi değerleri ara. Bookstore örneğinde bile sanitized bir HAR, 2000 kitabın tamamını ve X-Auth-Seen header'ını taşır.
WebSocket ve EventSource Mesajları
Sürekli açık bağlantılar Network panelinde tek satırdır, mesajlar ayrı bir sekmede görünür: WebSocket için Messages, Server-Sent Events için EventStream. API'ye .NET 10'un TypedResults.ServerSentEvents'i ile stok güncellemesi yayınlayan bir endpoint ekledik:
app.MapGet("/api/stock/stream", (CancellationToken ct) =>
TypedResults.ServerSentEvents(StockUpdates(ct), eventType: "stock"));
static async IAsyncEnumerable<StockUpdate> StockUpdates([System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken ct)
{
int[] bookIds = [3, 7, 12];
for (int i = 0; i < 3 && !ct.IsCancellationRequested; i++)
{
await Task.Delay(300, ct);
yield return new StockUpdate(bookIds[i], 10 - i);
}
}
Sayfada new EventSource('http://localhost:5391/api/stock/stream') açtığımızda isteğin EventStream sekmesine düşen mesajlar:
[EventStream] event=stock data={"bookId":3,"stock":10}
[EventStream] event=stock data={"bookId":7,"stock":9}
[EventStream] event=stock data={"bookId":12,"stock":8}
Her satırın tipi (stock), verisi ve zamanı görünür. Angular tarafında "stok güncellemesi gelmiyor" şikâyetinde önce buraya bak: mesaj burada varsa sorun component'te, yoksa sunucuda ya da bağlantıda. WebSocket'te Messages sekmesi giden ve gelen frame'leri ayrı renklerle gösterir ve bir filtre kutusu vardır.
Üretimde Dikkat
Disable cache açıkken ölçtüğün süre gerçek kullanıcının tekrar ziyaretini temsil etmez; ilk ziyaret senaryosu için açık, tekrar ziyaret için kapalı ölç ve hangisini ölçtüğünü yaz. Production'da protokol de farklı olacak: bizim yerel Kestrel'imiz http/1.1 konuşuyor, bir CDN (Content Delivery Network) arkasında aynı istek h2 ya da h3 olarak görünür ve Stalled fazı bambaşka davranır.
Copy as cURL, HAR ve ekran paylaşımı token sızdırmanın en yaygın üç yolu. Production token'larıyla alınan bir HAR'ı sanitized olsa bile kontrol etmeden paylaşma. Local Overrides ve request blocking ise yalnız senin tarayıcında çalışır; bir hatayı "override ile düzelttim" demek, kullanıcının gördüğü hiçbir şeyi değiştirmez.
Sık Düşülen Tuzaklar
- CORS hatasını frontend'de çözmeye çalışmak: izin sunucunun yanıtındaki header'larla verilir.
- Preflight satırını gözden kaçırıp "istek iki kez gidiyor" diye bug açmak.
- Uzun Waiting'i ağ sorunu sanmak: Timing sekmesi süreyi sunucuya yazıyorsa CDN ya da sıkıştırma ayarı çözmez.
- Throttling'i açık unutup her şeyin neden yavaş olduğunu aramak; ikon Network sekmesinin yanında uyarı gösterir.
- Sanitized HAR'ı tamamen temiz sanmak: query string ve body'ler dosyada kalır.
- Override'ı açık bırakıp gerçek API'nin yanıtını hiç görmemek.
Küçük Bir Deneme
★☆☆ Bookstore listesini Network paneli açıkken yenile. method:OPTIONS filtresiyle preflight'ı bul, ardından X-Auth-Seen için bir header kolonu ekle ve sayfayı yenileyince kolonun kaybolmadığını gözle.
★★☆ /api/books/slow için Timing sekmesini aç, sonra Task.Delay'i 200 ms'ye indir ve Waiting fazının nasıl değiştiğini karşılaştır. Aynı isteği Slow 4G'de tekrarla; hangi fazların uzadığını yaz.
★★★ Local Overrides ile /api/books/3 yanıtında stock'u 0 yap ve detay sayfasının davranışını gözle. Ardından Export HAR (sanitized) ile bir HAR al, dosyada Authorization olmadığını ama demo-token-42'nin başka bir yerde geçip geçmediğini ara.
Network panelinde sunucuya giden her şeyi gördük; ama tarayıcının kendi sakladığı veriler, yani localStorage, IndexedDB, cookie'ler, Cache Storage ve Service Worker başka bir panelin işi. Bir sonraki yazıda Application panelini açıyoruz: cookie flag'leri, bloklanan bir cookie'nin nedenini okumak ve deploy sonrası eski bundle'ı sunan Angular Service Worker'ını teşhis etmek.