Mert Özen Her Satırda Daha İleri
Geliştirici Araçları Browser DevTools — #09

Angular DevTools: Component Tree, Profiler, Injector Tree ve Router Tree

Mert Özen 8 Kas 2026 9 dk 3 görüntülenme
Angular DevTools: Component Tree, Profiler, Injector Tree ve Router Tree

Angular DevTools yalnız development build'de çalışır ve optimization: false ile alınan build de development modunda davranır: Service Worker kapanır, budget'lar aşılır. Components, Signal Graph, Profiler, Injector Tree ve Router Tree'yi gerçek verilerle okuyoruz: NG0201 hatasını route seviyesinde provider ile çözüp yeni Route: reviews injector'ını görmek ve OnPush'ta track book.id ile kart template güncellemelerinin 855'ten 0'a inmesi.

Sürüm notu: Bu yazıdaki örnekler Chrome 156 (156.0.8078.80), Angular 22.2 ve .NET 10 ile denenmiştir. Angular DevTools extension'ı bu ortamda (headless Chrome) yüklenip çalıştırılmadı; sekmelerin arayüzü ve adımları angular.dev'deki Angular DevTools dokümanından aktarıldı. Gösterilen veriler, extension'ın da okuduğu Angular debug API'lerinden (window.ng) ve Performance trace'lerinden, aynı Chrome sürümünde Chrome DevTools Protocol (CDP) üzerinden alındı.

Önce Tahmin Et

Bookstore'a kitap yorumları için bir sayfa ekliyoruz. Service providedIn olmadan yazılmış:

@Injectable()
export class ReviewService {
  readonly reviews = signal(['Clear and practical', 'Too long']);
}

@Component({
  selector: 'app-reviews-page',
  template: `
    <h2>Reviews</h2>
    @for (r of reviews.reviews(); track r) {
      <p class="review">{{ r }}</p>
    }
  `,
})
export class ReviewsPage {
  protected readonly reviews = inject(ReviewService);
}

/reviews'a gidince sayfa boş kalıyor. Konsolda hangi hata çıkar ve hata mesajı hangi injector'ı işaret eder? Service'i nereye provide edersen yalnız bu sayfaya özel olur? Bir de şu: ekip arkadaşın staging ortamındaki bu hatayı Angular DevTools'la incelemek istiyor; production build'de extension çalışır mı?

Kurulum ve Tek Şart: Development Build

Angular DevTools, Chrome Web Store'dan ve Firefox Add-ons'tan kurulan bir extension. Kurduktan sonra DevTools'u açınca panel sekmelerinin arasında "Angular" sekmesi çıkar. Chrome'un yeni sekme sayfasında extension'lar çalışmadığı için önce uygulamanın sayfasını aç. Dokümanın açık şartı: Angular DevTools yalnız development build'leri destekler. ng serve bunu varsayılan olarak sağlar; production build'de "Angular application not detected" ya da production build uyarısı görürsün.

Deploy edilmiş bir ortamı incelemek için dokümanın önerisi optimization: false ile build almak. Bunu production configuration'ına yazıp denedik ve iki sürprizle karşılaştık:

✘ [ERROR] bundle initial exceeded maximum budget. Budget 1.00 MB was not met by 500.21 kB with a total of 1.50 MB.

Optimize edilmemiş bundle production'ın budget'larını aştı ve build durdu. Ayrı bir configuration tanımladık:

"debug": { "optimization": false, "sourceMap": true, "outputHashing": "all" }

ng build -c debug çıktısını bir static sunucudan açınca:

typeof ng: object | SW controller: false | registrations: 0
   log: Angular is running in development mode.
   error: Access to fetch at 'http://localhost:5391/api/books' from origin 'http://localhost:5392' has been blocked by CORS policy

İkinci sürpriz: optimization kapalı bir build development modunda çalışıyor. ng global'i var, Angular DevTools bağlanabilir; ama isDevMode()'a bağlı her şey de development davranışına döndü: provideServiceWorker(..., { enabled: !isDevMode() }) yüzünden Service Worker kayıt olmadı, API adresini isDevMode()'a göre seçen kodumuz da localhost'taki dev API'ye gitti. Yani "production'ı Angular DevTools'la incelemek" aslında production'dan farklı davranan bir build'i incelemek demek. Uygulama davranışını isDevMode() yerine environment dosyalarıyla ayırmak bu farkı küçültür.

Components: Ağaç, Property'ler ve $ng0

Components sekmesinde uygulamanın component ve directive ağacı durur; üstteki arama kutusuyla ada göre süzersin. Bir öğeye tıklayınca sağda input'ları, output'ları ve diğer property'leri görünür; çift tıklamak host element'i Elements panelinde seçer. Sol üstteki inspect ikonu ile sayfada üzerine geldiğin element'in component'ini seçersin. Düzenlenebilir bir değeri değiştirip Enter'a basınca uygulama o değerle güncellenir. Properties bölümünün sağ üstündeki ikon component'in tanımını Sources panelinde açar.

Seçili component konsolda $ng0 olarak durur, önceki seçimler $ng1, $ng2. Bu, Elements'teki $0'ın Angular karşılığı; ama $0 DOM (Document Object Model) element'i, $ng0 component instance'ıdır. Bu serinin 02. yazısında ng.getComponent ve ng.getDirectiveMetadata ile aldığımız bilgilerin (input/output listesi, changeDetection: 0 yani OnPush) aynısını sekme görsel olarak gösterir.

@defer blokları da ağaçta görünür: seçince @loading, @placeholder, @error blokları, tetikleyicileri (defer, prefetch, hydrate) ve minimum/after zamanlamaları listelenir. SSR (Server-Side Rendering) ve hydration kullanan uygulamalarda ağaç her component'in hydration durumunu gösterir, hatalar ilgili component'in üzerinde mesaj olarak çıkar ve sayfanın üzerine hydration durumunu gösteren bir overlay açılabilir. Deneysel Transfer State sekmesi de SSR'da sunucudan istemciye taşınan state'i incelemek için; deneysel sekmeler Angular DevTools'un kendi ayarlarından açılır.

Signal Graph: Hangi Signal Neyi Besliyor?

Components sekmesinde bir component seçip property bölümünün başlığındaki Show Signal Graph'a tıklayınca component'in reaktif grafiği açılır. Doküman şartı Angular 19+ ve development modu. Extension'ın okuduğu veriyi BookList için doğrudan aldık (ng.ɵgetSignalGraph, private bir API; kodda kullanma):

signal graph of BookList → {"nodes":["0 template:app-book-list","1 signal:query=\"\"","2 computed:visible=[{\"id\":1,\"title\":\"Compilers Vo","3 computed:index=[{\"book\":{\"id\":1,\"title\":\"Comp","4 computed:toSignal.source=[{\"id\":1,\"title\":\"Compilers Vo","5 signal:toSignal.state={\"kind\":1,\"value\":[{\"id\":1,\"ti"],"edges":["1->0","2->0","1->2","3->2","4->3","5->4"]}

Grafiği oku: template (0) iki şeyi okuyor, query (1) ve visible (2). visible hem query'ye hem index'e (3) bağlı, index de toSignal'ın ürettiği iki node üzerinden HTTP (HyperText Transfer Protocol) yanıtına. Filtreye yazınca yalnız 1 → 2 → 0 yolu yeniden hesaplanır; index cache'te kalır. 06. yazıdaki düzeltmenin neden işe yaradığı bu grafikte bir bakışta görünüyor. Biz kodda hiç debugName yazmadık; development build'de node'lar property adlarıyla (query, visible, index) etiketlendi. Effect gibi bir property'ye atanmayan node'lar için debugName vermek grafiği okunur kılar.

Bir node'a tıklayınca güncel değeri, kaynağına gitme, breakpoint koyma ve güncellemelerini konsolda izleme seçenekleri gelir. Sonuncusu, 02. yazıda Object.keys(ng) çıktısında gördüğümüz toggleWatchSignal'ın arayüzdeki karşılığı.

Profiler: Hangi Component Neden Çalıştı?

Profiler sekmesinde sol üstteki daireye basıp kaydı başlatır, uygulamayla etkileşime girip tekrar basarak durdurursun. Her çubuk bir change detection döngüsüdür; yükseklik o döngüde harcanan süre. Bir çubuğu seçince o döngüdeki component'ler, toplam süre ve 60 fps'in (frames per second) altına düştüyse tahmini frame hızı görünür; bir component seçince çağrılan method'ları ve süreleri gelir. Flame graph görünümünde x ekseni döngünün toplam süresi, y ekseni element hiyerarşisidir. "Show only change detection" kutusu yalnız change detection'dan geçen component'leri gösterir; çalışmayanlar, örneğin kirli işaretlenmemiş OnPush component'ler, gri görünür. Kayıtları JSON (JavaScript Object Notation) olarak kaydedip başka bir makinede açabilirsin.

Angular 22'de component'ler varsayılan olarak OnPush; beklenti, filtreye yazınca yalnız input'u değişen kartların yeniden render edilmesi. Bunu Performance panelinin Angular track'iyle iki liste için ölçtük. Ölçüm dev build'de, CPU (Central Processing Unit) 4x yavaşlatılmışken, "Comp" yazarken dört change detection turunda alındı:

books-slow {'_BookCard': (2284, 241.9), 'BookCard_Template (update)': (855, 98.8), 'RouterLink:ngOnDestroy': (1714, 45.2), 'HostBindings': (867, 46.2)} CD passes 4
books {'_BookCard': (1429, 34.2), 'BookCard_Template (update)': (0, 0.0), 'RouterLink:ngOnDestroy': (1714, 44.2), 'HostBindings': (12, 1.2)} CD passes 4

Parantez içindeki sayılar kayıt adedi ve toplam milisaniye. track $index'li listede 855 kartın template'i güncellenmiş: filtre daralınca satırlar sıralarıyla eşleştiği için her satırın book input'u değişti ve OnPush onları durdurmadı. track book.id'li listede kartların template güncellemesi sıfır: Angular 1429 kez karta uğramış ama input'u değişmeyen OnPush kartları atlamış. Profiler'ın flame graph'ında "Show only change detection" açıkken bu kartlar gri görünür. 1714 RouterLink:ngOnDestroy ise iki listede de aynı: silinen kartların temizliği, track stratejisinden bağımsız.

Injector Tree: NullInjectorError ve Provider Scope

Önce Tahmin Et'teki sayfanın gerçek hatası:

ERROR ɵNotFound: NG0201: No provider found for `_ReviewService`. Source: Environment Injector. Find more at https://v22.angular.dev/errors/NG0201

Hata bir NullInjectorError ailesinden: zincirin hiçbir yerinde provider yok, ve arama en son Environment Injector'da bitmiş. Injector Tree sekmesi (Angular 17+) iki ağaç gösterir: environment hiyerarşisi ve element hiyerarşisi. Bir injector seçince Angular'ın o noktadan köke kadar izlediği çözümleme yolu vurgulanır; element injector'larda, element hiyerarşisinde bulunamayınca atlanan environment injector'lar da yola dahildir. Provider'ı olan bir injector seçince sağda token'lar ve tipleri listelenir, her biri tek tıkla konsola log'lanabilir.

Extension'ın gösterdiği yolu bir kitap kartı için kendimiz aldık:

resolution path from first app-book-card → ["NodeInjector","NodeInjector","NodeInjector","NodeInjector","R3Injector [Environment Injector]","R3Injector","NullInjector"]
providers in root environment injector (first 12 tokens) → {"count":80,"tokens":["ChangeDetectionScheduler","NgZone","InjectionToken Zoneless enabled","InjectionToken Set Injector scope.","ErrorHandler","InjectionToken EventManagerPlugins","InjectionToken EventManagerPlugins","DomRendererFactory2","InjectionToken SHARED_STYLES_HOST","SharedStylesHost","EventManager","RendererFactory2"]}

Yol önce element injector'lardan (kartın kendisi, BookList, App…) geçiyor, sonra uygulamanın root environment injector'ına, onun üstündeki platform injector'ına ve en sonda NullInjector'a varıyor. Root'ta 80 provider var, çoğu framework'ün kendisi. ReviewService'i yalnız bu sayfaya özel yapmak için route seviyesinde provide ettik:

{ path: 'reviews', component: ReviewsPage, providers: [ReviewService] },
--- /reviews (fixed) h2: Reviews
resolution path from app-reviews-page → ["NodeInjector","NodeInjector","NodeInjector","R3Injector [Route: reviews]","R3Injector [Environment Injector]","R3Injector","NullInjector"]

Yolda yeni bir halka var: Route: reviews. Route'a verilen providers o route ve çocukları için ayrı bir environment injector yaratır; service bu route ağacının dışından görünmez. Injector Tree'de bu halkayı görmek, "service neden iki kez oluşuyor" ya da "neden bulunamıyor" sorularının cevabını tahmin etmekten kurtarır. Injector hiyerarşisinin teorisi Angular serisinin "Angular Service ve Dependency Injection: providedIn, Injector Tree ve Provider'lar" yazısında.

Router Tree

Router Tree sekmesi uygulamanın route ağacını iç içe çizer; aktif route'lar yeşil vurgulanır. Bir route seçince yan panelde path ya da matcher, component ya da redirect hedefi, path match stratejisi, data, resolver'lar, provider'lar, canActivate/canActivateChild/canDeactivate/canMatch guard'ları, title ve active/auxiliary/lazy durumu görünür. Path'in yanındaki Navigate ikonu uygulamayı o adrese götürür. Extension'ın okuduğu router config'in bizdeki özeti:

router config → ["'' -> books","books","books-slow","books/:id","cart","reviews (providers: 1)","admin (lazy)"]

"Bu guard neden çalışmıyor" ya da "bu lazy route neden yüklenmiyor" sorularında önce burada route'un gerçekten beklediğin yerde ve beklediğin guard'la tanımlı olup olmadığına bak; route config'i dosyalara dağılmış büyük uygulamalarda en çok zaman kazandıran sekme budur.

Performance Paneliyle Birlikte Kullanmak

Angular DevTools'un Profiler'ı Angular'ın kendi gözünden, Performance panelinin Angular track'i ise tarayıcının bütün işiyle yan yana gösterir. Pratikte sıra şu: Performance'ta Angular track'i açık bir trace kaydet, long task'ın içinde hangi component'in olduğunu bul (06. yazı), sonra Profiler'da o component'in hangi döngüde neden çalıştığına bak. Angular dokümanına göre Performance panelindeki bir component kaydının Summary'sindeki link onu doğrudan Angular DevTools'un Components sekmesinde açar; bunun için extension'ın kurulu olması ve chrome://flags/#enable-devtools-deep-link-via-extensibility-api deneysel flag'inin açık olması gerekiyor.

Üretimde Dikkat

optimization: false ile deploy edilen bir build hem büyüktür (bizde 1.50 MB initial) hem de development modunda çalışır; bunu kullanıcıya açık bir ortama koyma. Debug build'i yalnız kısa süreli, erişimi kısıtlı bir ortamda kullan ve davranışının production'dan farklı olabileceğini (Service Worker, environment seçimi) unutma.

Angular DevTools uygulamanın bütün state'ini, service'lerdeki verileri ve route config'ini gösterir. Kişisel veri barındıran bir ortamda extension'la çalışırken ekran paylaşımına ve Profiler'dan export ettiğin JSON dosyalarına dikkat et.

Sık Düşülen Tuzaklar

  • Production build'de "Angular application not detected" görüp extension'ı bozuk sanmak.
  • optimization: false'ı production configuration'ına yazıp budget hatasıyla karşılaşmak; ayrı bir configuration tanımla.
  • Development modunda çalışan debug build'i production davranışının birebir kopyası sanmak.
  • Service'i bir component'in providers'ına koyup her instance'ta yeni bir service oluşmasına şaşırmak; Injector Tree'de nerede provide edildiğine bak.
  • $0 ile $ng0'ı karıştırmak: biri element, diğeri component instance.
  • OnPush varsayılan diye listenin kendiliğinden hızlı olmasını beklemek; track stratejisi kartların input'unu değiştiriyorsa OnPush durduramaz.

Küçük Bir Deneme

★☆☆ Angular DevTools'u kur, ng serve ile Bookstore'u aç ve Components sekmesinde bir BookCard seç. Konsolda $ng0.book() ile kitabı oku, sonra BookList'i seçip Signal Graph'ı aç ve filtreye yazarken hangi node'ların değiştiğini izle.

★★☆ ReviewService'i önce ReviewsPage'in providers'ına, sonra route'a, sonra providedIn: 'root''a taşı. Her seferinde Injector Tree'de service'in hangi injector'da göründüğünü ve /reviews'tan çıkıp girince yeni instance oluşup oluşmadığını not et.

★★★ Profiler'da /books-slow ve /books için ayrı kayıt al, "Show only change detection" açıkken flame graph'larda gri ve renkli BookCard'ları karşılaştır. Kayıtları Save Profile ile JSON'a kaydet ve bulduğun farkı yukarıdaki Angular track sayılarıyla eşleştir.

Bu yazıya kadar her aracı elle kullandık. Bir sonraki ve son yazıda iş değişiyor: DevTools'un içindeki Gemini AI (Artificial Intelligence) assistance'ı, coding agent'lara bu araçların hepsini açan Chrome DevTools MCP (Model Context Protocol) server'ını ve kullanıcı akışlarını kaydedip tekrar oynatan Recorder'ı, gizlilik ve güvenlik sınırlarıyla birlikte ele alıyoruz.