티스토리 뷰
https://medium.com/geekculture/10-tricks-to-optimizing-the-performance-of-angular-app-d998d48ca634 를 해석한 글입니다.
1. ChangeDetectionStrategy.OnPush
Change detection은 JS와 관련 프레임워크에서 가장 기본적인 기능 중 하나이며 데이터가 변경되었을 때 DOM을 업데이트하여 변경사항을 반영하는 기능이다. 앵귤러는 Zone.js를 사용하여 각 비동기 이벤트를 monkey-patch하므로, 이벤트가 발생할 때마다 앵귤러는 해당 컴포넌트 트리에서 change detection을 실행한다. 데이터가 크게 변경되지 않았지만 참조적으로 변하였을 때도 CD를 실행하면 성능저하가 발생하기 쉽다.
컴포넌트에 부모컴포넌트로부터 받아온 데이터를 사용하는 입력값이 있다. 비동기 이벤트가 발생했을 때, 앵귤러는 컴포넌트 트리를 분석하고 이전값과 차이가 있는지 확인한다. 이러한 차이를 확인하는것은 strict equality 연산자를 통해 수행한다. 이 연산자는 컴포넌트의 입력값에서 참조변화를 체크한다. 이는 입력값의 현재 값에 대해 새로 할당된 메모리이다.
ChangeDetectionStrategy {
OnPush,
Default, // default
}
OnPush 전략은 컴포넌트 및 자식 컴포넌트에서 CD를 실행하지 않도록 설정한다. 앱이 부트스트랩될 때 앵귤러는 OnPush 컴포넌트에 CD를 실행하고 사용하지 않도록 설정한다. 이후 CD 실행 시, OnPush 컴포넌트는 서브트리의 자식컴포넌트와 함께 스킵된다. CD는 입력값이 참조적으로 변경된 경우에만 OnPush컴포넌트에서 실행된다.
2. Detaching the Change Detector
앵귤러 프로젝트 트리의 모든 컴포넌트는 change detector를 갖는다. 그래서 change detector(ChangeDetectorRef)를 주입하여 CD트리에서 컴포넌트를 분리하거나 연결할 수 있다. 따라서 앵귤러가 컴포넌트 트리에서 CD를 실행할 때 서브트리에 있는 구성요소를 스킵한다.
// ChangeDetectorRef class
export abstract class ChangeDetectorRef {
abstract markForCheck(): void;
abstract detach(): void;
abstract detectChanges(): void;
abstract checkNoChanges(): void;
abstract reattach(): void;
}
markForCheck: 뷰가 OnPush(check once) change detection 전략을 사용하는 경우, 뷰에 changed로 명시적으로 표시하여 다시 확인할 수 있도록 한다. 컴포넌트는 입력값이 변경되거나 이벤트가 발생한 경우 일반적으로 dirty(재렌더링 필요)로 표시한다. 이러한 트리거가 발생하지 않은 경우에도 컴포넌트가 체크되었다는 것을 보장하기 위해 이 메서드를 사용한다.
detach: change detection tree에서 뷰를 분리한다. 분리된 뷰는 reattach될 때까지 체크하지 않는다. detectChanges()와 함께 사용하여 local change detection checks를 구현한다. 분리된 뷰는 CD가 실행되더라도 다시 reattach 때까지는 dirty로 표시되어 있어도 체크하지 않는다.
detectChanges: 뷰와 그 자식까지 체크한다. local change detection checks를 구현하려면 detach와 함께 사용한다.
checkNoChanges: CD와 그 자식을 체크하고 변화가 감지되면 버린다. 개발모드에서 다른 변경을 유발하지 않는지 확인하기위해 사용한다.
reattach: CD tree에 이전에 분리된 뷰를 다시 연결한다. 뷰는 디폴트로 트리에 연결된다.
@Compoennt({
...
})
class TestComponent {
constructor(private changeDetectorRef: ChangeDetectorRef) {
changeDetectorRef.detach()
}
}
constructor는 시작 포인트이기 때문에 constructor에서 detach()를 호출했다. 그러면 코드가 실행될 때 컴포넌트 트리에서 컴포넌트가 분리된다. 전체 컴포넌트 트리에서 실행되는 CD는 TestComponent에 영향을 주지 않는다. 컴포넌트에서 템플릿에 바인딩된 데이터가 바뀌면 컴포넌트를 다시 연결해야 다음 CD실행 시 DOM이 업데이트 된다. 그럼 다음과 같은 코드가 된다.
@Component({
...
template: `<div>{{data}}</div>`
})
class TestComponent {
data = 0
constructor(private changeDetectorRef: ChangeDetectorRef) {
changeDetectorRef.detach()
}
clickHandler() {
changeDetectorRef.reattach()
data ++
}
}
3. Local Change Detection
위에서 살펴본 컴포넌트의 detach()를 사용하면 컴포넌트 서브트리에서 실행되는 컴포넌트에서 CD를 트리거 할 수 있다.
@Component({
...
template: `<div>{{data}}</div>`
})
class TestComponent {
data = 0
constructor(private changeDetectorRef: ChangeDetectorRef) {
changeDetectorRef.detach()
}
}
데이터 바인딩된 데이터 속성을 업데이트하고, detachChanges()를 사용하여 TestComponent와 자식컴포넌트에 대해서만 CD를 실행할 수 있다.
@Component({
...
template: `<div>{{data}}</div>`
})
class TestComponent {
data = 0
constructor(private changeDetectorRef: ChangeDetectorRef) {
changeDetectorRef.detach()
}
clickHandler() {
data ++
changeDetectorRef.detectChanges()
}
}
clickHandler()는 데이터값을 1 증가시키고 detectChanges()를 호출하여 TestComponent와 자식컴포넌트에서 CD를 실행한다. 이렇게하면 CD트리에서 분리된 상태에서 DOM데이터가 업데이트 된다. 로컬CD는 루트에서 자식컴포넌트로 실행되는 글로벌CD와 다르게 컴포넌트에서 자식컴포넌트로 실행된다. 데이터 변수가 매초마다 업데이트된다면 엄청난 성능변화가 있을 것이다.
4. Run outside Angular
NgZone/Zone은 앵귤러가 컴포넌트 트리에서 CD를 실행할 시기를 알기위해 비동기 이벤트를 사용한다. 이를 통해 앵귤러에서 작성하는 모든 코드는 앵귤러영역에서 실행되며 이 영역은 Zone.js에 의해 생성되어 비동기 이벤트를 감지하고 이를 앵귤러에게 알려준다.
이제 외부 앵귤러영역에서 비동기이벤트는 더이상 NgZone/Zone에 의해 선택되지 않으므로 방출된 모든 비동기 이벤트에 대해 CD가 실행되지 않아 UI가 업데이트되지 않는다.
UI를 매초 업데이트 시키는 코드에서 매우 유용하다. UI 업데이트를 생략한 다음, 앵귤러영역에 다시 입력하기 원하는 데이터를 기다리는 것이 최적화임을 알 수 있다.
@Component({
...
template: `
<div>
{{data}}
{{done}}
</div>
`
})
class TestComponent {
data = 0;
done;
constructor(private ngZone: NgZone) {}
processInsideZone() {
if(data >= 100)
done = "Done"
else
data += 1
}
processOutsideZone() {
this.ngZone.runOutsideAngular(()=> {
if(data >= 100)
this.ngZone.run(()=> {data = "Done"})
else
data += 1
})
}
}
processInsideZone()은 앵귤러 내부에서 코드를 실행하므로 메서드가 실행될 때 UI가 업데이트 된다. 하지만 processOutsideZone()은 앵귤러영역 외부에서 코드가 실행되기 때문에 UI가 업데이트 되지 않는다. UI에 Done을 보여주기위해 데이터가 100 이상일때 Done을 표시하도록 ngZone에 data="Done"을 설정해준다.
5. Use pure pipes
Pure pipe는 side effects가 없기때문에 모든 동작을 예측할 수 있고 재계산을 피하기위해 shortcut CPU-intensive operation에 인풋을 캐시해둘 수 있다.
결과를 얻는데 상당한 시간이 걸리는 @Pipe함수가 있다고 상상해보자.
function bigFunction(val) {
...
return something
}
@Pipe({
name: "util"
})
class UtilPipe implements PipeTransform {
transform(value) {
return bigFunction(value)
}
}
이 함수는 UI를 실행하는 메인스레드를 멈추게하고 사용자들에게 지연을 유발할것이다. 더 나빠진다면 파이프는 매초 호출되어 사용자들이 지옥같은 경험을 하게 만들것이다.
파이프가 호출되는 횟수를 줄이기위해 파이프가 파이프외부에서 데이터를 변경하지 않은 경우(파이프가 순수함수인 경우), 파이프의 동작에 먼저 주목해야한다. 결과를 캐시하고 다음에 동일한 입력이 입력될 때 캐시된 값을 리턴해준다. 따라서 인풋을 받아 파이프가 얼마나 많이 호출되는가에 상관없이 bigFunction()은 한번만 호출되고 이후 호출에서는 캐시된 값이 리턴된다.
이 동작을 추가하려면 @Pipe데코레이터에 pure:true를 추가해야한다.
function bigFunction(val) {
...
return something
}
@Pipe({
name: "util",
pure: true
})
class UtilPipe implements PipeTransform {
transform(value) {
return bigFunction(value)
}
}
이를 통해 해당 파이프가 순수하고 사이드이펙트가 없음을 앵귤러에 알리고, 파이프는 결과를 캐싱하여 인풋이 다시 발생했을때 캐싱된 결과를 리턴한다. 이를 통해 모든 인풋에 대해 bigFunction은 한번 계산하여 캐시해두고 같은 인풋으로 호출되면 bigFunction의 재계산을 스킵하고 캐시된 결과를 리턴한다.
6. Use trackBy option for *ngFor the directive
*ngFor 은 iterable을 반복하고 DOM에서 렌더링하는데 사용된다. 매우 유용하지만 성능의 병목현상을 유발한다. 내부적으로 ngFor는 differs를 사용하여 iterable에 변경사항이 있는 경우 다시 렌더링한다. differs는 엄격한 참조 연산자 === 을 사용하여 객체참조(메모리주소)를 확인한다. 이것은 불변성과 결합하여, ngFor 각 interable 항목에서 객체의 참조를 끊어 지속적으로 DOM의 파괴와 재생성을 유발한다. iterable이 10~100개라면 문제가 되지 않지만 1000개가 넘어간다면 UI스레드에 큰 영향을 끼칠것이다. ngFor에는 trackBy옵션(Differs를 위한 옵션)이 있는데 iterable에서 요소id를 추적하는데 사용된다.
iterable에 Differ를 추적하기 위해 id를 명시할 수 있다. trackBy를 사용하면 전체 DOM이 지속적으로 파괴되고 재생성되는것을 방지할 수 있다.
@Component({
selector: 'my-app',
template: `
<li *ngFor="let item of items; index as i; trackBy: trackByFn">...</li>
`
})
export class MyApp {
items:[];
trackByFn(index, item){
return item.id;
}
7. Optimize template expressions
@Component({
template: `
<div>
{{func()}}
</div>
`
})
class TestComponent {
func() {
...
}
}
CD가 TestComponent에서 실행될때 func()가 실행될 것이다. 또한 func()는 CD와 다른 코드들이 이동하기 전에 완료되어야 한다. 다른 UI 코드가 실행되기 전에 func가 완료되어야 하는데 완료하는데 시간이 오래걸리면 사용자에게 지연된 UI가 보이게 된다. 그러므로 템플릿 표현식은 빠르게 완료되어야하고 템플릿 표현식의 계산량이 많아지면 캐싱을 사용해야 한다.
8. Web Workers
Js는 싱글스레드 언어라 JS코드는 메인스레드에서 실행된다. 메인스레드는 알고리즘과 UI알고리즘을 실행한다.
non-UI알고리즘이 무거워지면 UI스레드에 영향을 주어 속도가 느려지는 것을 알 수 있다. 웹워커는 또 다른 스레드에서 코드를 생성하고 실행할 수 있도록 추가된 기능이다. 앵귤러에서 웹워커를 사용하면 CLI도구를 통해 설정, 컴파일, 번들링, 코드분할을 쉽게 수행할 수 있다. 웹워커를 생성하기 위해 아래 커맨드를 실행한다.
$ ng g web-worker webworker
그럼 앵귤러앱의 src/app에 webworker.ts파일이 생성될 것이다. web-worker는 CLI도구에 워커에 의해 저 파일을 사용할 것이라고 알려준다.
앵귤러에서 성능을 최적화하기위해 어떻게 웹워커를 사용해야 하는지 알아보자.
피보나치 수를 계싼하는 앱이 있을 때 DOM스레드에서 피보나치 숫자를 찾는 것은 숫자를 찾을 때까지 DOM과 사용자사이의 인터렉션이 중단되기 때문에 UI가 느려진다. 처음 짠 코드가 이거라면
// webWorker-demo/src/app/app.component.ts
@Component({
selector: 'app',
template: `
<div>
<input type="number" [(ngModel)]="number" placeholder="Enter any number" />
<button (click)="calcFib">Calc. Fib</button>
</div>
<div>{{output}}</div>
`
})
export class App {
private number
private output
calcFib() {
this.output =fibonacci(this.number)
}
}
function fibonacci(num) {
if (num == 1 || num == 2) {
return 1
}
return fibonacci(num - 1) + fibonacci(num - 2)
}
피보나치 수 계산은 재귀적이며 0-900과 같은 작은 수를 계산하는 것은 성능에 영향을 미치지 않지만 10,000이 넘는다고 생각하면 성능저하가 발생하기 시작할 것이다. 가장 좋은 방법은 피보나치 함수 또는 알고리즘을 다른 스레드에서 실행하도록 하는 것이다. 그럼 숫자가 아무리 많아져도 DOM스레드에서는 성능저하가 발생하지 않을 것이다. 다음과 같이 웹워커를 사용하여 리팩토링 해보자.
// webWorker-demo/src/app/webWorker.ts
function fibonacci(num) {
if (num == 1 || num == 2) {
return 1
}
return fibonacci(num - 1) + fibonacci(num - 2)
}
self.addEventListener('message', (evt) => {
const num = evt.data
postMessage(fibonacci(num))
})
// webWorker-demo/arc/app/app.component.ts
@Component({
selector: 'app',
template: `
<div>
<input type="number" [(ngModel)]="number" placeholder="Enter any number" />
<button (click)="calcFib">Calc. Fib</button>
</div>
<div>{{output}}</div>
`
})
export class App implements OnInit{
private number
private output
private webworker: Worker
ngOnInit() {
if(typeof Worker !== 'undefined') {
this.webWorker = new Worker('./webWorker')
this.webWorker.onmessage = function(data) {
this.output = data
}
}
}
calcFib() {
this.webWorker.postMessage(this.number)
}
}
웹워커 파일로 웹워커를 초기화하기위해 ngOnInit 라이프사이클훅을 추가했다. 전달받은 모든 데이터를 DOM에 표시하기 위해 onmessage 핸들러에 웹워커가 보낸 메세지를 수신하도록 했다. calcFibo()를 만들어 웹워커로 number를 보내고(postMessage) 웹워커는 number를 받아(addEventListener) 피보나치 수를 처리하여 결과를 DOM스레드로 다시 보낸다(onmessage).
피보나치 수를 처리하는 동안 DOM스레드는 유저인터렉션에 집중하고 웹워커는 무거운 처리를 수행한다.
9. Lazy-Loading
레이지로딩은 브라우저에서 가장 인기있고 효과적인 최적화기술 중 하나이며, 로드시간에 리소스(이미지, 오디오, 비디오, 웹페이지) 로드를 필요할때까지 연기하는 것이 포함된다. 레이지로딩을 사용하면 웹페이지의 초기 로드시 로드되는 번들 파일의 양을 줄이고 웹페이지에서 직접 사용한 리소스만 로드하게 된다. 다른 모든 리소스는 로드되지 않으며 사용자가 필요로할 때 필요한 리소스가 로드된다.
앵귤러에선 레이지로드 리소스를 위해 아주 쉬운 방법이 제공된다.
const routes: Routes = [
{
path: '',
component: HomeComponent
},
{
path: 'about',
loadChildren: ()=> import("./about/about.module").then(m => m.AboutModule)
},
{
path:'viewdetails',
loadChildren: ()=> import("./viewdetails/viewdetails.module").then(m => m.ViewDetailsModule)
}
]
@NgModule({
exports: [RouterModule],
imports: [RouterModule.forChild(routes)]
})
class AppRoutingModule {}
앵귤러 라우터에 레이지로딩을 시키기 위해 dynamic import를 사용한다. 앵귤러는 about과 viewdetails에 대해 별도의 청크를 생성한다. 앱의 초기 로드 시, about과 viewdetails 청크는 로드되지 않고 사용자가 about 또는 viewdetails로 경로 이동을 하려는 경우 지정된 청크가 로드된다.
레이지로딩을 사용하지 않은 전체번들의 크기가 1MB이고 about은 300kb, viewdetails는 500kb인 경우, 레이지로딩을 사용하면 번들사이즈를 원래크기의 절반보다 더 작은 200kb로 줄일 수 있다.
10. Preloading
빠른 네비게이션 또는 소비를 위해 리소스를 로드하는 최적화 전략이다. 리소스가 이미 브라우저 캐시에 있기때문에 리소스 로드 및 렌더링 속도 모두 빨라진다. @angular/router모듈에 프리로딩이 구현되어 있고 앵귤러 앱에서 리소스, routes/link, 모듈 등을 미리 로드할 수 있다. 모든 클래스가 앵귤러에 프리로딩 전략을 추가하기 위해 구현된 추상클래스인 PreloadingStrategy를 앵귤러 라우터에서 제공한다
// our-preloading-strategy.component.ts
class OurPreloadingStrategy implements PreloadingStrategy {
preload(route: Route, fn: ()=> Observable <any>) {
// ...
}
}
// routing.module.ts
// ...
RouterModule.forRoot([
...
], {
preloadingStrategy: OurPreloadingStrategy
})
// ...
Conclusion
여기까지 앵귤러앱을 최적화하는 10가지 방법이였습니다. (파파고보단 구글번역기가 더 잘하네..)
'프로그래밍 > Angular' 카테고리의 다른 글
constructor와 ngOnInit의 차이점 (0) | 2022.03.16 |
---|---|
NgRx Entity (0) | 2021.12.07 |
ViewContainerRef를 사용한 Angular DOM manipulation (0) | 2021.06.19 |
[RxJS] 유용하게 사용하는 RxJS문법 추천 (2) | 2020.10.22 |
Angular 9와 Ivy (0) | 2020.10.22 |