LCP'yi Optimize Etmek: TTFB, Resource Load Delay, Load Duration ve Render Delay'i Tek Tek Düşürmek
LCP'yi düşürmek için onu TTFB, resource load delay, load duration ve render delay olarak dört parçaya ayırıp her birine ayrı bakmak gerekir; görsel HTML'de keşfedilebilir değilse fetchpriority tek başına işe yaramaz. Bookstore ana sayfasında NgOptimizedImage priority'nin client-side render'da hiçbir şey kazandırmadığını, prerender'ın load delay'i 1,1 sn'den 27 ms'ye indirdiğini ölçüyor; SSR tuzaklarını (document, transfer cache, UseDefaultFiles) gösteriyoruz.
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), 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çı) ana sayfası, önceki yazının sonunda telefonda (mobile profil, beş koşunun ortancası) şu durumdaydı:
$ node measure.mjs http://127.0.0.1:5300/ --runs 5 --profile mobile
run TTFB FCP LCP CLS
med 151 1104 14852 0.0318
LCP subparts (median): ttfb=151 loadDelay=1106 loadDuration=13590 renderDelay=7
LCP element: html>body>app-root>main>app-home-page>section.hero>div.hero-cover
LCP (Largest Contentful Paint; viewport'taki en büyük içeriğin çizildiği an) 14,85 sn ve LCP element'i öne çıkan kitabın, Dune'un kapağı. Bu yazıda üç değişiklik deneyeceğiz: kapağı NgOptimizedImage priority ile önceliklendirilmiş gerçek bir <img> yapmak, ana sayfayı build sırasında prerender etmek ve ikisini birlikte uygulamak. Okumaya devam etmeden önce tahmin et: hangisi LCP'yi en çok düşürür ve LCP 2,5 sn'nin altına iner mi?
LCP'nin Dört Alt Parçası
Bir görselin LCP'si arka arkaya gelen dört süreye bölünüyor; aralarında boşluk ya da çakışma yok, toplamları LCP'ye eşit:
- TTFB (Time to First Byte; ilk byte süresi): navigasyonun başlamasından HTML'in ilk byte'ının gelmesine kadar. Sunucu süresi, ağ gecikmesi ve yönlendirmeler.
- Resource load delay: TTFB'den LCP görselinin isteğinin başlamasına kadar. Görsel geç keşfediliyorsa ya da düşük öncelikle sıraya giriyorsa büyür.
- Resource load duration: görselin inmesi. Byte sayısı, bant genişliği ve aynı anda inen diğer kaynaklar.
- Element render delay: görsel indikten sonra ekrana çizilmesine kadar. Render-blocking CSS, main thread'i tutan JavaScript ya da element'i henüz DOM'a (sayfanın tarayıcıdaki ağaç yapısı) koymamış bir framework.
web.dev'in optimize-lcp rehberi iyi optimize edilmiş bir sayfada kabaca TTFB ve load duration'ın %40'ar, iki "delay"in %10'un altında olmasını öneriyor. İsimlerinde "delay" geçen iki parça boşa geçen zaman; sıfıra yaklaşmalılar. Rehberin önemli bir uyarısı daha var: bir parçayı küçültmek LCP'yi düşürmeyebilir, kazanılan süre bir sonraki parçaya kayabilir. O yüzden dördüne birlikte bakıyoruz. İlk yazıdaki RUM (gerçek kullanıcı ölçümü) kodu bu dört değeri web-vitals'ın attribution build'inden zaten gönderiyor; measure.mjs da ortancalarını yazdırıyor.
Bookstore'un başlangıç durumunda tablo net: TTFB 151 ms (lab profilindeki gecikme kadar), load delay 1,1 sn, load duration 13,6 sn, render delay 7 ms. Sorunun büyük kısmı load duration'da; o kısmı byte'lar belirliyor ve bir sonraki yazının konusu. Bu yazıda önce keşfi, önceliği ve render'ı düzeltiyoruz.
LCP Element'i HTML'de Görünüyor mu?
Başlangıçta kapak bir div'in background-image'ı ve bu div Angular'ın API'den veriyi alıp template'i render etmesiyle oluşuyor:
@if (featured.value(); as book) {
<section class="hero">
<div class="hero-cover" role="img" [attr.aria-label]="book.title"
[style.background-image]="'url(' + book.coverUrl + ')'"></div>
<div>
<p class="eyebrow">Book of the week</p>
Tarayıcının görsel isteğini başlatabilmesi için önce main.js'in inip çalışması, sonra /api/books/featured'in dönmesi, sonra stilin hesaplanması gerekiyor; load delay'in 1,1 sn olmasının sebebi bu zincir. Preload scanner CSS'teki background-image'ı göremez; HTML'de yazmayan, JavaScript'in sonradan eklediği bir <img>'i de göremez. LCP görselinin keşfedilebilir olması için iki koşul var: HTML'de <img src> (ya da bir <link rel="preload">) olarak yazması ve HTML'in bu içeriği sunucudan hazır getirmesi.
Adım 1: fetchpriority ve NgOptimizedImage priority
Önce kapağı gerçek bir <img> yapıyoruz. NgOptimizedImage'ın priority attribute'ü üç şey yapıyor: fetchpriority="high" ekliyor, loading="lazy" koymuyor ve decoding="sync" kullanıyor. Sunucu tarafında render edildiğinde bir de <link rel="preload"> ekliyor; bunu birazdan göreceğiz. Diğer kapaklar priority almadığı için NgOptimizedImage onlara varsayılan olarak loading="lazy" veriyor:
@Component({
selector: 'app-home-page',
imports: [RouterLink, NgOptimizedImage],
template: `
@if (featured.value(); as book) {
<section class="hero">
<!-- Gerçek bir <img>: keşfedilebilir, önceliklendirilebilir; priority = fetchpriority="high" + lazy loading yok -->
<img class="hero-cover" [ngSrc]="book.coverUrl!" width="1200" height="1800" priority [alt]="book.title" />
<div>
<p class="eyebrow">Book of the week</p>
<h1>{{ book.title }}</h1>
<p class="author">{{ book.author }}</p>
<p>{{ book.summary }}</p>
<a [routerLink]="['/books', book.isbn]">View details</a>
</div>
</section>
}
<h2>New arrivals</h2>
<ul class="grid">
@for (book of newArrivals.value() ?? []; track book.isbn) {
<li>
<a [routerLink]="['/books', book.isbn]">
<img [ngSrc]="book.coverUrl!" width="1200" height="1800" [alt]="book.title" />
<span>{{ book.title }}</span>
</a>
</li>
}
</ul>
`,
http://127.0.0.1:5300/ profile=mobile runs=5
run TTFB FCP LCP CLS
1 151 1128 14840 0.0318
2 150 1116 14852 0.0318
3 150 1104 14868 0.0318
4 151 1124 14852 0.0318
5 150 1116 14840 0.0318
med 150 1116 14852 0.0318
LCP subparts (median): ttfb=150 loadDelay=1123 loadDuration=13559 renderDelay=8
LCP element: html>body>app-root>main>app-home-page>section.hero>img.hero-cover
LCP değişmedi (14.852 ms). Load delay hâlâ 1,1 sn, çünkü uygulama hâlâ client-side render ediliyor ve <img> ancak JavaScript ve API'den sonra DOM'a giriyor; fetchpriority keşfi hızlandırmıyor, yalnız sırayı değiştiriyor. Load duration da değişmedi: diğer sekiz kapak loading="lazy" olsa da telefon ekranında viewport'a yakın oldukları için Chromium onları da hemen istiyor ve hepsi aynı anda iniyor. Bu lab'da API HTTP/1.1 konuşuyor ve tarayıcı origin başına altı bağlantı açıyor; başlamış indirmeler bant genişliğini paylaşırken yüksek öncelik tek başına bir şey kazandırmadı. Değişiklik yine de doğru: sonraki adımlar ancak gerçek bir <img> ile işe yarıyor.
Adım 2: Ana Sayfayı Prerender Etmek
Load delay'i asıl düşürecek olan, kapağı içeren HTML'in sunucudan hazır gelmesi. Bookstore'un ana sayfası her kullanıcıya aynı içeriği gösteriyor (haftanın kitabı ve yeni çıkanlar); bu yüzden her istekte render etmek (SSR) yerine build sırasında bir kez render edip statik HTML olarak sunmak, yani prerender ya da SSG (Static Site Generation; statik site üretimi), yetiyor. Angular'da paketler ve iki yeni dosya:
npm install @angular/ssr@22.2.7 @angular/platform-server@22.2.6
{
"build": {
"builder": "@angular/build:application",
"options": {
"browser": "src/main.ts",
"server": "src/main.server.ts",
"outputMode": "static"
}
}
}
import { RenderMode, ServerRoute } from '@angular/ssr';
export const serverRoutes: ServerRoute[] = [
{ path: '', renderMode: RenderMode.Prerender },
{ path: '**', renderMode: RenderMode.Client },
];
import { mergeApplicationConfig, ApplicationConfig } from '@angular/core';
import { provideServerRendering, withRoutes } from '@angular/ssr';
import { appConfig } from './app.config';
import { serverRoutes } from './app.routes.server';
import { API_ORIGIN } from './core/api-origin';
const serverConfig: ApplicationConfig = {
providers: [
provideServerRendering(withRoutes(serverRoutes)),
// Prerender adımı build sırasında çalışır ve ayakta bir Bookstore API'si ister
{ provide: API_ORIGIN, useValue: process.env['BOOKSTORE_API'] ?? 'http://127.0.0.1:5300' },
],
};
export const config = mergeApplicationConfig(appConfig, serverConfig);
outputMode: "static" ile build bir Node sunucusu üretmiyor; ana sayfa için prerender edilmiş index.html'i, diğer route'lar için de client-side render edilecek index.csr.html'i yazıyor. app.config.ts'e provideClientHydration() ekleniyor; tarayıcı prerender edilmiş DOM'u atıp yeniden çizmek yerine onu hydrate ediyor. Hydration'ın ve incremental hydration'ın ayrıntıları bu serinin SSR yazısında.
İlk build denemesi şöyle bitti:
$ npx ng build
Prerendered 0 static routes.
Application bundle generation failed. [1.379 seconds]
✘ [ERROR] An error occurred while extracting routes.
ReferenceError: document is not defined
at Cg (file:///home/user/bookstore-web/.angular/prerender-root/…/main.server.mjs:44:20021)
Prerender, uygulamayı Node'da çalıştırıyor ve provideAppInitializer orada da çalışıyor. İlk yazıda yazdığımız RUM kodu web-vitals'ı başlatıyor; o da document'a dokunuyor. Çözüm, ölçümü yalnız tarayıcıda başlatmak:
export function provideRum(options: RumOptions): EnvironmentProviders {
return makeEnvironmentProviders([
provideAppInitializer(() => {
// Yalnız tarayıcıda: prerender sırasında ölçülecek bir sayfa yok
if (!isPlatformBrowser(inject(PLATFORM_ID))) return;
// Sampling sayfa görüntüleme başına: bu ziyaretin ya bütün metrikleri gider ya hiçbiri
if (Math.random() < options.sampleRate) {
startRum(options, inject(Router), inject(DOCUMENT));
}
}),
]);
}
İkinci sorun veri: prerender build sırasında çalışıyor ve httpResource'un /api/books/featured isteğinin gideceği bir sayfa origin'i yok. API adresini bir token'la veren küçük bir interceptor yazdık:
import { DOCUMENT, InjectionToken, inject } from '@angular/core';
import { HttpInterceptorFn } from '@angular/common/http';
// Tarayıcı: sayfanın kendi origin'i. Prerender (app.config.server.ts): build sırasında API'nin adresi.
// İki taraf da aynı mutlak URL'yi gönderir, böylece HTTP transfer cache sunucunun yanıtını bulur.
export const API_ORIGIN = new InjectionToken<string>('API_ORIGIN', {
factory: () => inject(DOCUMENT).location.origin,
});
export const apiOriginInterceptor: HttpInterceptorFn = (req, next) =>
req.url.startsWith('/api/') ? next(req.clone({ url: inject(API_ORIGIN) + req.url })) : next(req);
Tarayıcıda da URL'yi mutlak yapmamızın sebebi HTTP transfer cache. Prerender sırasında alınan yanıtlar HTML'in içine (ng-state script'i) gömülüyor ve tarayıcı aynı isteği yaptığında ağa gitmeden oradan okunuyor. Anahtar istek URL'sinden üretiliyor; ilk denemede tarayıcı göreli /api/..., sunucu mutlak http://127.0.0.1:5300/api/... kullandığı için anahtarlar tutmadı ve tarayıcı hydration'dan sonra iki API isteğini yeniden gönderdi (waterfall'da 4,3 sn'de başlayıp ancak 13,9 sn'de biten iki gereksiz istek). Üretimde sunucunun API'ye iç ağdan farklı bir adresle gittiği durumda HTTP_TRANSFER_CACHE_ORIGIN_MAP token'ı iç adresi dış adrese eşliyor; yalnız sunucu config'inde verilmesi gerekiyor.
Son parça ASP.NET Core. / için prerender edilmiş index.html, diğer her route için index.csr.html sunulmalı:
app.UseResponseCompression();
// "/" -> index.html, build sırasında prerender edilen ana sayfa
app.UseDefaultFiles();
app.UseStaticFiles();
app.UseStaticFiles(new StaticFileOptions
{
FileProvider = new PhysicalFileProvider(Path.Combine(builder.Environment.ContentRootPath, "Covers")),
RequestPath = "/covers",
});
// Route eşleştirme statik dosyalardan sonra: yoksa aşağıdaki fallback endpoint
// "/"'ı zaten eşler ve UseDefaultFiles prerender edilen index.html'i hiç sunmaz
app.UseRouting();
app.MapCatalogEndpoints();
app.MapRumEndpoints();
// Diğer bütün route'lar istemcide render edilir: prerender edilen ana sayfayı değil, CSR kabuğunu sun
app.MapFallbackToFile("index.csr.html");
UseRouting()'i biz eklemezsek WebApplication routing'i pipeline'ın en başına koyuyor; fallback endpoint /'ı da eşliyor, UseDefaultFiles endpoint seçilmiş bir isteğe dokunmuyor ve ana sayfa olarak sessizce CSR (client-side rendering; sayfanın tarayıcıda render edilmesi) kabuğu dönüyor. Bunu curl ile fark ettik: /index.html prerender edilmiş sayfayı, / ise boş <app-root>'u döndürüyordu. Build çıktısındaki index.html'den birkaç satır:
<link rel="preload" as="font" crossorigin="anonymous" href="/fonts/inter-latin-400-normal.woff2">
<link rel="preload" as="font" crossorigin="anonymous" href="/fonts/inter-latin-700-normal.woff2">
<link _ngcontent-ng-c4283705217="" as="image" href="/covers/dune.jpg" rel="preload" fetchpriority="high">
...
<app-root ng-version="22.2.6" _nghost-ng-c3872582091="" ngh="1" ng-server-context="ssg">
...
<img _ngcontent-ng-c4283705217="" width="1200" height="1800" priority="" class="hero-cover" alt="Dune" loading="eager" fetchpriority="high" decoding="sync" ng-img="true" src="/covers/dune.jpg">
...
<script id="ng-state" type="application/json">
Kapak artık HTML'de bir <img> ve NgOptimizedImage sunucuda render edildiği için <head>'e fetchpriority="high" ile bir preload link'i eklemiş. Beasties de prerender edilmiş içerikte kullanılan iki font için preload link'i üretmiş.
Ölçüm: Hangi Adım Ne Kazandırdı?
Prerender'ın etkisini ayrı görmek için arada bir ölçüm daha yaptık: kapak hâlâ background-image iken prerender (C). Son durum (D) prerender ile <img priority>. Tabloda LCP'nin yanında FCP (First Contentful Paint; ilk içeriğin çizildiği an) da var:
http://127.0.0.1:5300/ profile=mobile runs=5
run TTFB FCP LCP CLS
1 151 308 13708 0
2 151 308 13692 0
3 151 308 13708 0
4 151 312 13692 0
5 151 308 13692 0
med 151 308 13692 0
LCP subparts (median): ttfb=151 loadDelay=27 loadDuration=13510 renderDelay=17
LCP element: html>body>app-root>main>app-home-page>section.hero>img.hero-cover
| Durum | FCP | LCP | TTFB | Load delay | Load duration | Render delay |
|---|---|---|---|---|---|---|
A: başlangıç (CSR, background-image) | 1.104 | 14.852 | 151 | 1.106 | 13.590 | 7 |
B: CSR, <img priority> | 1.116 | 14.852 | 150 | 1.123 | 13.559 | 8 |
C: prerender, background-image | 312 | 13.848 | 151 | 131 | 13.553 | 9 |
D: prerender, <img priority> | 308 | 13.692 | 151 | 27 | 13.510 | 17 |
Prerender FCP'yi 1,1 sn'den 0,3 sn'ye indirdi ve load delay'i neredeyse sıfırladı. C ile D arasındaki fark da ilginç: prerender edilmiş HTML'de inline stil olarak duran background-image bile, HTML parse edilip stil hesaplanır hesaplanmaz istendiği için yalnız 131 ms gecikiyor; <img> ve preload link'i ise görseli HTML'le neredeyse aynı anda başlatıyor (27 ms). Ama LCP yalnız 1,2 sn düştü: tahminin "2,5 sn'nin altı" idiyse, sebep tablonun beşinci sütunu. Load duration hâlâ 13,5 sn ve şimdi LCP'nin %98'i. CLS (Cumulative Layout Shift; beklenmedik kaymaların toplam puanı) de 0,03'ten 0'a indi; içerik artık JavaScript'le sonradan yerleşmiyor.
resource start end render-blocking
/ 0 180 non-blocking
/fonts/inter-latin-400-normal.woff2 189 945 non-blocking
/fonts/inter-latin-700-normal.woff2 190 990 non-blocking
/covers/dune.jpg 190 13687 non-blocking
/main-G4DYV3YR.js 190 4192 non-blocking
/styles-EHOBXKHN.css 200 382 non-blocking
/covers/the-hobbit.jpg 336 13866 non-blocking
/covers/emma.jpg 336 14250 non-blocking
/covers/middlemarch.jpg 336 15952 non-blocking
/covers/frankenstein.jpg 336 16904 non-blocking
/covers/pride-and-prejudice.jpg 336 18247 non-blocking
/covers/jane-eyre.jpg 336 22868 non-blocking
/covers/moby-dick.jpg 336 23018 non-blocking
/covers/war-and-peace.jpg 336 23152 non-blocking
analytics.thirdparty.test:5302/analytics.js 23156 23789 non-blocking
analytics.thirdparty.test:5302/collect 23855 24111 non-blocking
FCP 340 ms, LCP 13696 ms
Waterfall nedenini gösteriyor: dune.jpg 190 ms'de, diğer sekiz kapak 336 ms'de başlıyor ve 1,6 Mbps'lik hat dokuz yaklaşık 450 KB'lık dosya arasında paylaşılıyor. Sadece Dune'un kapağı inseydi bu hatta 450 KB yaklaşık 2,3 sn sürerdi (hesap, ölçüm değil). Asıl çözüm byte'ları azaltmak: ekrandaki boyuta uygun genişlik, AVIF (AV1 tabanlı yeni nesil görsel formatı) ya da WebP ve srcset. Bunu bir sonraki yazıda ASP.NET Core'da yapacağız.
Render Delay ve TTFB
Render delay bu ölçümlerde hep 20 ms'nin altında kaldı, çünkü görsel her seferinde sayfanın geri kalanından çok sonra iniyor. Görsel küçülünce durum değişir: client-side render edilen bir sayfada küçük bir görsel main.js'ten önce inebilir ve element DOM'a girene kadar beklemek zorunda kalır. web.dev rehberinin uyarısı tam olarak bu: load duration'dan kazanılan süre render delay'e kayar. Prerender bu yüzden LCP için önemli; element HTML'de olduğu için JavaScript'i beklemiyor. Render delay'i büyüten diğer sebepler render-blocking CSS, uzun task'lar ve @defer gibi içerik geciktiren yapılar; sonuncusu JavaScript maliyeti yazısının konusu.
TTFB burada lab profilinin eklediği 150 ms'lik gecikmeden ibaret, çünkü prerender edilmiş sayfa diskteki statik bir dosya. Gerçek hayatta TTFB'yi üç şey büyütüyor: kullanıcıyla sunucu arasındaki mesafe (CDN, yani içeriği kullanıcıya yakın sunuculardan dağıtan ağ, bunu kısaltır; Domain, DNS ve CDN serisi), her istekte yapılan render ve veri erişimi (SSR'da her istek Angular'ı çalıştırır; sonucu ASP.NET Core Output Cache ya da CDN'de cache'lemek Caching serisinin Output Cache yazısında) ve redirect'ler (her redirect bir tur daha demek). RUM'daki TTFB attribution'ı (waitingDuration, dnsDuration, connectionDuration) hangisi olduğunu ayırmaya yarıyor.
Üretimde Dikkat
LCP element'i herkes için aynı değil. Geniş ekranda başlık metni, telefonda kapak görseli LCP olabilir; giriş yapmış kullanıcıya özel bir banner ya da ilk ziyarette çıkan cookie banner'ı da LCP element'i olabilir. Lab'da iyi, field'da kötü LCP'nin en sık sebebi bu. RUM'da LCP ile birlikte attribution.target'ı (element'in selector'ü) ve url'i (görselin adresi) sakla ve route başına en sık görülen LCP element'lerine bak; düzeltmeyi lab'daki değil, kullanıcıların gerçekten gördüğü element için yap.
Prerender her sayfa için uygun değil. Fiyatı ve stoğu değişen bir kitap detay sayfasını build sırasında üretmek, deploy'lar arasında eski veriyi göstermek demek; o tür sayfalar için SSR ve kısa ömürlü bir cache daha doğru. Ana sayfanın "haftanın kitabı" bile ancak her hafta yeniden build ediliyorsa prerender'a uygun. Build'in API'ye ihtiyacı olduğunu CI/CD pipeline'ında da hesaba kat: prerender adımı API'ye ulaşamazsa, bu yazıdaki ilk denemede olduğu gibi, içeriği boş bir sayfa üretebilir; build'i bu durumda kırmak için prerender edilen HTML'de beklenen içeriği kontrol eden küçük bir adım ekle.
Sık Düşülen Tuzaklar
- LCP görselini
loading="lazy"bırakmak ya da bir lazy-load library'sinindata-src'si ile yüklemek. Preload scanner ikisini de göremez. - LCP görselini CSS
background-imageya da JavaScript ile eklemek vefetchpriority'nin bunu telafi edeceğini sanmak. - Her görsele
priorityvermek. NgOptimizedImage beşten fazla priority görselde NG02961 (Angular'ın uyarı kodu) uyarısını veriyor; her preload LCP görseliyle aynı hattı paylaşıyor. - Cookie banner'ını ya da bir kampanya modal'ını sayfanın üstüne büyük bir blok olarak koymak; LCP element'i o olur ve geç geldiği için LCP'yi uzatır.
- Prerender ya da SSR sırasında
window,document,localStoragegibi tarayıcı API'lerine koşulsuz dokunmak; build bu yazıdaki gibi kırılır. MapFallbackToFileileUseDefaultFiles'ıUseRouting()olmadan birlikte kullanmak; ana sayfa sessizce CSR kabuğu döner.- Sunucu ve tarayıcıda farklı API URL'leri kullanıp transfer cache'i kaçırmak; hydration'dan sonra aynı veri ikinci kez iner.
Küçük Bir Deneme
★☆☆ measure.mjs'i son durum için --profile desktop ile çalıştır. Masaüstü profilinde LCP'nin alt parçalarının oranı telefondakinden nasıl farklı?
★★☆ app.routes.server.ts'e Dune'un detay sayfası için getPrerenderParams ile bir prerender route'u ekle. Detay sayfasına doğrudan girildiğinde LCP alt parçaları nasıl değişiyor? Fiyat API'de değişirse sayfa ne zaman güncellenir?
★★★ Ana sayfayı RenderMode.Server ile her istekte render eden bir Node sunucusu kur (outputMode: "server"), API'deki /api/books/featured'e 300 ms yapay gecikme ekle ve TTFB ile LCP'yi prerender'la karşılaştır. Sonra API yanıtını kısa süreli cache'leyerek farkı tekrar ölç.
Load delay ve render delay neredeyse sıfır; LCP'nin geri kalanı byte. Bir sonraki yazıda kapakları ASP.NET Core'da ekrandaki boyuta göre küçültüp tarayıcının desteklediği en iyi formatta (AVIF ya da WebP) sunacak, NgOptimizedImage'a bunun için bir loader yazacak ve srcset'i ona ürettireceğiz. Aynı yazıda font'lara da döneceğiz: Türkçe karakterleri içermeyen bir subset'in neden fallback font'a düştüğünü ve font değişiminden kaynaklanan kaymayı size-adjust ile nasıl önleyeceğimizi göreceğiz.