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

Critical Rendering Path: HTML, CSS ve JavaScript Ekrana Nasıl Ulaşır, Neyi Render-Blocking Yapar?

Mert Özen 15 Kas 2026 12 dk 7 görüntülenme
Critical Rendering Path: HTML, CSS ve JavaScript Ekrana Nasıl Ulaşır, Neyi Render-Blocking Yapar?

Critical rendering path, ilk paint'e kadar tarayıcının beklediği en uzun zincirdir; head'deki stylesheet'ler render'ı, async/defer'siz script'ler parser'ı bloklar ama her blocking kaynak kritik yolda değildir. Bookstore ana sayfasının waterfall'unda font CSS'ini ve analytics'i düzeltmenin FCP'yi değiştirmediğini, asıl zincirin main.js olduğunu ölçüyor; response compression ve analytics'i load'dan sonra yükleyerek FCP'yi 2,35 sn'den 1,10 sn'ye indiriyoruz.

Sürüm notu: Bu yazıdaki örnekler Angular 22.2 (Angular 22.2'nin desteklediği TypeScript 6.0 ile), .NET 10 (ASP.NET Core 10), Lighthouse 13.6 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

Sitedeki serilerin ortak örnek uygulaması Bookstore'un (bir online kitapçı) Angular istemcisi bookstore-web'in kaynaktaki index.html'i şu. İlk yazıda kurduğumuz ölçümde ana sayfanın telefondaki FCP'si (First Contentful Paint; ekrana ilk metnin ya da görselin çizildiği an) 2,35 sn çıkmıştı:

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Bookstore</title>
  <base href="/">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" type="image/x-icon" href="favicon.ico">
  <link rel="stylesheet" href="http://fonts.thirdparty.test:5302/css2?family=Inter:wght@400;700">
  <script src="http://analytics.thirdparty.test:5302/analytics.js"></script>
</head>
<body>
  <app-root></app-root>
</body>
</html>

Okumaya devam etmeden önce tahmin et: <head>'deki iki üçüncü parti satır (bir font sağlayıcısının CSS'i ve bir analytics script'i) düzeltilirse FCP ne kadar düşer? Yarım saniye mi, bir saniye mi, hiç mi?

Ölçüm düzeni ilk yazıdakiyle aynı: ASP.NET Core API'si Angular build'ini sunuyor; Playwright ile başlatılan Chromium, "mobile" profilinde (4x CPU yavaşlatma, 150 ms gecikme, 1,6 Mbps) sayfayı beş kez açıyor ve değerleri uygulamanın kendi RUM (Real User Monitoring; gerçek kullanıcı ölçümü) kodundan okuyor. Font sağlayıcısı ve analytics sağlayıcısı gerçek değil: *.thirdparty.test adlarını Chromium'un --host-resolver-rules seçeneğiyle yerel bir Node sunucusuna yönlendiriyoruz ve o sunucu, uzak bir sunucunun yanıt süresini taklit etmek için her yanıta 250 ms ekliyor. Gerçek bir üçüncü partiye istek atmadık.

HTML'den Piksele: DOM, CSSOM, Render Tree

02-crp-blocking-tr.png

Tarayıcı HTML'i geldikçe parse eder ve DOM'u (Document Object Model; sayfanın tarayıcıdaki ağaç yapısı) kurar. Karşılaştığı her stylesheet'i indirip CSSOM'a (CSS Object Model; stil kurallarının ağaç hali) çevirir. İkisi birleşince render tree çıkar: ekranda gerçekten görünecek node'lar ve hesaplanmış stilleri. Ardından layout her kutunun yerini ve boyutunu hesaplar, paint çizim komutlarını üretir, composite de katmanları birleştirip ekrana verir. Critical rendering path, ilk paint'e kadar bu hattın üzerindeki bütün işlerin ve kaynakların zinciri.

Hattın iki durma noktası var. Birincisi CSS: tarayıcı stilsiz içeriği bir an gösterip sonra yeniden çizmek istemediği için <head>'deki stylesheet'ler inene kadar hiçbir şey çizmez. Bu render-blocking. İkincisi JavaScript: async ya da defer olmayan bir <script> DOM'u değiştirebileceği için parser onu indirip çalıştırana kadar durur. Bu parser-blocking. Script CSSOM'u da okuyabildiği için önündeki stylesheet inmeden çalışmaz; yani sync bir script kendinden önceki CSS'in gecikmesini de devralır.

Parser beklerken boş durmayan bir yardımcı var: preload scanner. Ham HTML'i ileriye doğru tarayıp src ve href'leri bulur ve parser'a sıra gelmeden indirmeye başlar. Bu yüzden HTML'de yazan bir <img> ya da <script src> erken keşfedilir; JavaScript'in sonradan DOM'a eklediği ya da CSS'te background-image olarak duran bir kaynak ise ancak onu ekleyen kod çalışınca keşfedilir. Bir sonraki yazının LCP (Largest Contentful Paint; viewport'taki en büyük içeriğin çizildiği an) sorunu tam olarak bu.

defer, async ve type="module"

EtiketParser'ı durdurur mu?Ne zaman çalışır?Sıra korunur mu?
<script src>Evet, inip çalışana kadarHemenEvet
<script async>Hayır (yalnız çalışırken)İner inmezHayır
<script defer>HayırParse bitince, DOMContentLoaded'dan önceEvet
<script type="module">Hayır (varsayılan olarak defer gibi)Parse bitinceEvet

Ayrıntılar JavaScript Temel serisindeki "JavaScript ile İlk Script: script Etiketi, defer, async ve DevTools" yazısında; burada önemli olan, bir script'in parser'ı durdurmaması ile kritik yolda olmaması arasındaki fark. Bu ayrımı birazdan ölçerek göreceğiz.

Angular Build'in Ürettiği index.html

Production build'in ürettiği index.html kaynaktakinden farklı:

<!doctype html>
<html lang="en" data-beasties-container>
<head>
  <meta charset="utf-8">
  <title>Bookstore</title>
  <base href="/">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" type="image/x-icon" href="favicon.ico">
  <link rel="stylesheet" href="http://fonts.thirdparty.test:5302/css2?family=Inter:wght@400;700">
  <script src="http://analytics.thirdparty.test:5302/analytics.js"></script>
<style>*{box-sizing:border-box}body{margin:0;font-family:Inter,system-ui,sans-serif;color:#0f172a;background:#f8fafc;line-height:1.5}</style><link rel="stylesheet" href="styles-SR7LJ3IE.css" media="print" data-beasties-media="all"><noscript><link rel="stylesheet" href="styles-SR7LJ3IE.css"></noscript></head>
<body>
  <app-root></app-root>
<script src="main-EWTR2XZC.js" type="module"></script><script>document.querySelectorAll('link[data-beasties-media]').forEach(function(l){l.media=l.getAttribute('data-beasties-media');l.removeAttribute('data-beasties-media')})</script></body>
</html>

Üç şey dikkat çekiyor. Birincisi, styles.css'in ilk görünüm için gereken kısmı (burada body kuralları) beasties ile <style> olarak inline edilmiş; asıl dosya media="print" ile isteniyor. Ekranda eşleşmeyen bir media render'ı bloklamaz; body'nin sonundaki küçük inline script de media'yı all'a çeviriyor. Angular'ın inlineCritical optimizasyonu production build'de varsayılan olarak açık. İkincisi, main.js type="module" ile geliyor, yani parser'ı bloklamıyor. Üçüncüsü, elle eklediğimiz iki satır <head>'in en üstünde duruyor: Angular onlara dokunmuyor. angular.json'daki budget'lar ise yalnız Angular'ın ürettiği dosyaları sayıyor; bu iki satırın getirdiği byte'lar hiçbir budget'a girmiyor.

Bookstore Ana Sayfasının Waterfall'u

Waterfall'u DevTools'un Network paneli de gösteriyor (Browser DevTools serisinin Network paneli yazısı); tekrarlanabilir olsun diye Resource Timing API'den okuyan küçük bir script kullanıyoruz. Chromium 107'den beri her kaynak kaydında render'ı bloklayıp bloklamadığını söyleyen renderBlockingStatus alanı var (Firefox'ta yalnız önizlemede, Safari'de yok):

await page.goto(positionals[0] ?? 'http://127.0.0.1:5300/', { waitUntil: 'networkidle', timeout: 120_000 });
const data = await page.evaluate(async () => {
  // LCP kayıtları yalnız buffered bir PerformanceObserver ile okunabiliyor
  const lcp = await new Promise((resolve) =>
    new PerformanceObserver((list) => resolve(list.getEntries().at(-1))).observe({ type: 'largest-contentful-paint', buffered: true }));
  const fcp = performance.getEntriesByName('first-contentful-paint')[0];
  const nav = performance.getEntriesByType('navigation')[0];
  const rows = [nav, ...performance.getEntriesByType('resource')].map((e) => ({
    name: e.name.replace(location.origin, '').replace('http://', '').slice(0, 56),
    start: Math.round(e.startTime),
    end: Math.round(e.responseEnd),
    blocking: e.renderBlockingStatus,
  }));
  return { rows, fcp: Math.round(fcp?.startTime), lcp: Math.round(lcp?.startTime) };
});
resource                                                  start    end  render-blocking
/                                                             0    164  non-blocking
fonts.thirdparty.test:5302/css2?family=Inter:wght@400;70    201    485  blocking
analytics.thirdparty.test:5302/analytics.js                 202   1235  blocking
/main-EWTR2XZC.js                                           203   2308  non-blocking
analytics.thirdparty.test:5302/collect                     1310   1573  non-blocking
/styles-SR7LJ3IE.css                                       1310   1475  non-blocking
/api/books/featured                                        2396   2555  non-blocking
/api/books/new                                             2396   2569  non-blocking
/covers/dune.jpg                                           2571  16265  non-blocking
/covers/emma.jpg                                           2591  16654  non-blocking
/covers/the-hobbit.jpg                                     2591  16227  non-blocking
/covers/pride-and-prejudice.jpg                            2591  17571  non-blocking
/covers/middlemarch.jpg                                    2591  17653  non-blocking
/covers/frankenstein.jpg                                   2591  18396  non-blocking
/covers/jane-eyre.jpg                                      2591  24381  non-blocking
/covers/moby-dick.jpg                                      2591  24575  non-blocking
/covers/war-and-peace.jpg                                  2591  24687  non-blocking
FCP 2420 ms, LCP 16272 ms

Font CSS'i ve analytics script'i blocking olarak işaretli. Analytics 1,2 sn'de bitiyor; o ana kadar parser bekliyor. Font dosyalarının listede olmaması da ayrıca not edilmeli: Chromium onları indirdi (aynı yüklemeyi DevTools protokolüyle izlediğimizde iki woff2 isteği görünüyor) ama başka bir origin'deki CSS'in başlattığı bu istekler sayfanın Resource Timing listesine düşmedi. RUM'la üçüncü parti kaynakları izlerken bu kör noktayı hesaba kat. Lighthouse da aynı iki kaynağı buluyor ve 900 ms kazanç tahmin ediyor:

Render-blocking requests: Est savings of 900 ms
  analytics.thirdparty.test:5302/analytics.js totalBytes=76286 wastedMs=1603
  fonts.thirdparty.test:5302/css2?family=Inter:wght@400;700 totalBytes=1806 wastedMs=854

Önce İki Bariz Düzeltme

Analytics script'inin sayfa içeriğiyle bir işi yok; parser'ı bekletmesi için sebep yok. Tek kelime yetiyor:

--- index.html
+++ index.html
@@ -7,7 +7,7 @@
   <meta name="viewport" content="width=device-width, initial-scale=1">
   <link rel="icon" type="image/x-icon" href="favicon.ico">
   <link rel="stylesheet" href="http://fonts.thirdparty.test:5302/css2?family=Inter:wght@400;700">
-  <script src="http://analytics.thirdparty.test:5302/analytics.js"></script>
+  <script async src="http://analytics.thirdparty.test:5302/analytics.js"></script>
 </head>
 <body>
   <app-root></app-root>

async script'i preload scanner yine erkenden indiriyor ama parser beklemiyor. Font CSS'i için iki yol var. Birincisi üçüncü partide kalıp bağlantıyı önceden açmak ve fontu display=swap ile istemek:

--- index.html
+++ index.html
@@ -6,7 +6,8 @@
   <base href="/">
   <meta name="viewport" content="width=device-width, initial-scale=1">
   <link rel="icon" type="image/x-icon" href="favicon.ico">
-  <link rel="stylesheet" href="http://fonts.thirdparty.test:5302/css2?family=Inter:wght@400;700">
+  <link rel="preconnect" href="http://fonts.thirdparty.test:5302" crossorigin>
+  <link rel="stylesheet" href="http://fonts.thirdparty.test:5302/css2?family=Inter:wght@400;700&display=swap">
   <script async src="http://analytics.thirdparty.test:5302/analytics.js"></script>
 </head>
 <body>

İkincisi font'u kendi origin'imizden sunmak. Font dosyaları (Inter, açık bir font lisansı olan Open Font License ile dağıtılıyor) public/fonts/'a kopyalanıyor, @font-face kuralları styles.css'e giriyor ve index.html'den font satırı tamamen çıkıyor:

/* Kendi origin'imizden Inter (SIL Open Font License, public/fonts/OFL.txt): latin + latin-ext, 400 ve 700 */
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('/fonts/inter-latin-ext-400-normal.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('/fonts/inter-latin-400-normal.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

Beasties bu @font-face kurallarını da inline CSS'e taşıyor; tarayıcı font'u ayrı bir CSS isteği ve ayrı bir bağlantı beklemeden istiyor. Üç değişikliğin ölçümü (beş koşunun ortancası, ms):

DurumFCPLCP
A: başlangıç2.34816.196
B: analytics async2.34816.196
C: B + preconnect + display=swap2.35216.208
D: B + font kendi origin'imizden2.34416.016
E: D + response compression1.49615.256
F: E + analytics load'dan sonra1.10414.852

İlk üç satır sürpriz: render-blocking iki kaynağı kaldırdık, FCP yerinden kıpırdamadı. Lighthouse ise D için başka bir şey söylüyor:

Lighthouse 5 runs, mobile: http://127.0.0.1:5300/
run  score    FCP    LCP    TBT    CLS     SI
1       71    606  12227    222  0.000    865
2       67   2111  12001    209  0.034   4367
3       72    610  12001    210  0.037   2438
4       72    610  12001    205  0.034   2457
5       67   2112  12001    206  0.034   4354
med     71    610  12001    209  0.034   2457

Lighthouse'un modeli beş koşunun üçünde FCP'yi 0,6 sn hesapladı; throttling'i gerçekten uygulayan ölçüm ise 2,34 sn diyor. Lighthouse varsayılan olarak sayfayı hızlı yükleyip yavaş cihazdaki zamanlamayı bir modelle tahmin ediyor ve bu sayfada tahmin gerçeği yakalamıyor. İnsight'ın "900 ms kazanç" tahmini de aynı modelden geliyordu.

Asıl Kritik Yol: main.js

Waterfall'a tekrar bak: <app-root> boş geldiği için ekrana çizilecek ilk içerik Angular çalışınca oluşuyor. Yani FCP'nin önündeki en uzun zincir HTML → main.js (282 KB) → Angular'ın ilk render'ı. CSS ve analytics bu zincirin üstünde değildi; main.js zaten 2,3 sn'de iniyordu ve onların bitmesini beklemiyordu. Critical rendering path'i kısaltmak, en uzun zinciri kısaltmak demek.

Zincirin en büyük parçası main.js'in byte'ları ve API onu sıkıştırmadan gönderiyordu. ASP.NET Core'da response compression iki satır:

builder.Services.AddSingleton(TimeProvider.System);
builder.Services.AddSingleton<BookCatalog>();
builder.Services.AddSingleton<RumStore>();
// Metin yanıtlarını sıkıştır (HTML, JS, CSS, JSON); Brotli/zstd ayrıntıları navigasyon yazısında
builder.Services.AddResponseCompression();

WebApplication app = builder.Build();

app.UseResponseCompression();
app.UseStaticFiles();

Varsayılan ayarla Brotli ve gzip açılıyor, istemcinin Accept-Encoding'ine göre seçiliyor ve yanıta Vary: Accept-Encoding ekleniyor. main.js 282 KB'tan 114 KB'a indi ve FCP 2,34 sn'den 1,50 sn'ye düştü (tablodaki E). Varsayılan Brotli seviyesi en hızlısı; build sırasında önceden sıkıştırılmış dosya sunmak, seviyeler ve zstd navigasyonları hızlandırma yazısının konusu.

Son adım bant genişliği: async analytics parser'ı bloklamıyordu ama 76 KB'ı main.js ile aynı anda iniyor ve hattı paylaşıyordu. Analytics'in ilk paint'e ihtiyacı yok; load event'inden sonra yükleniyor:

--- index.html
+++ index.html
@@ -6,8 +6,14 @@
   <base href="/">
   <meta name="viewport" content="width=device-width, initial-scale=1">
   <link rel="icon" type="image/x-icon" href="favicon.ico">
-  <link rel="stylesheet" href="http://fonts.thirdparty.test:5302/css2?family=Inter:wght@400;700">
-  <script async src="http://analytics.thirdparty.test:5302/analytics.js"></script>
+  <script>
+    // Analytics load event'inden sonra: artık main.js ile bant genişliği için yarışmıyor
+    addEventListener('load', () => {
+      const script = document.createElement('script');
+      script.src = 'http://analytics.thirdparty.test:5302/analytics.js';
+      document.head.append(script);
+    });
+  </script>
 </head>
 <body>
   <app-root></app-root>

FCP 1,10 sn'ye indi. Son durumun waterfall'u:

resource                                                  start    end  render-blocking
/                                                             0    157  non-blocking
analytics.thirdparty.test:5302/analytics.js                 168   1260  non-blocking
/main-EWTR2XZC.js                                           168   1410  non-blocking
/styles-EHOBXKHN.css                                        170    329  non-blocking
/fonts/inter-latin-400-normal.woff2                         181    638  non-blocking
analytics.thirdparty.test:5302/collect                     1326   1582  non-blocking
/fonts/inter-latin-700-normal.woff2                        1472   1770  non-blocking
/api/books/featured                                        1477   1634  non-blocking
/api/books/new                                             1478   1642  non-blocking
/covers/dune.jpg                                           1641  15233  non-blocking
/covers/the-hobbit.jpg                                     1655  15210  non-blocking
/covers/emma.jpg                                           1655  15629  non-blocking
/covers/middlemarch.jpg                                    1655  16649  non-blocking
/covers/pride-and-prejudice.jpg                            1655  16664  non-blocking
/covers/frankenstein.jpg                                   1655  17376  non-blocking
/covers/moby-dick.jpg                                      1655  23572  non-blocking
/covers/jane-eyre.jpg                                      1655  23370  non-blocking
/covers/war-and-peace.jpg                                  1655  23684  non-blocking
FCP 1492 ms, LCP 15252 ms

Bu waterfall E durumundan; F'de analytics satırları load'dan sonraya kayıyor. LCP hâlâ 15 sn civarında: kapak görselleri 1,6 sn'de istenip on saniyelerce iniyor. Onları bir sonraki iki yazıda ele alacağız.

Resource Hint'ler: Tarayıcıya Önceden Haber Vermek

Resource hint'ler tarayıcıya henüz keşfetmediği bir şeyi söyler. Hepsinin bir bedeli var: tarayıcının zaten bildiği bir şeyi tekrar söylemek boşa, gereksiz yere erken istemek ise kritik kaynağın bant genişliğini çalar; F adımında gördüğümüz gibi.

  • preconnect: bir origin'e DNS, TCP ve TLS (şifreli bağlantı kurulumu) bağlantısını önceden kurar. Kritik kaynağı kesin olarak istenecek bir-iki üçüncü parti origin için. web.dev'e göre tarayıcı 10 saniye içinde kullanılmayan bağlantıyı kapatıyor; font gibi anonymous modda istenen kaynaklar için crossorigin da gerekiyor, yoksa yalnız DNS çözülüyor.
  • dns-prefetch: yalnız DNS çözümlemesi; daha ucuz, daha az kesin bağlantılar için.
  • preload: belli bir kaynağı (font, LCP görseli, geç keşfedilen bir CSS) şimdi indirir. as doğru verilmezse kaynak doğru önceliği alamaz ve script'ler iki kez inebilir; font preload'unda crossorigin şart.
  • modulepreload: bir ES (ECMAScript; JavaScript'in standardı) module'ünü indirip parse eder; lazy chunk'lar için.
  • fetchpriority: high ya da low ile tarayıcının öncelik tahminini düzeltir; keşfi hızlandırmaz, yalnız sırayı değiştirir.

Bir de 103 Early Hints var: sunucu asıl yanıtı hazırlarken önce Link: <...>; rel=preload başlıklı bir ara yanıt gönderir, tarayıcı HTML'i beklemeden kritik kaynakları istemeye başlar (browser-compat-data'ya göre Chrome 103, Firefox 120, Safari 17). TTFB'si (Time to First Byte; HTML'in ilk byte'ının gelme süresi) uzun sayfalarda işe yarar. ASP.NET Core 10'un public API'sinde 1xx ara yanıt göndermeye yarayan bir method yok (referans assembly'lerinde yalnız HttpStatusCode.EarlyHints sabiti var); bu yüzden 103 genelde önündeki CDN (içeriği kullanıcıya yakın sunuculardan dağıtan ağ) ya da reverse proxy tarafından üretiliyor. Bu konu Domain, DNS ve CDN serisinde.

Üretimde Dikkat

Üçüncü parti script'ler zamanla birikir. Pazarlama ekibi tag manager'a bir pixel, destek ekibi bir chat widget'ı ekler ve hiçbiri kod review'dan geçmez. RUM'daki FCP ve LCP'yi deploy'lar kadar tag manager değişikliklerine göre de işaretle; bir sıçrama kod değişikliği olmadan da gelebilir. Kendi origin'imize taşıdığımız font'un cache ömrünü de düşün: dosya adında hash olmadığı için uzun bir max-age verirsen font güncellendiğinde eski dosya kalır. Angular'ın kendi çıktılarında bu sorun yok, onların adında hash var; HTTP caching ayrıntıları Caching serisinde.

Response compression'ı CPU maliyetiyle birlikte düşün: dinamik yanıtlar her istekte yeniden sıkıştırılıyor. Statik dosyalar için build sırasında sıkıştırılmış kopyalar üretip onları sunmak hem daha iyi oran hem sıfır istek anı maliyeti demek. HTTPS'te EnableForHttps seçeneği varsayılan olarak kapalı; açarken kullanıcıya özel veriyi gizli bilgiyle aynı yanıtta sıkıştırmanın BREACH (sıkıştırılmış HTTPS yanıtlarının boyutundan gizli bilgi çıkaran saldırı) tipi riskini değerlendir.

Sık Düşülen Tuzaklar

  • "Render-blocking kaynak yok" ile "kritik yol kısa" aynı şey sanmak. Bu sayfada iki blocking kaynağı kaldırmak FCP'yi hiç değiştirmedi; kritik yol main.js'ti.
  • Lighthouse insight'larındaki tahmini kazancı ölçmeden kabul etmek. Değişikliği önce/sonra gerçek throttling ile ölç.
  • Her şeyi preload etmek. Preload edilen her kaynak kritik kaynakla aynı hattı paylaşır; Chrome kullanılmayan preload'lar için load'dan yaklaşık 3 saniye sonra console'a uyarı yazıyor.
  • Kullanılmayan preconnect'ler ve font için crossorigin'siz preconnect.
  • async'i "maliyetsiz" sanmak. Parser'ı bloklamayan script de bant genişliği ve main thread zamanı harcar.
  • Kritik bir script'i JavaScript ile DOM'a ekleyerek "async" yapmak. Preload scanner onu göremez; <script async src> daha erken iner. Analytics gibi kritik olmayan script'lerde ise geç keşfedilmek tam istediğimiz şey.

Küçük Bir Deneme

★☆☆ third-party.mjs'i DELAY_MS=1000 ile yeniden başlat ve başlangıçtaki index.html ile son hâlini waterfall.mjs'ten geçir. Hangi sürümün FCP'si üçüncü partinin yavaşlamasından etkileniyor?

★★☆ Kendi origin'imizden sunulan font için index.html'e <link rel="preload" as="font" type="font/woff2" crossorigin> ekle ve measure.mjs ile ölç. Sonra crossorigin'i sil ve DevTools'un Network panelinde font'un kaç kez indiğine bak.

★★★ third-party.mjs'teki analytics script'ini 60 ms yerine 300 ms main thread'i meşgul edecek şekilde değiştir. F durumunda FCP değişmeyecek; peki ilk yazının RUM tablosunda hangi metrik ve hangi route bundan etkilenir? Tahmin et, sonra field-sim.mjs ile doğrula.

FCP 2,35 sn'den 1,10 sn'ye indi ama LCP hâlâ 15 sn: kapak görseli JavaScript çalışıp API'den veri gelene kadar keşfedilmiyor ve sekiz büyük görselle bant genişliği paylaşıyor. Bir sonraki yazıda LCP'yi dört alt parçasına (TTFB, resource load delay, load duration, render delay) ayırıp her birini ayrı ayrı düşüreceğiz: kapağı gerçek bir <img> yapacak, NgOptimizedImage priority ile önceliklendirecek ve ana sayfayı prerender edeceğiz.