Tips to Improve AngularJS Performance

AngularJS 성능향상을 위한 11가지 팁

angular를 처음 접한지도 이제 2년 가까운 시간이 흘렀습니다. 잘(?) 써야지 하면서도 아직 디테일한 부분에선 버벅거리고, 항상 API Docs를 뒤적이고 있네요. :)
오늘 올리는 글은 전반적으로 angular 1.x 성능향상(얼마전 GDG Korea WebTech에서 하는 angular 2 스터디에 가 보았는데, 1.x와 2.x는 개념적으로 적지않게 다른듯 합니다. angular 2에서는 아직 테스트 해 보진 못했어요.)을 위한 팁 입니다. 회사 프로젝트에서 만들었던 웹을 angular 2로 마이그레이션 하기 전, 조금이나마 성능을 높일 수 있는 방법을 찾아보다, 다른분들과 공유하기에 괜찮은 아티클이 있어서 함께 나누고자 합니다. 짧은 글이니 한번 얼른 읽어 보시고, 한번 적용해 보시겠어요? :)
ps. 언제나 그렇듯이 그냥 편하게 읽으시라 번역한 글이니, 정확히 이해가 되시지 않으시다면 원문을 읽는것을 추천드립니다.
ps. angular 2가 이제 알파에서 베타 버전이 되었네요. 관심이 있으시다면 이곳도 한번 방문 해 보세요.

웹개발이 처음은 아니지만, Angular는 처음입니다. 그래서 제가 말하는 모든것들을 쉽사리 믿을 수 없다는것을 알아요. 하지만, 이 포스트는 많은 토론을 보고, Angular의 성능에 관련된 많은 기사를 읽은 조사결과의 요약입니다.

목차:

  1. watcher를 최소화 하거나 피하세요.
  2. ng-repeat을 피하세요. 만약 여러분이 ng-repeat을 사용해야만 한다면 무한 스크롤이나 페이지네이션을 사용해야합니다.
  3. 가능하다면 일회성 바인딩을 사용하세요.
  4. $watch 대신에 $watchCollection을 사용하세요. (3번째 파라메터와 함께)
  5. 가능하다면 반복되는 필터와 캐시 데이터를 피하세요.
  6. ng-model의 debounce
  7. ng-show 대신에 ng-if를 사용하세요. ( ng-if가 실제로 사용하는데 더 나은 방법임을 보여줍니다 )
  8. 함수를 벤치마킹하기 위해서 console.time을 사용하세요.
  9. 느린 함수를 위해 네이티브 자바스크립트나 Lodash를 사용하세요.
  10. 여러분 watcher의 벤치마킹을 위해 Batarang을 사용하세요.
  11. 성능의 병목지점을 확인하기위해 크롬의 Timeline과 profiler를 사용하세요.

1. watcher를 최소화하거나 피하세요 ( Minimize / Avoid watchers )

일반적으로 여러분들의 angular 앱이 느리다면, 그것은 watcher가 매우 많거나 그 watcher들이 할 수 있는 것보다 더 많은 일을 하는 경우를 의미합니다.

angular는 앱의 모든 변화를 기록하기 위해 지저분한 체크(dirty checking)을 사용합니다. 이것은 업데이트가 필요할 때(digest 주기 호출) 모든 watcher들을 체크하기 위해 살펴보아야 함을 의미죠. 만약 어떤 watcher가 다른 watcher와 의존관계가 있다면, angular는 모든 변경들이 전파되었음을 확인하기위해 digest 주기를 다시 실행합니다. 모든 watcher들이 업데이트되고 앱이 안정화되기 전까지 이것은 계속 반복됩니다.

현대의 브라우저들이 매우 빠르게 자바스크립트를 실행함에도 불구하고, angular에서는 여러분들의 앱을 기어갈 정도로 느리게 만들, 매우 많은 watcher를 추가하는것은 아주 쉽습니다.

angular 앱을 구현하거나 리팩토링 할 때 아래의 내용을 명심하세요.

http://www.codelord.net/2014/06/17/angular-performance-101-slides/

  1. watchers 설정:
  • $scope.$watch
  • {{}} 타입의 바인딩
  • 대부분의 디렉티브 (예. ng-show)
  • 스코프 변수 scope : { bar : ‘=‘ }
  • 필터 {{ value | myFilter }}
  • ng-repeat

2. Watchers (digest 주기) 실행:

  • 사용자 액선 (ng-click 등등). 대부분의 디렉티브 빌트에서 digest주기 완료가 $scope.apply를 호출합니다.
  • ng-change
  • ng-model
  • $http 이벤트( 모든 ajax 호출 )
  • $q 프로미스 해결
  • $timeout
  • $interval
  • $scope.apply와 $scope.diges 수동호출

2. ng-repeat을 피하세요. 만약 여러분이 ng-repeat을 사용해야만 한다면 무한 스크롤이나 페이지네이션을 사용해야합니다. (Avoid ng-repeat. If you have to use ng-repeat use infinite scrolling or pagination)

이것은 우리 앱에서 얻을 수 있는 가장 큰 사항입니다. 이것에 대해서는 자세히 설명하지는 않을꺼예요. 대신, 이를 주장하는 매우 도움이 되는 글을 발겼했거든요.

http://www.williambrownstreet.net/blog/2013/07/angularjs-my-solution-to-the-ng-repeat-performance-problem/

게다가 무한 스크롤을 위해서, 가능하다면 반듯이 추적(track)을 사용하도록 합니다.

https://docs.angularjs.org/api/ng/directive/ngRepeat#tracking-and-duplicates

예를 들어, 유일한 단계별 id는 ng-repeat을 동작할 때 추적을 하기 위한 좋은 값입니다.

<li ng-repeat = “Task in Tasks track by Task.Id></li>

3. 가능하다면 일회성 바인딩을 사용하세요. ( Use Bind once when possible )

angular 1.3 버전에서는 일회용 바인딩을 허용하기위해 :: 표기법을 추가했습니다. 요약하자면, angular는 digest주기의 첫번째 시리즈 이후의 어떤 값이 변하지 않을때까지 기다립니다. 그리고 Dom 엘리먼트를 표현하기위해 그 값을 사용합니다. 그 후 angular는 바인딩에 대해서 잊고 watcher를 제거합니다.

https://code.angularjs.org/1.3.15/docs/guide/expression#one-time-binding

만약 여러분이 angular 1.3 이전버전을 사용한다면, 비슷한 결과를 위해 이 라이브러리를 사용할 수 있습니다.

https://github.com/Pasvaz/bindonce

4. $watch 대신에 $watchCollection을 사용하세요. (3번째 파라메터와 함께) (Use $watchCollection instead of $watch (with a 3rd parameter))

2개의 파라미터만을 가지는 $watch는 빠릅니다. 그렇지만 angular는 이 함수에서 3번째 파라미터를 지원합니다. 그것은 $watch(‘value’, function(){}, true)와 같이 표현합니다. 3번째 파라미터는 angular에게 깊은 체크(deep checking)를 실행하라고 말하는데, 이는 오브젝트의 모든 속성들까지 체크하라는 의미입니다. 이것은 매우 많은 자원이 소비됩니다.

이 성능문제를 해결하기위해, angular는 $watchCollection(‘value’, function(){})을 추가했습니다. $watchCollection은 오브젝트의 모든 속성들의 첫번째 레이어만을 체크한다는 점만 제외하면, 3번째 파라미터가 있는 $watch와 거의 비슷하게 동작합니다. 따라서 성능이 크게 향상됩니다.

공식문서 :

https://code.angularjs.org/1.3.15/docs/api/ng/type/$rootScope.Scope#$watchCollection

도움이되는 블로그 포스트 :

http://www.bennadel.com/blog/2566-scope-watch-vs-watchcollection-in-angularjs.htm

5. 가능하다면 반복되는 필터와 캐시 데이터를 피하세요. ( Avoid repeated filters and cache data whenever possible )

one-time 바인딩은 필터와는 잘 동작하지 않는것으로 보입니다. 그것을 동작하게 만드는것을 피하며 일하는것 같습니다. 하지만 제 생각에는 그것은 말끔하고, 단순히 변수에 값을 할당하는것 보다 더 직관적입니다. ( 아니면 어떤 오브젝트의 속성으로 설정하거나, 많은 변수를 처리하는 경우 )

예를들자면, 아래의 표기 대신에

{{ ‘DESCRIPTION’ | translate }}

여러분은 이렇게 할 수 있습니다.

  • 자바스크립트
 $scope.description : $translate.instant(‘DESCRIPTION’)
  • HTML
 {{::description}}

아니면 이렇게요

{{ step.time_modified | timeFormatFilter }}
  • 자바스크립트
var timeFormatFilter = $filter(‘timeFormatFilter’);
step.time_modified = timeFormatFilter(step.time_modified);
  • HTML
 {{ step.time_modified }}

6. ng-model의 debounce (Debounce ng-model)

만약 여러분이 어떠한 ng-model에 많은 변화가 있을 것을 알고 있다면, 입력을 디바운스(debounce, 모델 업데이트 시간제어를 의미합니다., 단위는 ms) 할 수 있습니다.

예를 들어 구글과 같은 검색 입력창이 있다면, 여러분은 ng-model의 옵션으로 디바운스를 다음과 같이 설정 할 수 있습니다.

ng-model-options=“{debounce:250}”

이것은 이 입력 모델 변화에 의한 digest주기가 250ms마다 한 번만 발생되는것을 보장합니다.

https://docs.angularjs.org/api/ng/directive/ngModelOptions

7. ng-show 대신에 ng-if를 사용하세요. ( ng-if가 실제로 사용하는데 더 나은 방법임을 보여줍니다 ) (Use ng-if instead of ng-show (but confirm that ng-if is actually better for your case ))

ng-show는 엘리먼트를 표현하고 display:none을 통해 그것을 숨깁니다.

ng-if는 DOM의 엘리먼트를 실제로 제거하고 필요할 때 다시 생성합니다.

여러분들은 자주 off로 전환하는 엘리먼트 때문에 ng-show가 필요할 수도 있습니다. 하지만 95%의 경우가 ng-if로 가는것이 더 좋은 방법입니다.

8. 함수를 벤치마킹하기 위해서 console.time을 사용하세요. ( Use console.time to benchmark your functions )

console.time은 훌륭한 API입니다. 그리고 angular 성능과 관련된 이슈를 디버깅 할 때, 특별하게 도움이 된다는것을 발견했습니다. 저는 제 리팩토링이 성능을 개선하는것이 사실인가를 확인하는것을 돕기위해 내 코드 도처에 다수의 console.time을 배치했습니다.

https://developer.mozilla.org/en-US/docs/Web/API/Console/time

API는 아래와 같습니다.

console.time(“TimerName”);
//Some code
console.timeEnd(“TimerName”);

아래는 간단한 예제입니다.

console.time(“TimerName”);
setTimeout(function(){
console.timeEnd(“TimerName”);
}, 100);
//In console $: TimerName: 100.324ms

만약 console.time이 여러분들이 원하는 만큼 충분히 정확하지 않다면, performance.now()를 읽고 사용함으로써 더 정확한 결과를 얻을 수 있습니다. 이 방법을 선택하는경우라면 결과계산은 직접 하셔야 합니다.

https://docs.google.com/presentation/d/15XgHRI8Ng2MXKZqglzP3PugWFZmIDKOnlAXDGZW2Djg/edit#slide=id.g10d2b49c1_0143

totalTime = 0; count = 0;
var someFunction = function() {
var thisRunStartTime = performance.now();
count++;
// some code
// some more code
totalTime += performance.now() — thisRunStartTime;
};
console.log(“Average time: “ + totalTime/count);

9. 느린 함수를 위해 네이티브 자바스크립트나 Lodash를 사용하세요. ( Use native JavaScript or Lodash for show functions )

우리 앱들은 이미 lodash를 사용하고 있습니다. 그래서 저의 최적화에서는 lodash를 사용하기위해 별다른 조치는 필요하지 않았습니다. 만약 lodash가 포함되어있지 않았다면 아마도 네이티브 자바스크립트로 모든것들을 재작성했을 것입니다.

테스트에서 angular에 내장되어있는 믿을만한 메소드들 대신에, lodash와 기본로직 일부를 재작성하여 상당한 성능향상을 얻었습니다. ( 훨씬 더 일반적인 사용 사례를 설명해야합니다. )

https://jsperf.com/의 공동제작자이자 Lodash의 관리자인 John-Dalton은 퍼포먼스의 대가입니다. 그래서 전 그를 믿고, 속도에 관해서라면 그와 그의 라이브러리를 신뢰합니다.

10. 여러분 watcher의 벤치마킹을 위해 Batarang을 사용하세요. ( Use Beatarang to benchmark your watchers )

Batarang은 디버깅하는 수고에 매우 도움을 주는, Angular팀에서 제작한 훌륭한 툴입니다. 그것은 유용한 기능을 많이 가지고 있지만, 이 사용 사례와 가장 관련있는 하나는 성능탭 입니다.

반드시 대부분의 사용자들을 위해 동작하는 안정화 버전을 얻으세요.

https://chrome.google.com/webstore/detail/angularjs-batarang-stable/niopocochgahfkiccpjmmpchncjoapek

Batarang에 대해 좀 이해가 필요하시다면, 동영상을 시청하시구요.

11. 성능의 병목지점을 확인하기위해 크롬의 Timeline과 profiler를 사용하세요. ( Use Chrome Timeline and Profiler to identify performance bottlenecks )

저는 제 스스로 크롬 개발자 툴의 파워유저라고 생각하기를 좋아합니다. 단지 가끔 Timeline과 Profiler 화면을 사용하는것이 아닙니다. 이 프로젝트에서는 두가지 다 매우 유용합니다.

프로 팁 : console.time API를 사용하는 경우(팁 # 8 참조), 기간이 타임 라인 스냅 샷에서 강조 얻을 것입니다 . 그러면 여러분은 가장 관심있는 정확한 시간을 검사 할 수 있습니다.

https://developer.chrome.com/devtools/docs/timeline#user-produced-timeline-events

timeline 화면과 마법같은 60fps의 라인은 매우 중요합니다. 저희의 프로젝트를 시작했을 때, 애플리케이션은 사용자에게 거의 완벽히 응답하기까지 풀 스트림으로 표현하는데 15초 이상이 걸렸습니다.

성능 최적화를 한 뒤에는, 애플리케이션은 이제 완전히 표현하는데 2초 이내였고(시간축척이 다름을 유의), 비교적 짧은 지연 후에 사용자가 UI와 자유롭게 상호작용할 수 있도록 되었습니다.

이미지를 살펴 본다면 애플리케이션을 더 최적화 할 수 있다는 것이 확실합니다. 하지만 저는 있는 그대로 사용자 경험의 발전에 매우 만족합니다.

timeline 화면으로 더 많은 경험을 얻으려면, Paul Irish의 이러한 웹 검사를 확인하시기 바랍니다.

https://docs.google.com/document/d/1K-mKOqiUiSjgZTEscBLjtjd6E67oiK8H2ztOiq5tigk/pub

마지막으로, 크롬 개발 툴에 있는 Profiling탭, 특히 자바스크립트의 CPU 프로파일러의 3가지 화면에 대해서 언급해 봅니다.

  1. Chart화면은 timeline화면과 비슷합니다. 하지만 chart는 좀 더 쉽게 관심있는 함수의 소스코드로 점프 할 수 있습니다.

2. Heavy (Bottom up view)

이 화면에서는 무거운 사용자 함수를 확인합니다. 그리고 함수의 핀포인트 시작에 도움이되는 역 호출 스택을 보여줍니다. $digest가 $apply앞에 오는 방법, 역순을 나타냅니다.

3. Tree (Top Down)

무거운 소비를 일으키는 함수를 노출시키고, 문제가 되는 함수를 찾기위해 더 세부적으로 파고들 수 있습니다.

또한 “!”가 있는 노란삼각형 위에 있는경우, 잠정적인 최적화 문제를 확인할 수 있습니다.

https://developer.chrome.com/devtools/docs/cpu-profiling

읽어주셔서 감사합니다. 에러를 발견하거나 안부를 전하실 분은 트윗터로 연락 주세요. : https://twitter.com/akras14