개발하다보면 내 환경은 잘되는데 성능이 느린 환경은 어떨까 궁금할 때가 있다. 그럴 때 사용하는  대표적인 도구 [WebPageTest] 가 있다.


다만, WebPageTest는 초기 로딩 성능을 주로 하고, 테스트 하는데 시간도 오래 걸리기 때문에 자주 실행해보는건 적당하지 않다. 가끔 확인이나 할 뿐. (물론 도구와 연결해서 주기적으로 할 수 있지만 바로 바로 확인해보고 싶은 경우가 많아서..)


근데 얼마 전에 크롬에서 Network throttling이란 기능이 생겼다. Network 탭에 가면 아래와 같이 Network을 조절할 수 있는 기능이 추가됐다.



거의 WebPageTest와 다르지 않게 많이 쓸 것 같은 대역폭과 레이턴시들이 존재하고 직접 상황에 맞는 설정을 추가할 수 있다.



물론 실제 환경이 아니지만 거의 비슷한 느낌을 받기 때문에 빠르게 네크워크가 안좋은 환경에서는 어떨까 확인하고 싶다면 확인해보는 것도 괜찮을 것 같다. 그리고 후진 폰에서는 인터랙션이 어떻게 동작할까? 이런 생각을 할 수 있는데 이걸 후진폰에서 직접 확인하는 방법이 좋긴 하지만, 귀찮기도 하고 빠르게 확인하기란 적절하지 않다.



이것도 최근에 크롬에서 CPU throttling이란 기능이 추가됐다. 현재는 바로 쓸 수 있는건 아니고  Setting > Experiments 에 가서 아래와 같이 활성화을 해야 한다.



그리고 타임라인 탭에 가면 Network와 같이 CPU로 throttling 할 수 있다.



직접 기기에서 보는 것과 다르긴 하지만, 써보니깐 편하게 느린 기기를 체험할 수 있다. 물론, 실제 기기랑 느린거랑은 좀 다르다.


실제 환경에서 확인하는게 더 정확하긴 하다. 하지만, 빠르게 확인하는 방법으로 이 두 기능은 꽤 괜찮은 방법이다.






Posted by 전용우
,

예전에 qunit에서 [code review] 받을 때 [Richard Gibson]이 알려준 내용인데 [jsmocktool] 관련해서 개발하다가 갑자기 생각하서 또 잊을까봐 정리한다.


당시 코드에서 regexp 객체에 flags가 있는지 확인하는 방법으로 속성에 접근해서 아래와 같이 사용했다.


1
return regexp.flags || regexp.toString().match(/[gimuy]*$/)[0];
cs


근데 Richard Gibson이 "나는 불필요한 동작을 하지 않기 위해 in연산자를 사용한다"며 in 연산자를 사용하길 바랬다. 첨에 "엇 뭐지?"라는 생각을 했지만, 잠깐 고민해보니 좋은 의견이였다. 그리고 오늘 글을 쓰기 위해 좀 더 조사를 해봤다.


먼저 속성이 있는지 확인하는 방법은 크게 3가지가 있다.


1. 동등 연산자( == ,=== )

1
2
if(o[k] === undefined){    
}
cs


2. hasOwnProperty 메서드

1
2
if(o.hasOwnProperty(v)){    
}
cs


3. in 연산자

1
2
if(v in o){    
}
cs



이 3가지중 어떤게 가장 빠를까? 한번 고민해보고 아래 글을 읽으면 재밌을거라 생각한다.


시점/브라우저 등 여러가지 상황에 따라 다를 수 있는데 V8(크롬)에서 보면 1번이 가장 빠르다. 왜 그런지는 V8 개발자인 [Vyacheslav Egorov]의 설명을 보면 알 수 있다. 요약하면, 1번의 경우는 흔히 얘기하는 hidden class로 특정 offset을 가지고 있고 반복 호출되면 [Inline Cache] 때문에 굉장히 빠르지만, hasOwnProperty와 in 연산자의 경우는 별도의 캐시를 하지 않고 매번 새로 찾기 때문에 느리다. [비교] (이 말은 키값(offset)이 고정되어 있지 않으면 차이가 없다는 말이다. 실제로 로직에서  키 값을 항상 변경해서 사용하면 성능에 크게 차이가 없다.)


하지만, 비교에서 보면 최근에 hasOwnProperty은 [개선]되어서 지금은 hasOwnProperty도 실제로 비슷한 성능을 내고 있으며 [in 연산자] 역시 언젠가 빠를 것 같다.


다시 돌아와서. 지금까지 보면 1번이 여러 상황에서 적절한 것 같은데 왜 Richard Gibson은 `in`연산자를 사용하라고 했을까?

단순 객체에 속성을 확인하는건 1번이 빠를진 몰라도, 일반적으로 FE을 개발할 때 DOM등 브라우저에 있는 native 객체들에 위의 비교문을 많이 사용한다. 그래서 속성을 읽을 때 단순히 값을 반환하는 경우도 있겠지만, 내부적으로 복잡한 연산이 들어가기 때문에 오히려 1번이 가장 느릴 경우가 많다.


예를 들어. 특정 객체에 offsetHeight라는 속성이 있는지 확인한다면, 1번의 경우는 [forced layout]이 발생하여 엄청나게 느리지만, 2번이나 3번의 경우는 속성이 있는지만 확인하기 때문에 불필요한 forced layout이 발생하지 않는다.


이렇게 단순히 자바스크립트 객체의 속성을 확인하는 방법으로 좋을지 모르지만, DOM이나 기타 native 객체들은 2, 3번(주로 3을 선호한다. 이유는 아래에)을 사용하는게 오히려 좀 더 나은 practice라고 생각하며 아마도 Richard Gibson이 이런 의미로 얘기한 것 같다.


당연하지만, getter의 경우도 1번에서는 동작하지만, 2, 3의 경우는 동작하지 않는다. 참고로 hasOwnProperty는 1, 3번 약간은 다르다. hasOwnProperty은 prototype chaining하지 않고 속성을 찾기 때문에 기대했던 생각과 다른 결과를 만날 수 있다. [참고


요약하면,


단순 오브젝트를 확인하는건 속성에 접근하여 확인하는게 빠르지만, 브라우저의 객체나 DOM은 in/hasOwnProperty가 괜찮은 방법인 것 같다.


ps. Code HighLight을 하기 위해 보통 markup.su/highlighter/ 을 사용했는데 관리를 안하는지 다른 것을 찾아보다가 http://colorscripter.com/ 을 찾았는데 완전 좋네. 



참고 URL.

http://stackoverflow.com/questions/21763617/why-is-getting-a-member-faster-than-calling-hasownproperty

https://bugs.chromium.org/p/v8/issues/detail?id=2472#c8

https://bugs.chromium.org/p/v8/issues/detail?id=2743

Posted by 전용우
,

모바일 웹에서 성능을 높이는 방법으로 GPU가속을 사용하기 위해 translateZ(0)와 같은 hack코드를 넣곤 했다. 이후에 will-change가 나와서 hack을 대신해서 사용했다. 둘 다 GPU가속을 위해 사용했는데 굳이 will-change을 사용하는 이유는  "브라우저가 효과적으로 GPU가속을 사용할 수 있기" 때문이였다.


문서에는 브라우저마다 will-change을 다르게 사용한다고 하지만, 실제로 크롬에서는 will-change가 어떤 영향을 주는지 코드로 확인하고 싶었다. 아쉽게도 당시 코드를 봤을 때 딱히 크롬이 스마트하게 판단하는지는 찾지 못했다. 그렇게 내가 아직 잘 모르는구나 하는 생각을 하고 지나갔다.


그리고 몇 일전에 will-change: scroll-position, content이 성능에 어떤 영향을 받는지 문의한 글이 있었는데 이 글을 보고 예전 생각이 나서 정리한다. 


일단 scroll-position는 스크롤의 위치가 변경될 수 있음을 알려주는 힌트이고 content는 엘리먼트가 변경될 수 있음을 알려주는 힌트다. [링크]


답변 내용은 현재까진 will-change: scroll-position은 어떤 영향을 미치지 않는다. will-change: content의 경우는  content을 가진 엘리먼트의 자식 엘리먼트가 will-change: <hint>을 가지면 변경할 가능성이 있다고 판단하여 자식 엘리먼트는 composited layer을 만들지 않는다. 


예제를 보면 이해하기 쉬운데 아래 두개만 composited layer을 만든다.

<div style="will-change: scroll-position"></div>

<div style="will-change: contents"></div>

<div style="will-change: transform"></div> // <--------- composited layer

<div style="will-change: contents">
    <div style="will-change: transform"></div>
</div>

<div style="will-change: scroll-position">
    <div style="will-change: transform"></div> // <--------- composited layer
</div>


blink코드를 보면 더 정확히 할 수 있다.

if (style.hasWillChangeCompositingHint() && !style.subtreeWillChangeContents())
        reasons |= CompositingReasonWillChangeCompositingHint;


그리고 will-change에 들어갈 수 있는 style은 [opacity, transform, webkit-transform, top, left, bottom, right]이다.


switch (rareNonInheritedData->m_willChange->m_properties[i]) {
        case CSSPropertyOpacity:
        case CSSPropertyTransform:
        case CSSPropertyAliasWebkitTransform:
        case CSSPropertyTop:
        case CSSPropertyLeft:
        case CSSPropertyBottom:
        case CSSPropertyRight:
            return true;
        default:
            break;
}


이걸 보면서, 예전에 궁금했던 점이 조금 이해됐다. 현재는 content속성에 대해서만 되어 있지만, 나중에는 scroll-position에 대해서도 최적화가 들어갈 것 같고 이후에는 W3C에 나와 있는 것 처럼 최적화가 되지 않을까 생각된다.




Posted by 전용우
,

며칠 전에 angular 코드를 보다가 design note을 찾으러 위키를 갔는데 우연히 [performance]을 봤다.(design note는 [다른 곳]에서 찾음.)

다른 내용들은 일반적인 내용인데 내가 의아하게 느낀 부분이 자식노드를 탐색할 때 `DOM.childNodes`이 `node.nextSibling`보다 느리다는 얘기다. 그 예로 [jsperf링크]가 있어서 확인해보니 실제로 nextSibling이 빨랐고 몇 개 브라우저에서 좀 더 확인해봤는데 모두 nextSibling이 항상 빨랐다.


"왜 그럴까?" 잠깐 고민해봤는데 딱히 아이디어가 안떠올라 blink의 구현로직을 찾아보기로 했다.


먼저 clideNodes부분을 보니 Container노드(parentNode)일 때 그 안에 childeNode을 찾아 nodelist로 만든 다음 반환하는데 뭔가 특별해보이진 않는다.

PassRefPtrWillBeRawPtr<NodeList> Node::childNodes()
{
    if (isContainerNode())
        return ensureRareData().ensureNodeLists().ensureChildNodeList(toContainerNode(*this));
    return ensureRareData().ensureNodeLists().ensureEmptyChildNodeList(*this);
}

그럼 nextSibling은 뭘까 찾아봤다.

Node* previousSibling() const { return m_previous; }
Node* nextSibling() const { return m_next; }

엇. 이거 뭐지 그냥 반환만 하네... 그래서 찾아보니 nextSibling, previousSibling을 호출할 때는 뭔가 찾는게 아니라 아래와 같이 Node가 변경되면 값을 업데이트하고 호출할 때는 위와 같이 그냥 반환한다.

void ContainerNode::insertBeforeCommon(Node& nextChild, Node& newChild)
{
    Node* prev = nextChild.previousSibling();
    nextChild.setPreviousSibling(&newChild);
    if (prev) {
        prev->setNextSibling(&newChild);
    } else {
        m_firstChild = &newChild;
    }
    newChild.setParentOrShadowHostNode(this);
    newChild.setPreviousSibling(prev);
    newChild.setNextSibling(&nextChild);
}

void ContainerNode::appendChildCommon(Node& child)
{
    child.setParentOrShadowHostNode(this);

    if (m_lastChild) {
        child.setPreviousSibling(m_lastChild);
        m_lastChild->setNextSibling(&child);
    } else {
        setFirstChild(&child);
    }

    setLastChild(&child);
}

즉, childNodes을 찾을 때는 하위 노드를 찾아서 nodelist로 만든 다음 반환하기 때문에 비용이 좀 드는 것 같고, nextSibling은 그냥 바로 노드를 반환하기 때문에 비용이 적게 드는 것 같다. 

갑자기 비슷한 children이 생각나서 이건 어떻게 구현되어 있을까 하고 찾아봤는데 아래와 같이 구현되어 있다. 

PassRefPtrWillBeRawPtr<HTMLCollection> ContainerNode::children()
{
    return ensureCachedCollection<HTMLCollection>(NodeChildren);
}

이건 우리가 알고 있는 [live nodelist]인 것 같아 아마도 성능이 좋지 않을까 하고 비교해봤는데 결과는 nextSibling보다는 느리지만, childNodes보다는 빨랐다. [링크]


처음 가지고 있던 의구심을 해결하고 nextSibling, previousSibling, lastChild, firstChild는 이미 계산되어 가지고 있다는 사실을 알게됐다.


ps. github에서 유명한 프로젝트에 childNodes을 사용하는 부분을 찾아서 pr줍기를 시도했지만... 테스트 코드를 제외하곤 거의 없어 아쉬었다. 하지만, 우연히 버그같은 코드를 발견해서 [pr 줍기 성공]. :)





Posted by 전용우
,

내가 느끼기에 FE성능은 크게 로딩 성능과 인터랙션 성능으로 구분되는데 어디에 포인트를 주고 할 것 인가에 따라 과정이나 결과물이 완전 다르다. 그래서 개선작업을 할 때 어떤 부분을 포인트를 주고 할 것 인지 설정한 후 진행하는게 좋다.


기본적으로 포인트의 키워드를 보면 로딩 성능의 핵심은 요청 횟수이고 인터랙션 성능의 핵심은 DOM이다.

자세히 들어가면 다양한 테크닉과 도구의 사용법이 있는데 이는 너무 잘 정리된 아티클이나 책에서 많이 나와있어 굳이 설명하자 않아도 많을 것 같다.


두 성능 중 인터랙션 성능은 비교적 전문 FE개발자가 아니면 쉽게 접근하기 힘든 부분인데 이 부분을 잘 정리한 글이 있어서 공유한다. 이 아티클은 모바일웹의 인터랙션 성능을 튜닝하고 싶다면 반드시 한번쯤 따라해보면 갑자기 고수가 되는 경험을 하게 될 정도로 잘정리되어 있다.



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

Posted by 전용우
,

document.write을 사용하면 돔 랜더링이 멈추기 때문에 느리게 보인다. 그래서document.write을 대신하기 위한 다양한 방법이 나와있다. 하지만, 광고의 경우 다양한 곳에 삽입되기 때문에 대부분은 document.write을 사용한다.

이 문제를 해결하고자 최근에 웹성능의 본좌인 사우더스 아저씨가 HTML import을 활용한 방법을 제안했다.[링크] (HTML import는 본인과 크게 상관도 없고 아직 완성도가 있는 스팩인데 이런걸 활용하다니 좀 놀랐다.)

HTML import를 간단히 설명하면  Web Components을 사용하기 위한 방법으로 link태그를 이용하여 html을 로딩하는 방법이다.[링크]

자세한 내용은 원문[링크]을 참고하고 요약하면 사우더스 아저씨는 아래와 같이  import안에서 document.write를 하는 방법을 제안했다.

main.html

<link rel="import" href="ad.html">

<div id="ad_area" style="width: 480px; height: 60px; margin-left: 2em; margin-bottom: 2em;"></div>

<script>
var link = document.querySelector('link[rel=import]');
var content = link.import.querySelector('#ad');
document.getElementById('ad_area').appendChild(content.cloneNode(true));
</script>

ad.html
<div id="ad" style="background: #E99; border: 2px; font-size: 2em; text-align: center; padding: 8px;">
<script>
document.write("IMPORTED CONTENT<div style='font-size=0.8em;'>(using document.write)</div>");
</script>

근데 문제가 있다.

1. import는 동기로 로딩된다.[링크]

2. import한 html에서 document.write을 하면 main페이지에서 context로 실행되는 문제가 있다.[링크]

그래서 이를 해결하고자 아래와 같이 스크립트로 import을 삽입하고 ad.html에서 document.write을 재정의하여 현재 context로 실행하도록 했다.

main.html

<div id="ad_area" style="width: 480px; height: 60px; margin-left: 2em; margin-bottom: 2em;"></div>

<script>
  var link = document.createElement('link');
  link.rel = 'import';
  link.onload = function() {
    var link = document.querySelector('link[rel=import]');
    var content = link.import.querySelector('#ad');
    document.getElementById('ad_area').appendChild(content.cloneNode(true));
  };
  link.href = "ad.html";
  document.getElementsByTagName('head')[0].appendChild(link);
</script>

ad.html

<div id="ad" style="background: #E99; border: 2px; font-size: 2em; text-align: center; padding: 8px;">
<script>
document.write = function(msg) {
    document.currentScript.ownerDocument.write(msg);
};
document.write("IMPORTED CONTENT<div style='font-size=0.8em;'>(using document.write)</div>");
</script>


이렇게 document.write를 사용하면서 돔의 랜더링을 지연하지 않도록 개선할 수 있다.[링크]

물론 import의 안에서 실행되는 context의 이슈등 문제가 있어 당장 사용하기 힘들지만 아이디어는 좋은 것 같다.

그리고 사우더스는 HTML import가 기본으로 랜더링을 막지 말고 동작해야 한다고 제안했지만 이 문제는 쉽지 않다. 이 문제에 대해서는 다음에 알아보자. 또한 위의 방법은 오해가 있기 때문에 반드시 [링크]을 추가로 읽기 바란다.


관련글 

알고보면 랜더링을 막지 않는 import

Posted by 전용우
,

IE에서는 한 CSS파일에 4095개의 선택자까지만 적용되고 이후는 적용이 안된다.(링크)

그래서 네트워크비용을 줄이기 위해 CSS파일을 합치려면 최대 선택자가 4095개여야 한다.

넘는 경우에는 모두 합치고 CSS을 xhr로 가져와 적용하는 사례들도 있다.


근데 문제는 XHR은 크롬에서 리소스를 가져오는 순서중 뒤에 속한다.

(아마 타 브라우저도 비슷한것 같다.)

크롬에서 리소스를 내려받는 순서는 HTML -> CSS -> JS -> 이미지 순이다.

좀더 자세한 순서는 CachedResourceLoader.cpp(링크)에 보면 알 수 있다.


그래서 xhr은 JS을 받으면 실행하기 때문에 

link를 사용했더라면 html -> css ->  js였지만 xhr을 사용하게 되면 html -> js -> css로 되어 네트워크 비용이 감소할지 모르지만 사용자가 느끼는 체감 속도는 더 느릴 수 있다.


이런 경우는 걍 2개로 나누어서 받는게 더욱 효율적일 수 있다.

SPDY의 경우 적게는 4배에서 많게는 8배 정도 빠르다고 한다.(링크)

Posted by 전용우
,
아래 원문을 보고 간단히 번역해봤습니다.
(오역이 많으니 가능하면 원문을 보시는게 좋을것 같습니다.)
뭐 아래 같은 얘기는 많이 들어봤을텐데 다시 상기시키는 차원에서 보면 좋을것 같습니다.
개인적으로는 Smash같은 경우는 첨 들었고 "문법설탕(syntactic sugar)을 제거하였나요?" 이 부분은 뭔가 고민되네요.


원문: http://slowjavascript.com/

  1. 스크립트 테그의 위치가 올바른가요?
  2. DOM Monster에서 확인 해보셨나요?
  3. 헤더에 캐시가 적용되었나요?
  4. 여러개의 파일을 합치고 한줄로 만들어서 캐시하게 만들었나요?
  5. 축소하고 압축(Gzip)하였나요?
  6. Smash을 이용하여 css sprite와 favicon을 만들었나요?
  7. 프래임워크를 확인해봤나요?
  8. 잘못 사용하는 timer들은 없나요?
  9. Dom을 최대한 효율적으로 사용했나요?
    • 노드 탐색
    • 이벤트 버블링과 캡쳐
    • innerHTML
    • 투명 이미지
    • 스타일
    • 복잡도
  10. Ajax를 적절하게 사용했나요?
  11. 코드를 간단하게 만들었나요?( 변수 할당, 연산자들, 리터럴들)
  12. 지나치게 완벽하게 만든 코드들을 수정하였나요?(Strip out excessive cleverness)
  13. 효율적으로 정규표현식을 사용했나요?
  14. JSON을 사용했나요?
  15. 정규식을 캐시하여 사용했나요?
  16. 클로져, 전역변수, 리터럴들을 조심해서 사용했나요?
  17. 가장 효율적인 구문을 사용했나요?
  18. 가장 효율적인 반복문을 사용했나요?
  19. 객체의 속성들을 가장 효율적으로 사용했나요?
  20. 변수들은 캐시해서 사용했나요?
  21. 메모리 릭을 최대한 제거했나요?
  22. 효율적으로 arguments를 사용했나요?
  23. 문법설탕(syntactic sugar)을 제거하였나요?
  24. 루프를 사용하지 않고 펼쳐서 사용하거나 적절하게 사용했나요?
  25. iPhone을 위해 최적화를 했나요?



DOM Monster

Thomas Fuchs(scriptaculs 제작자)와 AmyHoy(디자이너)가 만든 프로파일링 툴

Smash
이미지의 품질을 지키면서 용량을 줄이는 웹 도구.

문법설탕(syntactic sugar)
개발할때 코드를 읽기 쉽거나 사용하기 쉽게 하기 위해서  만든 문법을 말한다.
사용하기에는 편하나 성능면에서는 비효율적일수 있다.

지나치게 완벽하게 만든 코드들을 수정하였나요?(Strip out excessive cleverness)
- 이 부분은 제가 이해하기로는 "추후를 예측하여 현재 불필요한 코드를 만들어서 성능에 영향을 미치는 경우"와 같은 류를 말하는것 같습니다.


Posted by 전용우
,
UI 개발을 한 후에 성능 테스트를 하려고 하면 쓸만한 도구가 없다.

물론 각종 프로파일링 툴로 확인도 하지만 매우 부족하다.

예로 처음 로딩시 초기 UI를 구성하기 까지 시간을 측정했을 때
네트워크 시간, javascript 시간, 랜더링 시간 이렇게 구분하려고 하면
상당히 원시적인 방법으로 해야했다.
(물론 내가 못본 툴이 있을수도 있겠지만 다들 만족스런 결과는 아니였다.)

그나마 지금 까지 본 툴중에는 젤 그럴듯 하게 지원하는 툴이 아마 dynatrace인것 같다.

일단 무료다.-_-;
그리고 사용법은 이곳에 동영상을 보면 쉽게 이해 할수 있어서 스킵.

잠깐 이것 저것 해본 결과 괜찬다고 생각하는 기능 몇가지를 소개할까 한다.
(정확한지는 모르겠다)
1. graph로 어디가 병목인지 알게 쉽게 되어 있었다.
사용자 삽입 이미지

위의 그림에서 보면 이벤트, javascript, network 이렇게 3가지로 구분하여 비용현황을 볼수 있다.

2. 시간의 순서대로 페이지에 어떤일이 있어났는지 확인이 가능하다.
사용자 삽입 이미지


3. 무엇이 가장 시간이 오래 걸리는지 한눈에 확인이 가능하다.
사용자 삽입 이미지

여기에는 랜더링 시간, 함수 시간, ajax등 모두 포함되어 있다.

아직 윈도우에 IE만 지원한다.(추후 firefox지원 예정)
하지만 지금꺼 보아온 성능 툴과 비교하여 편하게 좋은 데이터를 볼수 있어 좋은것 같다.

혹시 UI개발에 성능 측정 도구가 필요하다면 dynatrace을 사용하라고 추천하고 싶다.

Posted by 전용우
,