브라우저는 화면을 Rendering하는 과정에는 배치(flow), 그리기(paint) 가 포함되어있다.
오늘날 대부분의 기기는 초당 60회의 빈도로 화면 뿌려주고 있다. 실행 중인 애니메이션 또는 화면전환이 있거나, 사용자가 페이지를 스크롤 중이면, 브라우저가 기기의 빈도에 일치하도록 각 화면 새로 고침에 대해 하나의 새 그림이나 프레임을 제공하려고 하고 있다.
각 프레임에는 16ms 가량의 시간이 할당된다. (1초/60 = 16.66ms). 실제로 브라우저는 실행 준비를 해야 하므로 10ms 내에 모든 작업을 완료해야 한다. 이 제한 시간을 충족하지 못하면 프레임 속도가 떨어지고 화면에서 콘텐츠가 끊어진다. 이러한 현상을 우리는 흔히 버벅거림 현상이라고 한다.
더 자세한 내용을 알고 싶으면 아래의 NAVER D2 자료를 참고하면 된다.
<body>
<p>The Caterpillar and Alice looked at each other for some time in silence:
at last the Caterpillar took the hookah out of its mouth, and addressed
her in a languid, sleepy voice.</p>
</body>
<style>
p {
display: inline;
animation-duration: 3s;
animation-name: slidein;
}
@keyframes slidein {
from {
margin-left: 100%;
width: 300%
}
to {
margin-left: 0%;
width: 100%;
}
}
</style>
우리가 위와 같이 부드럽게 보이도록 작업할 수 있는 영역을 간단하게 표현하게 되면 아래와 같이 5개의 단계가 있다. 오늘은 그 중 Layout과 Paint에 대해서 보자.
생성된 DOM 노드의 레이아웃이 변경될 때, 브라우저는 변경 후 영향을 받는 모든 노드를 다시 계산하고 렌더트리를 재생성한다.
이러한 과정을 Reflow
라 하고 Reflow
가 일어나게 되면 위의 사진에서 보이듯이 paint
, composite
이 일어난다.
브라우저의 특정 api에는 강제로 Reflow를 일으키는 것들이 있다. 아래의 링크를 확인해보자.
<body>
<p>The Caterpillar and Alice looked at each other for some time in silence:
at last the Caterpillar took the hookah out of its mouth, and addressed
her in a languid, sleepy voice.</p>
</body>
<script>
const pElement = document.getElementsByTagName('p')[0];
function test() {
for (let i = 0; i < 1000; i++) {
const offsetLeft = pElement.offsetLeft
}
}
test();
</script>
test function 안쪽을 돌게 되면 style을 재계산을 하고 Laytout 하는 것을 볼 수 있다.
Layout 영역을 열어보면 line by line로 걸린 시간 역시 Dev Tool에서 확인할 수 있다.
생성된 DOM 노드에 대하여 style을 변경시켰을 때, Reflow
는 발생하지 않는다.
레이아웃 수치에 대한 변경이 일어나지 않는다면, Reflow
가 일어나지 않고, Repaint
만 일어난다.
<body>
<p id="test">The Caterpillar and Alice looked at each other for some time in silence:
at last the Caterpillar took the hookah out of its mouth, and addressed
her in a languid, sleepy voice.</p>
</body>
<script>
setTimeout(() => {
document.getElementById('test').style.color = 'red'
}, 3000);
</script>
Repaint
와 Reflow
가 많아질수록 애플리케이션의 렌더링 성능은 느려지게 된다.
즉, 이를 줄일수록 성능을 높일 수 있다.
- Style 속성을 통해 설정하면, Reflow가 발생한다.
- Element의 클래스가 변경될 때 한 번의 Reflow만 발생한다.
- Inline Style은 HTML이 다운로드될 때, 레이아웃에 영향을 미치면서 추가 Reflow를 발생시킨다.
<div id="animation" style="background:blue;position:absolute;"></div>
프레임에 따라 Reflow 비용이 많은 애니메이션 효과엔 노드의 position
을 absolute
나 fixed
로 주면 전체 노드에서 분리된다.
이 경우엔, 전체 노드에 걸쳐 Reflow 비용이 들지 않으며 해당 노드의 Repaint 비용만 들어가게 된다.
- 한 번에 1px씩 Element를 이동하면 부드러워 보이지만, 성능이 저하되는 원인이 될 수 있다.
- Element를 한 프레임당 4px씩 이동하면 덜 부드럽게 보이겠지만, Reflow 처리의
1/4
만 필요하게 된다.
<table>
은 점진적으로 렌더링되지 않고, 모두 불려지고 계산된 다음에서야 렌더링이 된다. 또한, 작은 변경만으로도 테이블의 다른 모든 노드에 대한 Reflow가 발생한다.- 레이아웃 용도가 아닌 데이터 표시 용도의
<table>
을 사용하더라고,table-layout: fixed
속성을 주는 것이 좋다.table-layout: fixed
를 사용하면, 열 너비가 머리글 행 내용을 기반으로 계산되기 때문이다.
- 사용하는 규칙이 적을수록 Reflow가 빠르다.
display: none;
으로 숨겨진 Element는 변경될 때, Repaint나 Reflow를 일으키지 않는다. 그렇기 때문에 Element를 표시하기 전에 Element를 변경한다.
//Before
const container = document.getElementById('container');
container.style.padding = "20px";
container.style.border = "10px solid red";
container.style.color = "blue";
//After cssText
container.style.cssText = 'padding:20px;border:10px solid red;color:blue;';
//After class
container.className = 'test';
- 3 개의 리스트를 추가하는 경우, 한 번에 하나씩 추가하면 최대 7 개의 Reflow가 발생한다.
<ul>
이 추가될 때<li>
에 대해 3번- 텍스트 노드에 대해 3번
const frag = document.createDocumentFragment();
const ul = frag.appendChild(document.createElement('ul'));
for (let i = 1; i <= 3; i++) {
li = ul.appendChild(document.createElement('li')); li.textContent = `item ${ i }`;
}
document.body.appendChild(frag);
- 브라우저는 레이아웃 변경을 큐에 저장했다가 한 번에 실행함으로써 Reflow를 최소화하는데,
offset
,scrollTop
과 같은 계산된 Style 정보를 요청할 때마다 정확한 정보를 제공하기 위해 큐를 비우고, 모든 변경을 다시 적용한다. - 이를 최소화하기 위해 수치에 대한 Style 정보를 변수에 저장하여 정보 요청 횟수를 줄임으로써 Reflow를 최소화한다.
for (let i = 0; i < len; i++) {
el.style.top = `${el.offsetTop + 10}px`; el.style.left = `${el.offsetLeft + 10}px`;
}
// Bad practice
let top = el.offsetTop, left = el.offsetLeft, elStyle = el.style;
for (let i = 0; i < len; i++) {
top += 10; left += 10; elStyle.top = `${top}px`; elStyle.left = `${left}px`;
}
// Good practice
Element에 어떠한 변경을 할 것인지를 미리 브라우저에 알려주는 것이 will-change
속성의 역할이다. 이를 사용하면 변경이 시작되기 전에 미리 브라우저에게 알려주어 will-change
속성이 사용된 특정 Element와 컨텐츠는 별도릐 Layer로 관리되고 나중에 다시 합쳐진다. 즉, 페이지 출력에 악영향을 줄 수 있는 처리 비용을 줄일 수 있다는 것이다.
이를 사용하게 되면 효율적으로 Element의 변경 또는 렌더링을 처리할 수 있고 페이지는 순식간에 갱신되어 부드러운 화면 처리가 가능하게 된다.
그러나 will-change는 무분별하게 사용하면 성능저하가 발생하고 결과적으로 페이지의 작동이 중단되는 사태가 발생할 수 있다.