브라우저의 레이아웃 그리기 및 DOM 작업

11108 단어 브라우저배치dom

브라우저의 레이아웃 그리기 및 DOM 작업


DOM 작업이 느리다뇨
항상 DOM이 느리다고 들었기 때문에 최대한 적게 DOM을 조작해야 한다고 해서 왜 모두가 이렇게 말하는지 더 깊이 탐구하고 싶었다. 인터넷에서 자료를 배웠는데 이쪽에서 정리했다.
우선DOM 대상 자체도 js 대상이기 때문에 엄밀히 말하면 이 대상을 느리게 조작하는 것이 아니라 이 대상을 조작한 후에 레이아웃(layout)과 그리기(paint) 같은 브라우저 행동을 촉발한다.다음은 주로 이러한 브라우저 행위를 소개하고 한 페이지가 어떻게 최종적으로 나타나는지 논술하며 코드의 측면에서 좋지 않은 실천과 최적화 방안을 설명한다.

브라우저는 어떻게 한 페이지를 보여 줍니까


한 브라우저에는 많은 모듈이 있는데 그 중에서 페이지를 보여주는 것은 렌더링 엔진 모듈이다. 비교적 익숙한 것은 웹키트와 Gecko 등이 있는데 여기도 이 모듈의 내용만 관련된다.
먼저 이 과정을 문자로 대체적으로 논술해 봅시다.
  • HTML 구문 분석 및 DOM tree 생성
  • 다양한 양식을 분석하고 DOM tree와 결합하여 Render tree 한 그루 생성
  • Render tree의 각 노드에 대해 레이아웃 정보를 계산한다. 예를 들어box의 위치와 사이즈
  • Render tree 및 브라우저의 UI 레이어를 기반으로 그리기
  • 그 중에서 DOM tree와 Render tree의 노드는 일일이 대응하는 것이 아니다. 예를 들어 display:none의 노드가 DOM tree와 존재하고 Render tree에 나타나지 않는다. 왜냐하면 이 노드는 그려질 필요가 없기 때문이다.
    위의 그림은 웹키트의 기본 절차로 용어상 Gecko와 다를 수 있습니다. 여기에 Gecko의 절차도를 붙이지만 글 아래의 내용은 모두 웹키트의 용어를 통일적으로 사용합니다.
    페이지의 표현에 영향을 주는 요소는 매우 많다. 예를 들어 링크의 위치가 첫 화면의 표현에 영향을 줄 수 있다.하지만 여기서는 레이아웃과 관련된 내용에 집중한다.
    paint는 시간을 소모하는 과정이지만 레이아웃은 시간을 더 소모하는 과정이다. 레이아웃이 위에서 아래로 진행되거나 아래에서 위로 진행되었는지 확인할 수 없다. 심지어 레이아웃이 전체 문서 레이아웃의 재계산에 영향을 미칠 수도 있다.
    그러나 레이아웃은 피할 수 없기 때문에 레이아웃의 횟수를 최소화해야 한다.

    어떤 상황에서 브라우저가layout을 진행합니까


    레이아웃 횟수를 최소화하는 방법을 고려하기 전에 브라우저가 레이아웃을 언제 하는지 알아야 합니다.
    layout (reflow) 은 일반적으로 레이아웃이라고 하는데, 이 동작은 문서의 요소의 위치와 크기를 계산하는 데 사용되며, 렌더링 전의 중요한 단계이다.HTML이 처음 불러올 때,layout을 제외하고, js 스크립트의 실행과 스타일의 변화 역시 브라우저가layout을 실행하게 할 수 있습니다. 이것은 본고에서 주로 논의해야 할 내용입니다.
    일반적으로 브라우저의layout은lazy입니다. 즉, js 스크립트가 실행될 때 DOM을 업데이트하지 않습니다. DOM에 대한 변경 사항은 하나의 대기열에 임시로 저장됩니다. 현재 js의 실행 상하문에서 실행이 완료되면 이 대기열의 수정에 따라layout을 한 번 진행합니다.
    그러나 때때로 js 코드에서 최신 DOM 노드 정보를 즉시 얻기를 원하면 브라우저는layout을 미리 실행해야 한다. 이것은 DOM 성능 문제의 주원인이다.
    다음 작업을 수행하면 일반이 깨지고 브라우저가 layout을 실행하도록 트리거합니다.
  • js를 통해 계산할 DOM 속성 가져오기
  • DOM 요소 추가 또는 제거
  • resize 브라우저 창 크기
  • 글꼴 바꾸기
  • css 위조류의 활성화, 예를 들어:hover
  • js를 통해 DOM 요소 스타일을 수정하고 이 스타일은 사이즈의 변화와 관련된다
  • 우리는 하나의 예를 통해 직관적으로 느낀다.
    // Read
    var h1 = element1.clientHeight;
    
    // Write (invalidates layout)
    element1.style.height = (h1 * 2) + 'px';
    
    // Read (triggers layout)
    var h2 = element2.clientHeight;
    
    // Write (invalidates layout)
    element2.style.height = (h2 * 2) + 'px';
    
    // Read (triggers layout)
    var h3 = element3.clientHeight;
    
    // Write (invalidates layout)
    element3.style.height = (h3 * 2) + 'px';  
    

    이것은 하나의 속성 clientHeight 과 관련이 있는데, 이 속성은 계산해야 하기 때문에 브라우저의layout을 터치합니다.크롬 (v47.0) 의 개발자 도구를 이용해서 보겠습니다. (캡처된 타임라인 record는 레이아웃만 표시됩니다.)
    위의 예에서 코드는 먼저 한 요소의 스타일을 수정했고 그 다음에 다른 요소clientHeight의 속성을 읽었다. 이전의 수정으로 인해 현재DOM이 더러워졌다. 이 속성을 정확하게 얻을 수 있도록 브라우저는layout을 한 번 한다(크롬의 개발자 도구가 이 성능 문제를 양심적으로 제시한 것을 발견했다).
    이 코드를 최적화하는 것은 매우 간단하다. 필요한 속성을 미리 읽고 함께 수정하면 된다.
    // Read
    var h1 = element1.clientHeight;  
    var h2 = element2.clientHeight;  
    var h3 = element3.clientHeight;
    
    // Write (invalidates layout)
    element1.style.height = (h1 * 2) + 'px';  
    element2.style.height = (h2 * 2) + 'px';  
    element3.style.height = (h3 * 2) + 'px';  
    

    이번 상황을 살펴보자.
    다음은 또 다른 최적화 방안을 소개한다.

    layout 최소화 방안


    위에서 언급한 대량 읽기와 쓰기는 하나의 것이다. 주로 계산이 필요한 속성 값을 얻기 때문이다. 그러면 어떤 값이 계산되어야 하는가?
    이 링크에는 계산이 필요한 대부분의 속성이 설명되어 있습니다.http://gent.ilcore.com/2011/03/how-not-to-trigger-layout-in-webkit.html
    또 다른 상황을 살펴보자.

    일련의 DOM 작업 수행


    일련의 DOM 작업(DOM 요소의 첨삭과 수정)에 대해 다음과 같은 시나리오를 사용할 수 있습니다.
  • documentFragment
  • display: none
  • cloneNode

  • 예를 들어 (documentFragment의 경우에만):
    var fragment = document.createDocumentFragment();  
    for (var i=0; i < items.length; i++){  
      var item = document.createElement("li");
      item.appendChild(document.createTextNode("Option " + i);
      fragment.appendChild(item);
    }
    list.appendChild(fragment);  
    

    이러한 최적화 방안의 핵심 사상은 모두 같다. 즉, 먼저 Render tree에 없는 노드에 대해 일련의 조작을 한 다음에 이 노드를 Render tree에 추가하면 아무리 복잡한DOM 조작이 있어도layout을 한 번만 촉발할 수 있다.

    스타일 수정


    스타일의 변화에 대해 우리는 우선 모든 스타일의 수정이 레이아웃을 촉발하는 것이 아니라는 것을 알아야 한다. 레이아웃의 작업이 RenderObject의 사이즈와 크기 정보를 계산하는 것임을 알고 있기 때문이다. 그러면 내가 한 색만 바꾸면 레이아웃을 촉발하지 않을 것이다.
    각 CSS 속성이 브라우저의 layout과paint 실행에 미치는 영향을 자세히 보여 주는 웹 사이트 CSS triggers가 있습니다.
    아래와 같은 경우 위에서 말한 최적화된 부분과 마찬가지로 읽기와 쓰기에 주의하면 된다.
    elem.style.height = "100px"; // mark invalidated  
    elem.style.width = "100px";  
    elem.style.marginRight = "10px";
    
    elem.clientHeight // force layout here  
    

    그러나 애니메이션을 말하자면 여기는 js 애니메이션을 말한다. 예를 들어 다음과 같다.
    function animate (from, to) {  
      if (from === to) return
    
      requestAnimationFrame(function () {
        from += 5
        element1.style.height = from + "px"
        animate(from, to)
      })
    }
    
    animate(100, 500)  
    

    애니메이션의 모든 프레임이layout을 초래할 수 있다. 이것은 피할 수 없는 것이다. 그러나 애니메이션이 가져오는layout의 성능 손실을 줄이기 위해 애니메이션 요소를 절대적으로 포지셔닝할 수 있다. 이렇게 하면 애니메이션 요소가 텍스트 흐름에서 벗어나layout의 계산량이 많이 줄어든다.

    RequestAnimationFrame 사용


    다시 그릴 수 있는 모든 작업을 넣어야 합니다requestAnimationFrame.
    현실 프로젝트에서 코드는 모듈에 따라 구분되기 때문에 전례처럼 대량으로 읽기와 쓰기를 조직하기 어렵다.그러면 이 때 쓰기 동작을 requestAnimationFrame의callback에 놓고 쓰기 동작을 다음paint 이전에 통일적으로 실행할 수 있습니다.
    // Read
    var h1 = element1.clientHeight;
    
    // Write
    requestAnimationFrame(function() {  
      element1.style.height = (h1 * 2) + 'px';
    });
    
    // Read
    var h2 = element2.clientHeight;
    
    // Write
    requestAnimationFrame(function() {  
      element2.style.height = (h2 * 2) + 'px';
    });
    

    Animation Frame이 터치될 시기를 잘 관찰할 수 있습니다. MDN에서는paint가 터치되기 전에 터치한다고 했지만, js 스크립트가 브라우저에 DOM의 invalidated check을 할 수 있는 제어권을 넘기기 전에 실행한 것으로 추정됩니다.

    기타 주의 사항


    layout을 트리거하여 성능 문제가 발생한 것 외에 다음과 같은 추가 세부 사항이 나열됩니다.
    캐시 선택기의 결과로 DOM 쿼리가 줄어듭니다.여기에는 HTMLCollection 이 필요합니다.HTMLCollection은 document.getElementByTagName를 통해 얻어진 대상 유형으로 그룹 유형과 유사하지만 매번 이 대상의 속성을 가져올 때마다 DOM 조회를 하는 것과 같다.
    var divs = document.getElementsByTagName("div");  
    for (var i = 0; i < divs.length; i++){  //infinite loop  
      document.body.appendChild(document.createElement("div"));
    }
    

    예를 들어 위의 이 코드는 무한 순환을 초래하기 때문에 HTMLCollection 대상을 처리할 때 가장 많은 캐시를 사용해야 한다.
    또한 DOM 요소의 삽입 깊이를 줄이고 css를 최적화합니다. 쓸모없는 스타일을 제거하면layout의 계산량을 줄이는 데 도움이 됩니다.
    DOM 조회 시querySelectorquerySelectorAll는 최후의 선택이어야 한다. 그들의 기능이 가장 강하지만 실행 효율이 매우 떨어지기 때문에 가능하다면 가능한 한 다른 방법으로 대체해야 한다.
    다음 두 jsperf 링크는 성능을 비교할 수 있습니다.
    https://jsperf.com/getelementsbyclassname-vs-queryselectorall/162 http://jsperf.com/getelementbyid-vs-queryselector/218

    뷰 레이어에 대한 자신의 생각


    위의 내용과 이론 방면의 것이 많고 실천적인 측면에서 볼 때 위에서 토론한 내용은 바로View층이 처리해야 할 일이다.이미 라이브러리 FastDOM이 이 일을 하기 위해 왔지만, 코드는 다음과 같습니다.
    fastdom.read(function() {  
      console.log('read');
    });
    
    fastdom.write(function() {  
      console.log('write');
    });
    

    문제는 뚜렷하다callback hell. 그리고FastDOM과 같은imperative의 코드가 확장성이 부족하다는 것을 예견할 수 있다. 관건은 requestAnimationFrame를 사용한 후에 비동기 프로그래밍의 문제가 되는 것이다.읽기와 쓰기 상태를 동기화시키려면 반드시 DOM의 기초 위에서 Wrapper를 써서 내부적으로 비동기적인 읽기와 쓰기를 통제해야 한다. 그러나 이 지경에 이르렀을 때 직접적으로React를 고려할 수 있을 것 같다.
    한 마디로 하면 위에서 말한 문제를 최대한 피하지만 라이브러리, 예를 들어 jQuery를 사용하면layout의 문제는 라이브러리 자체의 추상적인 데서 나온다.React가 자신의 구성 요소 모델을 도입하고 virtual DOM을 사용해서 DOM 조작을 줄인 적이 있고 state가 바뀔 때마다 layout이 한 번밖에 없어요. 내부에 쓸모가 있는지 없는지requestAnimationFrame 모르겠어요. View층을 잘 만들면 어려울 것 같아서 나중에 React 코드를 배울 준비를 해요.자신이 1, 2년 후에 와서 이 문제를 다시 볼 때 새로운 견해를 가질 수 있기를 바란다.

    참고 자료

  • http://www.html5rocks.com/en/tutorials/internals/howbrowserswork/
  • https://dev.opera.com/articles/efficient-javascript/?page=3
  • http://wilsonpage.co.uk/preventing-layout-thrashing/
  • https://www.nczonline.net/blog/2009/02/03/speed-up-your-javascript-part-4/
  • http://gent.ilcore.com/2011/03/how-not-to-trigger-layout-in-webkit.html
  • 좋은 웹페이지 즐겨찾기