Nicholas Zakas는 Yahoo! 홈페이지의 프론트-엔드 최고(원문에 principle 이라고 되어있지만, principal의 오타로 간주했습니다.) 엔지니어이며, Javascript에 관한 권위자이기도입니다. 그는 최근 High Performance JavaScript라는 책을 발간하기도 했습니다.
그는 Javascript 퍼포먼스에 관해 매우 큰 관심을 가지고 있으며, 6월에 Google Tech Talk에서 ‘당신의 JavaScript를 빠르게 하라(Speed Up Your Javascript)’라는 주제로 발표를 하기도 했습니다.
Javascript 최적화라는 것이 모든 경우에 통용되는 것은 아니지만, Nicholas는 당신의 Javascript 코드를 정말로 날아갈 수 있게 만드는 10가지 팁을 공개하였습니다.
1. 지역변수를 정의하라
변수가 참조될 때 Javascript는 scope chain내의 다른 멤버들을 돌면서 해당변수를 찾습니다. 이 scope Chain은 현재 scope 내에서 사용 가능한 변수들의 모음이고, 대부분의 브라우저는 이것은 최소 두 가지 이상의 항목으로 구성하고 있습니다. 하나는 지역 변수들의 집합이고, 다른 하나는 전역변수들의 집합입니다.
이 때 엔진이 탐색해야할 scope chain의 깊이가 깊을 수록, 작업 시간이 더 오래 걸리게 될 것입니다. 엔진은 먼저 function의 인자인 this로 시작하는 지역변수들 부터 찾습니다. 그 다음에 지역적으로 정의된 변수들을 찾고, 그 후에 전역 변수들을 찾는 것을 반복합니다.
이 chain 내에서는 지역변수가 우선이기 때문에 그들은 항상 전역변수도 더 빠르게 발견됩니다. 그래서 한 번 이상 전역변수를 사용할 때는 항상 지역적으로 재정의를 해야합니다. 그래서 다음 예는 :
var blah = document.getElementById('myID'), blah2 = document.getElementById('myID2'); |
var doc = document, blah = doc.getElementById('myID'), blah2 = doc.getElementById('myID2'); |
2. with()문을 사용하지 마세요
다음은 매우 분명한 사실입니다 : with()문은 Javascript의 악마같은 존재입니다.
왜나하면 with()는 위에서 설명한 scope chain의 시작부분에 변수들의 추가적인 모음을 덧붙이기 때문입니다. 이 것은 JavaScript엔진이 변수들을 돌 때, 먼저 with() 변수들을 돌고, 다음에 지역 변수를 돌고, 다음으로 전역변수를 돌게 된다는 것을 의미합니다.
본질적으로 with()는 지역변수들에게 전역변수가 갖는 모든 단점을 가지게 만듭니다. 그리고 결국 Javascipt 최적화를 막는 요인이 됩니다.
3. closure 사용을 아껴라.
closure는 javascript의 매우 강력하고 유용한 특징입니다. 하지만 closure는 퍼포먼스의 저하를 함께 수반합니다.
아마 당신은 closure라는 용어는 모를지 모르지만, 알게 모르게 자주 사용했을 것입니다. closure를 JavaScript의 새로운 기능으로 생각할 수도 있습니다. 그렇지만 우리는 다음과 같은 방식으로 함수를 정의해왔습니다.
document.getElementById('foo').onclick = function(ev) { }; |
closure가 정의되면 최소 세 개의 객체를 scope chain 내에 존재하게 만듭니다 : closure변수, 지역변수, 전역변수. 이 추가된 변수들은 우리가 앞의 1,2 번 팁에서 피하고자 했던 퍼포먼스 문제를 일으키는 원인이 됩니다.
그렇지만 나는 Nicholas가 당장 closure를 버리라고 하는 것은 아니라고 생각합니다. 분명히, closure는 편의성에서나 가독성에서 유용함을 가지고 있습니다. 단지 과용이 돼서는 안 된다는 것을 말하고 있습니다. (특히, loop내에서)
4. 객체 프로퍼티 와 배열은 변수보다 느립니다.
Javascript의 자료에 접근하는 방법에는 크게 네 가지로 나뉩니다. 상수, 변수, 객체 프로퍼티, 배열. 최적화의 관점에서 본다면, 상수와 변수의 퍼포먼스는 거의 비슷하고, 객체와 배열의 퍼포먼스보다는 눈에 띄게 빠르다고 할 수 있습니다.
그렇기 때문에 당신이 객체 프로퍼티나 배열을 여러 번 참고하려고 한다면, 이를 변수에 정의해서 퍼포먼스를 향상시키는 것이 좋습니다.(이것은 정보를 읽고 쓸 때 모두에 해당됩니다.)
이 방법은 대부분의 경우에서 적용 가능하지만, Firefox는 array 인덱스 최적화라는 흥미로운 기능을 탑재했고, 실제 변수를 사용 시 보다 배열을 사용하는 경우의 퍼포먼스가 더 나은 모습을 보여줍니다. 그러나 다른 브라우저에서는 배열 사용 시의 퍼포먼스 문제로 인해 가급적 배열 활용을 피해하는 것이 더 낫습니다. 당신이 Firefox의 퍼포먼스만을 고려하지 않는다면 말입니다.
5. 배열의 깊은 레벨로 들어가지 마세요.
또한 배열의 너무 깊은 레벨까지 탐색하는 것도 피하는 것이 좋습니다. 깊게 들어가면 들어갈 수록 작업 속도는 느려질 것입니다.
단순하게 말하면, 배열 탐색 자체가 매우 느리게 이루어지기 때문에, 배열 안으로 들어가는 것 역시 느려질 수 밖에 없습니다. 생각해보세요. 당신이 배열의 세번째 레벨로 들어간다면 하나가 아닌 세 개의 배열을 검색해야만 합니다.
그래도 foo.bar를 탐색하고 싶다면, var bar = foo.bar와 같이 정의함으로서 퍼포먼스를 향상시킬 수 있습니다.
6. for-in 루프(와 함수 기반의 반복문)사용을 피하세요.
여기 또 하나의 명백한 사실이 있습니다 : for-in 루프를 사용하지 마십시오.
이유는 매우 단순합니다 : 인덱스를 따라 도는 for, do-while을 사용하는 대신에, for-in 문을 사용한다면, 추가적인 배열 객체를 돌게 될 뿐만 아니라, 더 많은 노력이 들어갑니다.
for-in에서 각각의 item들을 돌게 될 때, Javascript는 각각 마다 함수를 생성하게 됩니다. 이런 함수 기반의 반복문은 느린 퍼포먼스의 원인이 됩니다 : 생성되는 함수는 함수가 실행되는 컨텍스트와 함께 생성되고 파괴되며, 이 추가적인 객체는 scope chain에 더해지게 됩니다.
7, 반복문을 사용할 때 조건식과 증감식을 결합하라.
퍼포먼스 이슈에 있어서, 반복문 내의 작업량을 줄이는 것은 매우 중요한 문제입니다. 말 그대로, 반복문은 여러 번 반복되기 때문입니다. 만약 반복문 내의 퍼포먼스를 조금이라도 향상시킬 수 있다면, 반복문의 속도는 엄청나게 빨라질 것입니다.
조건식과 증감식을 결합시키는 것은 이런 향상을 가져올 수 있는 방법 중의 하나입니다. 여기에 둘을 결합시키지 않은 예제가 있습니다.
for ( var x = 0; x < 10; x++ ) { }; |
아직 반복문 안에 뭔가를 입력하진 않았지만, 이를 실행시키면 일련의 작업이 수행될 것입니다. JavaScript 엔진은 첫째로, x가 존재하는 지를 확인할 것이고, 두 번째로, x < 10 인지를 확인할 것이고, 세 번째로 x++를 수행할 것입니다.
그렇지만 단순히 배열 안에 있는 아이템을 반복시키려는 것이라면, 반복문을 수정해서, while 문을 사용함으로써 작업 하나를 줄이는 것이 가능합니다.
var x = 9; do { } while ( x-- ); |
Zakas는 반복문의 퍼포먼스를 한차원 상승시킬 수 있는. 더욱 발전된 반복문 최적화 테크닉을 알려주고 있습니다. 이 테크닉은 반복문을 비동기적으로 작동시키는 것입니다.(cool!)
8. HTML 콜랙션 객체에 대한 배열을 정의하라
JavaScript는 document.forms, document.images 등과 같은 document 콜랙센 객체를 많이 사용합니다. 또한 이런 것들은 getElementByTagName 이나 getElementByClassName 같은 메소드을 사용할 때 호출되기도 합니다.
DOM 셀렉션을 사용할 때처럼, HTML 콜렉션 객체는 매우 느리며, 또한 추가적인 문제를 일으킵니다. DOM level 1 명세에는 "HTML DOM 안의 콜렉션은 살아있는 것으로 가정하고, 이것은 document가 변경될 때마다 자동적으로 업데이트 된다는 것을 의미한다"라고 기술되어 있습니다.
이것은 끔찍한 일입니다.
콜랙센 객체는 array와 비슷해 보이지만, 큰 차이점이 있습니다 : 쿼리의 결과입니다. 객체를 읽고 쓰기 위해 접근 할 때 마다, 쿼리는 재실행되게 됩니다. 이 재실행에서 객체의 length와 같은 모든 사소한 부분들이 업데이트 됩니다.
HTML 콜랙션 객체는 극도로 느립니다. Nicholas는 이 작은 작업이 60배 정도 느리다고 말했습니다. 게다가 이 콜렉션 객체는 의도하지 않은 곳에서 무한 루프를 유발하기도 합니다. 예를 들어:
var divs = documet.getElementsByTagName('div'); for (var i=0; i < divs.length; i++ ) { var div = document.createElement("div"); document.appendChild(div); } |
divs가 가리키는 것이 살아있는 HTML 객체를 무한 루프를 유발하게 됩니다. 아마 당신은 배열처럼 작동하기를 기대했을 것입니다. 살아있는 객체는 당신이 DOM에 <div>추가 할 때마다 업데이트 되고, i < divs.length 는 영원히 만족되지 않을 것 입니다.
이것을 배열처럼 사용하려고 한다면, var divs = document.getElementByTagName('div')를 정의하는 것보다 조금 복잡한 방식을 사용해야 합니다. 다음에 Zakas가 배열같은 사용을 강제할 때 사용하는 스크립트가 있습니다.
function array(items) { try { return Array.prototype.concat.call(items); } catch (ex) { var i = 0, len = items.length, result = Array(len); while (i < len) { result[i] = items[i]; i++; } return result; } } var divs = array( document.getElementsByTagName('div') ); for (var i=0l i < divs.length; i++ ) { var div = document.createElement("div"); document.appendChild(div); } |
9. DOM을 건드리지 마세요!
DOM에 손대지 않는 것은 JavaScript 최적화에 있어 또 다른 중요한 이슈이다. 고전적인 예로 리스트들의 배열에 덧붙이기를 하는 것이 있다 : 만약 당신이 DOM에 이것들을 각각 덧붙이려면, 이것들을 한 번에 붙이는 것보다 눈에 띄게 느릴 것 입니다. 이것은 DOM 작업 비용이 매우 비싸기 때문입니다.
Zakas는 이것을 매우 자세히 설명했습니다. 그의 설명에 따르면 DOM 작업이 자원을 많이 잡아먹는 것은 reflow 때문입니다. reflow는 브라우저가 화면 상의 DOM 엘레멘트를 다시 랜더링하는 과정을 말합니다. 예를 들어, 만약 당신이 JavaScript로 div의 너비를 바꾼다면, 브라우져는 이것을 반영하기 위해 렌더링 된 페이지로 새로 고침을 하게 됩니다.
엘리먼트가 DOM에서 추가 혹은 제거될 땐 항상 reflow가 발생하게 됩니다. documentFragment는 이에 대해 매우 편리한 해결책이 될 수 있습니다. Steve Souders 역시 같은 을 말했으며, 그 때는 사용하지 않는 것이 더 낫다고 생각됐었습니다.
documentFragment는 기본적으로 document와 유사한 브라우저에서 보이지 않는 조각입니다. 보여지 않는다는 것은 많은 장점을 가지고 있습니다. 그 중 하나는 당신이 브라우저의 reflow 없이 documentFragment에 객체를 덧붙일 수 있다는 것입니다.
10. Style을 바꾸지 말고 CSS 클래스를 바꾸세요.
당신은 아마 CSS 클래스를 바꾸는 것이 style을 여러번 바꾸는 것보다 더 최적화에 가깝다고 들었을 것입니다. 이 것 역시 reflow 이슈를 줄일 수 있습니다 : 언제나 레이아웃 스타일이 바뀌면 항상 reflow가 발생합니다.
레이아웃 스타일은 레이아웃에 영향을 미치고, 브라우저의 reflow를 강제합니다. 레이아웃 스타일에는 width, height, font-size, float 등이 있습니다.
오해하지 마세요. CSS 클래스들은 reflow를 피하는 것이 아니고, 단순히 이것을 줄일 뿐 입니다. 스타일이 변경될 때마다 reflow를 발생시키는 대신, 스타일을 변경하기 위해 오직 한번 CSS 클래스를 사용함으로서, reflow만 한번만 발생시킬 수 있습니다.
그래서 레이아웃 스타일을 여러번 변경할 때는 CSS 클래스명을 사용하는 것이 효과적입니다. 반대로 여러 번의 클래스 변경을 해야한다면 스타일 노드를 DOM에 덧붙이는 것이 더 좋을 수도 있습니다.
요약
Nicholas C.Zakas는 JavaScript 구루 중의 구루입니다. 이 포스트를 작성하면서 그가 쓴 다른 많은 글을 참고 했습니다. 이 주제와 관련해서 다른 사람이 쓴 글을 찾는 것이 더 힘들 정도 였습니다.
Zakas의 tech talk는 대단해습니다. 그는 내가 진리로 받아들인 JavaScript 최적화 규칙 뒤의 근거에 대해 말해주었습니다.
저는 당신에게 Zakas의 새로운 책을 추천합니다. 그의 Speed Up Your JavaScript 포스팅을 찾아보시고, 그를 트위터에서 팔로우하시기 바랍니다.
Zakas의 사진을 제공해준 Dustin Diaz에게 감사합니다.
(원문 : http://jonraasch.com/blog/10-javascript-performance-boosting-tips-from-nicholas-zakas )
(KTH 개발자 블로그 : http://dev.paran.com/2011/08/01/kth-news-internationalization-personal-names-around-the-world/ )
'의역과 오역 > Dev' 카테고리의 다른 글
왜 iOS용 Facebook앱은 성능이 떨어지는가?(UIWebViews, no Nitro) (0) | 2012.05.18 |
---|---|
5년째 헤메고 있는 Google(from Android Gripes) (0) | 2012.01.01 |
혁신을 만드는 8가지 원칙 - 수잔 워지스키(Google) (0) | 2011.09.01 |
당신은 JavaScript를 모른다 (0) | 2011.08.30 |
LinkedIn은 어떻게 HTML5와 Node.js를 사용하여 더 낫고, 더 빠른 앱을 만들었는가 (0) | 2011.08.29 |