Performance Paneli: Live Metrics, Trace Kaydı, Insights ve Angular Custom Track
Performance paneli bir etkileşimin süresini input delay, processing ve presentation delay olarak ayırır; zoneless Angular'da render presentation delay'e düşer. 2000 kitaplık listede filtreye yazmanın INP'sini 20x CPU'da 712 ms'den ölçüp computed signal ve @for track ile düzeltiyoruz; live metrics, flame chart, long task, Insights ve ng.enableProfiling() ile açılan Angular custom track gerçek trace verisiyle.
Sürüm notu: Bu yazıdaki örnekler Chrome 156 (156.0.8078.80), Angular 22.2 ve .NET 10 ile, Apple M5 işlemcili bir makinede denenmiştir. Trace'ler aynı Chrome sürümünde headless modda Chrome DevTools Protocol (CDP) ile kaydedildi; Insights çıktıları DevTools'un trace motorunu kullanan chrome-devtools-mcp 1.12.0'dan alındı. Panel adımları Chrome DevTools ve Angular dokümanından aktarıldı.
Önce Tahmin Et
Bookstore'un listesinde 2000 kitap var ve üstteki filtreye yazdıkça liste daralıyor. İlk sürüm şöyle:
@Component({
selector: 'app-book-list-slow',
imports: [BookCard],
template: `
<input class="filter" placeholder="Filter books" [value]="query()" (input)="query.set($any($event.target).value)" />
<p class="count">{{ visibleBooks().length }} books</p>
@for (book of visibleBooks(); track $index) {
<app-book-card [book]="book" (addToCart)="cart.add($event)" />
}
`,
})
export class BookListSlow {
protected readonly cart = inject(CartStore);
protected readonly query = signal('');
private readonly books = toSignal(inject(BookService).getBooks(), { initialValue: [] as Book[] });
protected visibleBooks(): Book[] {
const q = normalize(this.query());
return this.books().filter(b => normalize(b.title).includes(q));
}
}
normalize bilerek pahalı: her çağrıda yeni bir Intl.Collator oluşturup metni birkaç kez dolaşıyor. Masaüstünde her şey akıcı. Peki CPU (Central Processing Unit) 20 kat yavaşlatıldığında filtreye "C" yazmak ne kadar sürer? Ve süre JavaScript'te mi, DOM'da (Document Object Model) mı geçer? Bir tahmin yaz, sonra ölçelim.
Live Metrics: Kaydetmeden Önce Ölç
Performance panelini açtığında karşına boş bir timeline değil, Live metrics ekranı çıkar. Sayfayla etkileşime girdikçe LCP (Largest Contentful Paint), CLS (Cumulative Layout Shift) ve INP (Interaction to Next Paint) değerleri yerelde, canlı olarak güncellenir. Altındaki Interactions listesi her tıklamayı ve tuş vuruşunu süresiyle gösterir; Layout shifts listesi de kayan element'leri. Hiç trace kaydetmeden "hangi etkileşim yavaş" sorusunun ilk cevabı burada.
Aynı ekranda Field data bölümü, sayfanın gerçek kullanıcı verisini CrUX'ten (Chrome User Experience Report) çekip yerel değerin yanına koyabilir. Bunun için URL'nin (Uniform Resource Locator) Google'ın CrUX API'sine (Application Programming Interface) gönderilmesine izin vermen gerekir; yerel bir geliştirme adresinde veri olmaz. Lab ve field farkını Lighthouse yazısında ayrıca ele alacağız.
Lab ölçümünü kullanıcının cihazına yaklaştırmak için Environment settings'te CPU ve network throttling seçersin. CPU throttling'de sabit yavaşlatma oranlarının yanında, makineni kalibre ederek oluşturulan düşük ve orta segment mobil preset'ler de var. Chrome 153–154 notlarına göre kalibre edilmiş CPU performans seviyesi CDP'nin Emulation.setCPUPerformanceOverride method'uyla, Chrome 155–156'da ise Sensors panelindeki CPU Performance Tier kontrolüyle navigator.cpuPerformance değerinin üzerine yazılabiliyor; uygulaman cihaz gücüne göre davranış değiştiriyorsa bunu test etmenin yolu bu.
INP'yi Ölçmek: Yavaş ve Düzeltilmiş Liste
Production build'de, CPU 4x ve 20x yavaşlatılmışken filtreye 400 ms arayla "Comp" yazdık ve her etkileşimin süresini Event Timing API'siyle (PerformanceObserver, type: 'event') topladık. Aynı ölçümü düzeltilmiş BookList için de yaptık. Gerçek sonuçlar, iki koşudan:
/books-slow (CPU 4x): interactions=4 worst=136 ms [input delay 12, processing 0, presentation 124] all=136,56,40,32 | 286 books
/books (CPU 4x): interactions=2 worst=80 ms [input delay 10, processing 0, presentation 70] all=80,24 | 286 books
/books-slow (CPU 20x): interactions=4 worst=712 ms [input delay 37, processing 0, presentation 675] all=712,352,200,200 | 286 books
/books (CPU 20x): interactions=4 worst=440 ms [input delay 43, processing 0, presentation 397] all=440,128,48,48 | 286 books
/books-slow (CPU 20x): interactions=4 worst=672 ms [input delay 35, processing 4, presentation 633] all=672,424,192,184 | 286 books
/books (CPU 20x): interactions=4 worst=400 ms [input delay 42, processing 0, presentation 358] all=400,112,40,40 | 286 books
INP eşikleri 200 ms'ye kadar iyi, 500 ms'ye kadar geliştirilmeli, üstü kötü. Apple M5'te 4x yavaşlatmada iki sürüm de "iyi"; 20x'te yavaş sürüm 672–712 ms ile kötü bölgede. Düzeltme ilk tuşu 400–440 ms'ye, sonraki tuşları 200 ms'den 40–48 ms'ye indiriyor. Bu tablo iki şey söylüyor. Birincisi, ilk tuştaki maliyetin büyük kısmı her iki sürümde de var: 2000 karttan 1700'den fazlasını DOM'dan söken iş. İkincisi, sonraki tuşlarda DOM değişikliği küçük; oradaki fark tamamen bizim JavaScript'imiz.
Bir şey daha dikkat çekici: processing neredeyse sıfır, sürenin hepsi presentation'da. Zoneless Angular'da input event'inin handler'ı yalnız signal'i set ediyor; asıl render, Angular'ın scheduler'ı tarafından bir sonraki frame'e planlanıyor. Event Timing bu işi "presentation delay" olarak sayıyor. "Processing kısa, demek ki kod hızlı" diye okursan yanılırsın.
Trace Kaydı ve Flame Chart
Kaydı Performance panelindeki Record düğmesiyle (Ctrl+E, Mac'te Cmd+E) başlatıp durdurursun; sayfa yüklemesini ölçmek için Record and reload kullanılır. Kaydı küçük tut: yalnız incelemek istediğin etkileşimi yap, sonra durdur. Development build'de, 4x yavaşlatmayla yavaş listeye ilk tuşu bastığımız anın trace'inden çıkan tablo:

Main track'te 564 ms'lik bir task var; köşesindeki kırmızı üçgen ve 50 ms sonrasındaki kırmızı tarama onun bir long task olduğunu söylüyor. Bu task'ın içinde Animation frame fired 368.4 ms, Layout 15.2 ms, Paint 4.5 ms. Flame chart'ta üstteki çubuk çağıranı, alttakiler çağrılanları gösterir; genişlik süredir. Bir çubuğa tıklayınca Summary sekmesinde süresi ve kaynak satırı, Bottom-up sekmesinde ise bu aralıkta en çok self time harcayan fonksiyonlar listelenir. "Neresi yavaş" sorusunu Bottom-up ile, "neden oraya geldik" sorusunu Call tree ile cevaplarsın. Chrome 155–156'dan beri Call tree ve Bottom-up'ta açtığın node'lar ve kaydırma konumu başka panele geçip dönünce korunuyor.
Production build'deki trace'te fonksiyon adları minify edilmiş olur; source map yüklüyse DevTools onları orijinal adlarına çevirir. Ama asıl ayrıntıyı veren, bir sonraki bölümdeki Angular track.
Angular Custom Track: Change Detection'ı Timeline'da Görmek
Angular development modunda Performance paneline kendi track'ini ekleyebiliyor. Açmanın iki yolu var: konsolda ng.enableProfiling() çağırmak ya da uygulama başlamadan önce koddan açmak:
import { enableProfiling } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { App } from './app/app';
// bootstrap'tan önce: başlangıçtaki işi de kaydet
enableProfiling();
bootstrapApplication(App, appConfig)
.catch((err) => console.error(err));
Angular dokümanına göre bu yalnız development modunda çalışır. Track, DevTools'un extensibility API'si üzerinden console.timeStamp çağrılarıyla besleniyor; ham trace'te bu kayıtların track adı 🅰️ Angular. Yavaş listeye "Comp" yazdığımız kayıtta 8064 Angular kaydı vardı. En pahalıları:
642.0 ms x4 Synchronization 0
367.7 ms x1 Change detection 0
260.7 ms x4 BookListSlow_Template (update)
241.9 ms x2284 _BookCard
98.8 ms x855 BookCard_Template (update)
46.2 ms x867 HostBindings
45.2 ms x1714 RouterLink:ngOnDestroy
Okuması kolay: ilk tuştaki change detection turu 367.7 ms sürmüş ve Main track'teki 368.4 ms'lik Animation frame fired ile neredeyse birebir örtüşüyor; yani o frame'deki işin tamamı Angular'ın. BookListSlow_Template (update), template'in binding'lerini güncellerken visibleBooks()'u iki kez (sayaç ve @for için) çağırdığı yer. 1714 kez RouterLink:ngOnDestroy, silinen her kartın içindeki routerLink directive'inin temizliği: bir kart component'inin içine koyduğun her directive, liste büyüdüğünde çarpan olarak geri döner. Dokümana göre track'teki renkler de anlamlı: senin TypeScript kodun, derlenmiş template kodun ve uygulamaya giriş noktaları farklı renklerde çizilir. Track'in boş olduğu aralıklarda Main track'te iş varsa, o iş Angular'ın değildir: layout, paint ya da üçüncü taraf bir script'tir.
Chrome 153–154 ile flame chart'ta Ctrl+F, extensibility API'siyle eklenen custom track'lerde de arama yapıyor; BookCard yazıp tüm kart kontrollerini vurgulayabilirsin. Angular DevTools kuruluysa bir component kaydının Summary'sindeki link onu doğrudan Angular DevTools'ta açar; bunun için dokümanın belirttiği bir Chrome flag'i gerekiyor. Angular DevTools'u bu serinin 09. yazısında ele alıyoruz.
Insights: Trace'i DevTools'a Yorumlatmak
Kaydın solundaki Insights sekmesi trace'i senin yerine tarar ve LCP breakdown, INP breakdown, layout shift culprit'leri, render-blocking istekler gibi başlıklarla bulguları listeler. Aynı insight motorunu kullanan chrome-devtools-mcp ile production build'de, 4x yavaşlatmayla yavaş listeye yazarken kaydettiğimiz trace'in INP breakdown'u:
Metrics (lab / observed):
- INP: 140 ms, event: (eventKey: s-6213, ts: 769234957450)
- CLS: 0.00
...
The longest interaction on the page was a `keypress` which had a total duration of `140 ms`. The timings of each of the three subparts were:
1. Input delay: 0.6 ms
2. Processing duration: 29 ms
3. Presentation delay: 110 ms.
Burada tuşlar arasında bekleme olmadan yazdık, bu yüzden sayı ilk ölçümden farklı; ama tablo aynı: süre presentation delay'de. Insight bunu üç alt parçaya ayırıyor ve en büyüğünden başlamanı söylüyor. Sayfa yüklemesi için de Record and reload ile kaydettiğimiz trace'te LCP 508 ms idi: TTFB (Time To First Byte) 7 ms, render delay 501 ms. Liste görünür olmadan önce 2000 kartlık ilk render'ın beklendiğini söyleyen bir tablo. Insight listesinin başında LCPBreakdown, RenderBlocking ve DOMSize vardı; sonuncusu yine 2000 kartlık DOM'u işaret ediyor.
Chrome 153–154 ile Insights, soft navigation'ları da analiz ediyor. Angular'da router ile sayfa değiştirmek tam sayfa yüklemesi değil, bir soft navigation; artık /books'tan /cart'a geçişin de ayrı bir insight seti olabiliyor.
Düzeltme: computed Signal ve @for track
Düzeltilmiş sürüm iki değişiklik yapıyor:
@for (book of visible(); track book.id) {
<app-book-card [book]="book" (addToCart)="cart.add($event)" />
}
private readonly index = computed(() => this.books().map(book => ({ book, key: normalize(book.title) })));
protected readonly visible = computed(() => {
const q = normalize(this.query());
return this.index().filter(e => e.key.includes(q)).map(e => e.book);
});
Pahalı normalize artık kitap başına bir kez, liste geldiğinde çalışıyor ve sonuç index computed'ında cache'leniyor. Her tuşta yalnız includes kalıyor ve visible bir computed olduğu için template onu iki kez okusa da hesap bir kez yapılıyor. track book.id, Angular'a satırları kimlikleriyle eşleştirmesini söylüyor: filtre daraldığında kalan kartların DOM'u ve component instance'ları yeniden kullanılıyor. Yukarıdaki ölçümde sonraki tuşların 200 ms'den 40–48 ms'ye inmesi bunun sonucu. İlk tuştaki 400 ms'nin kaynağı ise hâlâ 2000 kartlık DOM; onu çözmek render'ı küçültmekle olur: sayfalama, virtual scroll ya da @defer. Bu stratejilerin kendisi ilerideki Web Performansı serisinin konusu; debounce ve layout thrashing JavaScript İleri serisinin "JavaScript Performans İpuçları: Debounce, Throttle ve Layout Thrashing" yazısında.
Üretimde Dikkat
Lab ölçümü bir hipotezdir. Bizim 20x sonuçlarımız Apple M5 işlemcide alındı; kullanıcının telefonundaki gerçek INP'yi CrUX ya da kendi RUM (Real User Monitoring) verin söyler. Bir düzeltmeyi lab'da doğruladıktan sonra field verisinde de iyileştiğini görmeden "çözüldü" deme.
Angular track ve ng global'leri yalnız development build'de var, ama development build production'dan belirgin şekilde yavaş: aynı 4x yavaşlatmada yavaş listenin en kötü etkileşimi trace kaydı açık dev build'de 568 ms, production'da 136 ms ölçüldü. Neyin yavaş olduğunu dev build'de bul, ne kadar yavaş olduğunu production build'de ölç.
Trace dosyaları büyük ve hassastır: bizim birkaç saniyelik kaydımız 45 MB'tı ve içinde URL'ler, script içerikleri ve ekran görüntüleri olabilir. Bir trace'i paylaşmadan önce ne içerdiğini düşün.
Sık Düşülen Tuzaklar
- Throttling'siz, güçlü bir geliştirici makinesinde ölçüp "INP iyi" demek.
- Processing kısa diye kodu temize çıkarmak: zoneless Angular'da render presentation delay'e düşer.
- Development build'in mutlak sürelerini production'a taşımak.
- Template'te method çağırmak: her change detection'da yeniden çalışır; türetilmiş değer için computed kullan.
track $indexile liste filtrelemek: kartlar kimlikleriyle değil sıralarıyla eşleşir.- Yarım dakikalık bir kayıt alıp içinde kaybolmak: yalnız ilgili etkileşimi kaydet.
Küçük Bir Deneme
★☆☆ Performance panelinin Live metrics ekranını aç, CPU throttling'i 4x yap ve /books-slow ile /books'ta filtreye yazarken Interactions listesindeki süreleri karşılaştır.
★★☆ Development build'de konsolda ng.enableProfiling() çağır, yavaş listeye tek bir harf yazarken trace kaydet. Angular track'te BookListSlow_Template (update) kaydını bul, flame chart'ta Ctrl+F ile RouterLink arayıp kaç kez göründüğünü say.
★★★ BookCard'daki routerLink'i kaldırıp yerine liste seviyesinde tek bir click handler ile navigasyon yap. Aynı ölçümü 20x'te tekrarla ve ilk tuşun INP'sinin ne kadar değiştiğini, Angular track'teki RouterLink:ngOnDestroy kayıtlarının kaybolduğunu doğrula.
Performance paneli zamanı ölçer; ama bazı sorunlar zamanla değil, bellekle büyür. Kitap detay sayfasına girip çıktıkça sekmenin yavaşladığını fark ettiysen sebebi burada değil. Bir sonraki yazıda Memory paneliyle heap snapshot'ları karşılaştırıp Angular component'lerinin neden bellekte kaldığını retainer zincirinden okuyacağız.