Angular Change Detection Strategy

Nahit Ferhat Ektaş
5 min readJun 10, 2023

--

Change detection mekanizması, Angular uygulamamızda yer alan component’ların değişimlerini takip eder ve bu değişimlere göre sayfaların güncellenmesini sağlar. Change detection yaşam döngüsü boyunca Angular, component’larda yer alan eski state ile yeni state’i kontrol eder ve eğer bir farklılık varsa bu farklılıkları DOM’ a yansıtır.

Change Detection mekanizması aşağıdaki durumlarda çalışır;

1- Component içerisinde bir event çağırıldığında(click, change gibi)

2- Component’lar da yer alan state’lerin güncellenmesinde.

3- @Input ile taşınan değerlerin güncellenmesi durumunda

Angular yukarıda yer alan işlemlerden biri meydana geldiğinde tüm component’ları kontrol eder ve değişiklik var ise sayfamız tekrar render edilir. Component’larda yapacağımız herhangi bir değişikliğin ilgili diğer component’lara yansıtılması sorumluluğunun Angular tarafında çözülüyor olması geliştiriciler için çoğu zaman kolaylık sağlayabilecek bir durum iken, bu kolaylık beraberinde zaman zaman uygulamamızın performansını etkileyecek bir duruma dönüşebilir. Bazı durumlarda yaptığımız değişikliklerin yalnızca component’e özel olmasını veya yalnızca bizim belirlediğimiz component’larda geçerli olmasını isteyebileceğimiz, kısacası sayfaların render edilme sürecini kendimiz yönetmek isteyeceğimiz senaryolar ile karşılaşabiliriz.

Angular bu noktada bize, ChangeDetectionStrategy.Default ve ChangeDetectionStrategy.OnPush olmak üzere 2 seçenek sunar.

ChangeDetectionStrategy.Default

Component ilk oluşturulduğunda eğer hiçbir konfigürasyon yapmazsak, Angular varsayılan olarak ChangeDetectionStrategy.Default olarak işlem yapar ve yukarıda da bahsettiğim gibi her asenkron değişiklikte tüm component’ları kontrol ederek sayfanın tekrar render edilme sorumluluğunu kendisi üstlenir.

@Component({
selector: 'app-root',
template: `
<div>
<app-product
*ngFor="let product of productList"
[product]="product"
></app-product>
<p>{{ counter }}</p>
<div><button (click)="updateCounter()">Update Counter</button></div>
</div>
`,
})
export class AppComponent {
counter = 0;
productList: { id: number; name: string }[] = [
{ id: 1, name: 'First Product' },
{ id: 2, name: 'Second Product' },
{ id: 3, name: 'Third Product' },
];
updateCounter(): void {
this.counter = this.counter + 1;
}
}

Yukarıdaki örnekte bir productList’imiz ve counter adında bir değişkenimiz var. ProductList AppComponent’inin içerisinde ngFor ile dönüyor ve her bir product’ı ProductComponent’ine gönderiyor. Bundan bağımsız olarak bir button’umuz var ve her tıklandığında counter degerini 1 arttırıyor ve sayfayı yeniden render ediyor.

Product Component:

@Component({
selector: 'app-product',
template: `
<div>
<h2>Product Name: {{ product?.name }}</h2>
</div>
<div>Check render: {{ checkRender() }}</div>
`
})
export class ProductComponent {
@Input() product: { id: number; name: string } | undefined;

checkRender(): boolean {
console.log('checkRender');
return true;
}
}
Örneğin ilk hali

Product Component ise, parent component dan gelen product’ı alarak sayfada gösteriyor. Tüm bu işlemler sonucunda sayfanın ChangeDetectionStrategy.Default yöntemi ile kaç kere render edildiğine checkRender() fonksiyonunu kullanarak bakalım.

ProductList de 3 tane product olduğu için her bir product oluştuğunda component’imizin 3 kere render edilmesini gerekir. Ancak yukarıdaki çıktıya baktığımızda toplamda 6 kez render işlemi gerçekleştiğini görüyoruz. Bunun sebebi Default Detection Strategy uyguladığımızda Angular, önce component’ı oluşturmak için bir render işlemi yapar daha sonrasında checkRender() fonksiyonunun çalışması sonucunda component’ımıza bir etki yaratıp yaratmadığı kontrolünü yaptığı için render işlemi her bir component için tekrar gerçekleşir. Buna ek olarak product listesi ile hiç bir alakası olmayan AppComponent da yer alan updateCounter() fonksiyonunu her çağırdığımızda render sayısı counter sayımıza göre artış gösterecek.

Product ile hiç bir alakası olmadığı halde updateCounter() fonksiyonu ile counter 2 defa arttırıldığında render sayısı 6 kere daha artmış oldu. Bunun sebebi Angular herhangi bir değişiklik olma durumunu göz önünde bulundurarak ProductComponent’ımızı tekrar render etti. Böylesine basit bir uygulamada bile sayfamızın defalarca render edildiğini düşünürsek, daha büyük uygulamalarda render sayısı artarken uygulamamızın performansı azalacaktır. Angular tüm component’lerimizi çok kısa bir süre içerisinde karşılaştıracak kadar performanslı olsa da sonuçta her işlemin bir maliyeti olacak ve uygulama büyüdükçe performans sıkıntılarını da beraberinde getirecektir.

ChangeDetectionStrategy.OnPush

OnPush detection strategy’yi component’ımıza tanımlarsak artık sayfamızda dirty checking gerçekleşmez. Yani default detection strategy’nin aksine component’imiz sürekli render edilmez, render edilme süreçlerini daha fazla kontrol altına almamıza olanak tanır.

Component’imiz OnPush ile konfigüre edildiğinde aşağıdaki durumlarda yeniden render edilir.

1- Gönderdiğimiz @Input parametrelerinin referansını değiştirdiğimizde render işlemi gerçekleşir. Eğer referans değişmez ise, sayfa yeniden render edilmez.

2- Observable’lara yeni bir değer emit edilmesi ve bu observable’ların async pipe olarak kullanması durumunda.

3- Kendisi veya child component’lardan event call edilmesi.

4- ChangeDetectorRef ile manuel olarak tetiklenmesi.

@Component({
selector: 'app-product',
template: `
<div>
<h2>Product Name: {{ product?.name }}</h2>
</div>
<div>Check render: {{ checkRender() }}</div>
`,
changeDetection:ChangeDetectionStrategy.OnPush
})
export class ProductComponent {
@Input() product: { id: number; name: string } | undefined;

checkRender(): boolean {
console.log('checkRender');
return true;
}
}

Tekrar aynı örnekten devam edecek olursak, change detection strategy’imizi değiştirmek istediğimizde tek yapmamız gereken, @component içerisine changeDetection:ChangeDetectionStrategy.OnPush eklemek olacaktır. Şimdi tekrar render sayılarına bakalım.

Başlangıçta 3 component’ımız için, beklediğimiz üzere 3 kere render edildi.

updateCounter() fonksiyonunun 2 kere çağırılmasına ve counter değişkeninin 2 olmasına rağmen ProductComponent’ımız tekrar render edilmedi. Kısacası counter da yer alan değişiklik ProductComponent’inin render sürecini etkilememiş oldu.

Şimdi ProductComponent’ımızı nasıl render ettiğimize bir bakalım.

@Component({
selector: 'app-root',
template: `
<div>
<app-product
*ngFor="let product of productList"
[product]="product"
></app-product>
<p>{{ counter }}</p>
<div><button (click)="updateCounter()">Update Counter</button></div>
<div><button (click)="updateProduct()">Update Product</button></div>
</div>
`,
})
export class AppComponent {
counter = 0;
productList: { id: number; name: string }[] = [
{ id: 1, name: 'First Product' },
{ id: 2, name: 'Second Product' },
{ id: 3, name: 'Third Product' },
];
updateCounter(): void {
this.counter = this.counter + 1;
}

updateProduct(): void {
//Component render edilmez.
this.productList[0].name = 'Update Product';
//Component tekrar render edilir
this.productList[0] = { ...this.productList[0], name: 'Update Product' };
}
}

Parent component’in içerisinde updateProduct() adında bir fonksiyonumuz var, bu fonksiyon productList’in 0'ıncı elemanının name field’ını, view daki button’ a tıklandığında ‘Update Product’ yapıyor. Burada dikkat edilmesi gereken konu update işleminin nasıl yapıldığı olacaktır. Eğer objemizi güncellersek yani mutate edersek child component’imiz değişikliği anlamayacaktır. OnPush kullandığımızda Input’larımız yalnızca referansı değiştiğinde, değişimden haberdar olacak ve sayfayı yeniden render edecektir.

--

--