Browser Render Test - Reflow, Repaint, dev Tools

💡 브라우저의 Reflow 와 Repaint 를 개발자 도구로 확인해봅니다.

1. 브라우저 렌더링 프로세스

일반적으로 웹을 개발할 때 세 가지 언어 HTML, CSS, Javascript 를 사용합니다. 작성된 각각의 파일들은 브라우저를 통해서 사용자들에게 보여집니다. 소스 코드들이 잘 갖춰진 형태의 웹 페이지로 만들어지는 과정을 브라우저 렌더링 프로세스를 통해 설명할 수 있습니다. 기본적인 흐름은 다음의 그림과 같습니다.


Rendering Process 를 도식화한 이미지

  1. 먼저 HTML 파일을 DOM Tree 로 변환하는 과정을 거칩니다. HTML 파일 내부에 작성된 여러 가지 태그들이 Tokenizing (작은 단위로 나누기), Lexing (의미를 가진 형태로 만들기) 과 Parsing (올바른 데이터인지 확인하기) 의 과정을 거쳐서 Tree 형태의 구조로 만들어집니다.

  2. CSS 파일도 동일하게 Parser 를 거쳐서 CSSOM Tree 로 만들어집니다.

  3. 앞서 만들어진 두 개의 트리인 DOM Tree 와 CSSOM Tree 를 합쳐서 Render Tree 를 만듭니다. CSSOM 에서 작성된 스타일이 적용되어야 할 부분을 DOM Tree 에서 찾아서 적용하는 방식으로 생성됩니다. 이 때, Render Tree 의 특징은 실제로 브라우저에서 나타나는 형태와 Render Tree 의 형태가 다를 수 있다는 점입니다. 예를 들어서 화면에 실제로 표시되지 않는 visibility: “hidden” 과 같은 속성이 적용된 태그의 경우에는 Render Tree 에 존재하지만 브라우저를 렌더링할 때는 제외됩니다.

  4. Render Tree 를 기반으로 하여 브라우저에서 각 태그가 표시되어야 하는 위치와 크기를 계산하는 과정인 Layout 을 거칩니다. 주로 위치와 관련된 속성을 계산합니다.

  5. 태그들은 각자 다른 계층으로 분류됩니다. 태그가 속할 Layer 를 나누는 과정을 Layer Tree 라고 부릅니다.

  6. 실제 렌더링될 요소들을 브라우저에 그리는 과정을 Paint (draw) 라고 부릅니다.

  7. 마지막으로 요소가 렌더링된 계층들이 하나로 합쳐진 것을 Composite Layer 라고 합니다. 여러 Layer 를 하나의 비트맵으로 만들어서 최종적인 페이지를 만듭니다.


2. Reflow 와 Repaint

Javascript 는 웹 페이지의 UI를 동적으로 바꿀 수 있습니다. 이렇게 바뀐 부분을 브라우저에 반영하기 위해서 렌더링 프로세스를 다시 진행해야 합니다. 이처럼 바뀐 부분이 렌더링될 때 Reflow, Repaint 가 발생한다고 합니다. 이는 앞서 설명한 렌더링 과정에서 각각 LayoutPaint 가 다시 발생하는 것을 말합니다.


Reflow 와 Repaint 가 발생하는 조건에 대해 간단하게 정리한 사진


3. Chrome dev Tools 살펴보기

코드로 Reflow 와 Repaint 를 확인하기 전에 잠깐 Chrome 개발자 도구에 대해서 살펴보겠습니다.


1) Performance

개발자 도구의 Performance 탭에서 웹 페이지를 일정시간 녹화하고 발생한 이벤트를 모니터링할 수 있습니다.


Performance 탭의 구성

  1. 웹 페이지 녹화를 시작합니다. 녹화를 중단하고 싶을 때 멈출 수 있습니다.
  2. 웹 페이지를 새로고침하여 처음 로딩될 때부터 녹화를 시작합니다.
  3. 녹화된 기록을 삭제할 수 있습니다.
  4. 녹화된 시간동안 각 프레임마다 스냅샷을 확인할 수 있습니다.
  5. 각각의 탭을 통해서 원하는 성능 정보를 모니터링할 수 있습니다.
  6. 찾고 싶은 Activity 를 검색하여 필터링할 수 있습니다.
  7. 녹화된 시간동안 발생한 Activity 목록을 확인할 수 있습니다.


2) Layers

Performance 탭에서 Paint Profiler 를 통해서 확인할 수 있는 정보를 Layers 탭에서 더 자세하게 확인할 수 있습니다. 뒤에서 코드로 살펴볼 예제 코드를 사용하여 Layers 탭에 대해서 살펴보겠습니다. 탭에 들어가면 다음과 같은 화면이 나옵니다.


Layers 탭의 구성

  1. 현재 윈도우의 Layers 를 나타냅니다.
  2. Layers 를 이동하거나 회전하면서 살펴볼 수 있는 컨트롤러입니다.
  3. Layers 구성을 확인할 수 있습니다.
  4. Details 탭에서 Layer 의 세부정보와 Profiler 에서 구체적인 렌더링 과정을 확인할 수 있습니다.
  5. 현재 페이지에서 렌더링된 요소들의 로그를 확인할 수 있습니다.
  6. 슬라이드로 범위를 지정하여 3번과 5번 영역을 필터링할 수 있습니다.


3) Rendering

마지막 개발자 도구인 Rendering 에 대해서 살펴보겠습니다. 해당 탭에서 옵션을 활성화하면 웹 페이지에서 실시간으로 변하는 요소를 더욱 편하게 찾을 수 있습니다.


Rendering 탭은 상단에 없는 경우가 있습니다. 다음 경로에서 찾을 수 있습니다.


처음 들어가면 모든 체크박스가 해제되어 있습니다. 위에서 세 가지 도구만 활성화했습니다.

  • Paint flashing : Repaint 가 일어나는 부분을 초록색으로 표시합니다.
  • Layout Shift Regions : 전환된 부분을 파란색으로 표시합니다.
  • Layer borders : 레이어 테두리를 주황색이나 올리브색으로 표시합니다.

기능을 활성화한 뒤 웹 페이지를 보면 설정한 옵션에 따라서 색상이 나타나는 것을 확인할 수 있습니다.

Rendering 탭 설정했을 때


4. 최적화 방법 확인해보기

앞서 소개한 개발자 도구 중에서 Performance 를 주로 활용하며, 네 가지 서로 다른 방식으로 코딩한 방식을 비교하면서 Reflow 와 Repaint 를 확인해보겠습니다.

*요소는 HTML Element 를 의미합니다. *속성은 CSS Property 를 의미합니다.


1) 여러 요소의 속성을 업데이트

먼저 요소의 width 를 변경하는 경우에 대해서 테스트 해보았습니다.


1-1) 여러 요소의 속성을 따로 업데이트

const btn = document.querySelector('button');
const first = document.querySelector("#first")
const second = document.querySelector("#second")
const third = document.querySelector("#third")
const forth = document.querySelector("#forth")

btn.addEventListener("click", () => {
  const step = 10;
  const firstWidth = first.getBoundingClientRect().width;
  first.style.width = firstWidth + step + "px";

  const secondWidth = second.getBoundingClientRect().width;
  second.style.width = secondWidth + step + "px";

  const thirdWidth = third.getBoundingClientRect().width;
  third.style.width = thirdWidth + step + "px";

  const forthWidth = forth.getBoundingClientRect().width;
  forth.style.width = forthWidth + step + "px";
});


1-2) 여러 요소의 속성을 한꺼번에 업데이트

btn.addEventListener("click", () => {
  const step = 10;
  const firstWidth = first.getBoundingClientRect().width;
  const secondWidth = second.getBoundingClientRect().width;
  const thirdWidth = third.getBoundingClientRect().width;
  const forthWidth = forth.getBoundingClientRect().width;

  first.style.width = firstWidth + step + "px";
  second.style.width = secondWidth + step + "px";
  third.style.width = thirdWidth + step + "px";
  forth.style.width = forthWidth + step + "px";
});


두 코드의 차이점은 Element.style.Property 를 설정하는 코드가 연속적으로 존재하는지 여부입니다. 이러한 소스 코드의 차이가 어떠한 점에서 다른지 Performance 탭에서 확인해보겠습니다.


1-1) 의 결과입니다. Layout 이벤트가 총 5번 발생한 것을 알 수 있습니다.


1-2) 의 결과입니다. Layout 이벤트가 총 2번 발생한 것을 알 수 있습니다.

위의 두 사진에서 확인할 수 있듯이 1-2) 여러 요소의 속성을 한꺼번에 업데이트 의 경우가 Layout 이벤트를 3번 덜 발생시키는 것을 알 수 있습니다. 이는 Element.style.Property 처럼 속성을 업데이트하는 코드가 모여 있기 때문에 Layout 을 재계산할 때 변경된 값이 한 번에 적용되기 때문입니다. 따라서 Layout 을 변경시키는 값을 코드 상에서 모아서 작성해야 Reflow 를 적게 발생시킨다는 것을 알 수 있습니다.


2) 한 요소의 속성을 업데이트

다음으로는 아이디가 first 인 요소의 속성만 업데이트하는 경우에 대해서 테스트 해보았습니다.


2-1) 한 요소의 속성을 따로 업데이트

const btn = document.querySelector('button');
const first = document.querySelector("#first")

btn.addEventListener("click", () => {
  const step = 10;
  const firstFontSize = "4em";
  first.style.fontSize = firstFontSize;

  const firstWidth = first.getBoundingClientRect().width;
  first.style.width = firstWidth + step + "px";

  const firstBackground = "burlywood";
  first.style.backgroundColor = firstBackground;
});


2-2) 한 요소의 속성을 한꺼번에 업데이트

btn.addEventListener("click", () => {
  const step = 10;
  const firstFontSize = "4em";
  const firstWidth = first.getBoundingClientRect().width;
  const firstBackground = "burlywood";

  first.style.fontSize = firstFontSize;
  first.style.width = firstWidth + step + "px";
  first.style.backgroundColor = firstBackground;
});

앞서 1) 여러 요소의 속성을 업데이트 의 예시에서 실험해봤던 점을 하나의 요소에서 동일하게 진행해보았습니다. 다른 점은 width 속성뿐만 아니라 fontSizebackgroundColor 속성까지 변경한다는 점입니다.


2-1) 의 결과입니다. Layout 이벤트가 총 3번 발생한 것을 알 수 있습니다.


2-2) 의 결과입니다. Layout 이벤트가 총 2번 발생한 것을 알 수 있습니다.

width 와 더불어 fontSize 를 변경한다는 점 때문에 2-1) 한 요소의 속성을 따로 업데이트 에서 Reflow 가 1번 더 일어난다는 점을 알 수 있습니다.


3) 노출 제어를 통한 속성 설정

요소를 화면에 표시되지 않는 상태로 만든 뒤에 원하는 속성을 설정하고, 다시 이전처럼 화면에 나타나도록 속성을 복원합니다. 이를 통해서 화면에 렌더링되지 않는 상태에서 스타일을 변경할 수 있기 때문에 Paint 할 때 드는 연산을 줄일 수 있습니다. 이를 실험을 통해서 확인해보겠습니다.


3-1) Opacity 를 통한 노출 제어

btn.addEventListener("click", () => {
  const step = 10;
  const firstFontSize = "4em";
  const firstWidth = first.getBoundingClientRect().width;
  const firstBackground = "burlywood";

  first.style.opacity = 0;
  first.style.fontSize = firstFontSize;
  first.style.width = firstWidth + step + "px";
  first.style.backgroundColor = firstBackground;
  first.style.opacity = 1;
});


3-2) Display 를 통한 노출 제어

first.style.display = "none";
first.style.fontSize = firstFontSize;
first.style.width = firstWidth + step + "px";
first.style.backgroundColor = firstBackground;
first.style.display = "block";


3-3) Visibility 를 통한 노출 제어

first.style.visibility = "hidden";
first.style.fontSize = firstFontSize;
first.style.width = firstWidth + step + "px";
first.style.backgroundColor = firstBackground;
first.style.visibility = "visible";

세 코드는 각각 opacity, display, visibility 속성을 조절하여 요소를 화면에서 지웁니다. 2) 한 요소의 속성을 업데이트 에서 진행한 것과 동일하게 3개의 속성을 수정한 뒤에 다시 화면에 렌더링한 것을 Performance 탭에서 녹화한 결과는 다음과 같습니다.


3-1) 의 결과입니다. Layout 은 0.3ms, Paint 는 0.4ms, Composite Layers 는 1.2ms 가 걸렸습니다.


3-2) 의 결과입니다. Layout 은 0.3ms, Paint 는 0.5ms, Composite Layers 는 0.9ms 가 걸렸습니다.


3-3) 의 결과입니다. Layout 은 0.6ms, Paint 는 0.9ms, Composite Layers 는 1.4ms 가 걸렸습니다.

앞의 두 경우에서 opacity 속성을 사용한 경우가 0.3ms만큼 더 Composite Layers 이벤트에 시간을 할애한 것을 확인할 수 있습니다. 또한 Layout 의 경우 절대적인 시간은 동일한 것으로 보이나 비율로 보았을 때, opacity 는 4.7%, display 는 6.1% 걸린 것을 확인할 수 있습니다. 이는 2. Reflow 와 Repaint 에서 첨부한 이미지에서 확인할 수 있듯이 opacity 속성은 Composite Layers 에 영향을 주고 display 속성은 Layout 에 영향을 주기 때문이라고 해석할 수 있습니다. 한편, 마지막 예제에서 Paint 가 0.9ms 가 걸렸습니다. 이는 visibility 속성이 Paint 에 영향을 주기 때문이라고 할 수 있습니다. 참고로 각 테스트 경우에서 Composite Layers 이벤트가 발생한 횟수는 92회, 52회, 53회였습니다.


4) 노드 복제를 통한 속성 설정

마지막으로 노드를 복제하고, 복제한 노드의 속성을 수정하는 방식으로도 렌더링할 수 있습니다.


const btn = document.querySelector('button');
const first = document.querySelector("#first")

btn.addEventListener("click", () => {
  const cloned = first.cloneNode(true);
  const step = 10;
  const firstFontSize = "4em";
  const firstWidth = first.getBoundingClientRect().width;
  const firstBackground = "burlywood";

  cloned.style.fontSize = firstFontSize;
  cloned.style.width = firstWidth + step + "px";
  cloned.style.backgroundColor = firstBackground;

  first.parentNode.replaceChild(cloned, first);
});

스타일을 한꺼번에 업데이트 한 2-2) 한 요소의 속성을 한꺼번에 업데이트 의 결과와 비교해보겠습니다.


4) 의 결과입니다. Layout 이 0.3ms, Paint 가 0.4ms 가 걸렸습니다.


2-2) 의 결과입니다. Layout 이 0.3ms, Paint 가 0.4ms 가 걸렸습니다.

두 가지 최적화 방식 모두 Reflow 와 Repaint 가 동일한 시간이 걸린 것을 확인할 수 있습니다.


5. 결론

  • Reflow 와 Repaint 는 웹 페이지 변경 요소에 따라서 발생합니다.
  • 해당 이벤트들을 브라우저의 개발자 도구를 통해서 확인할 수 있습니다.
  • 렌더링 최적화를 위해서
    • 1) 스타일 속성을 업데이트하는 코드는 한 곳에 모아서 작성합니다.
    • 2) 노출 제어를 통해서 최적화할 수 있습니다. opacity 속성이 가장 유효합니다.
    • 3) 노드를 복사하는 방식으로도 최적화할 수 있습니다.

6. 생각해볼 점

  • 단순한 구조로 테스트해서 눈에 띄는 차이를 확인하기 힘들었습니다. 더 복잡한 구조에서 실험해볼 필요가 있습니다.

[출처 & 참고]


[코드 링크]