force layout

js에서 요청/호출되는 일부 속성은 브라우저가 스타일과 레이아웃을 동기적으로 계산하도록 한다.

layout trashing

웹 브라우저는 레이아웃 변경을 즉시 처리하지 않고 비슷한 Style의 수정을 모아서 하게 된다. 즉 화면에서 레이아웃에 대한 정보를 실제로 알아야 할 때까지 레이아웃 계산을 지연시킨다.

layout trashing이 발생하는 경우

DOM이 변경되지 않았음을 보장할 수 있는 경우엔 레이아웃 캐시(이전 계산된 값)에서 값을 가져온다.

하지만 offsetHeight과 같은 최신으로 동기화된 레이아웃에 대한 정보에 접근하는 속성같은 경우 강제로 레이아웃 계산(리플로우)를 발생시켜 동기화 후 값을 가져오게 된다.
즉 아래와 같이 코드를 작성하게 되면 불필요한 레이아웃 계산을 과정이 요구된다.

1
2
3
4
elementA.className = "a-style";
var heightA = elementA.offsetHeight; // layout is needed
elementB.className = "b-style"; // invalidates the layout
var heightB = elementB.offsetHeight; // layout is needed again

layout trashing이 발생하지 않는 경우

위 코드는 다음과 같이 수정할 수 있다.

1
2
3
4
elementA.className = "a-style";
elementB.className = "b-style";
var heightA = elementA.offsetHeight; // layout is needed and calculated
var heightB = elementB.offsetHeight; // layout is up-to-date (no work)

똑같은 작업을 하는 코드지만 위의 코드에 비해 레이아웃 계산의 수가 줄어들게 된다.
레이아웃 계산을 강제로 발생시키는 속성은 여기에서 확인할 수 있다.

연속된 layout trashing 최적화하기

paragraph의 너비를 box의 너비와 같도록 하는 코드를 작성한다고 생각해보자

1
2
3
4
5
6
function resizeAllParagraphsToMatchBlockWidth() {
// Puts the browser into a read-write-read-write cycle.
for (var i = 0; i < paragraphs.length; i++) {
paragraphs[i].style.width = box.offsetWidth + "px";
}
}

위와같이 작성한다면 box의 offsetWidth에 접근한 뒤 paragraphs[i]의 너비로 설정하게 된다.

offsetWidth 속성에 접근할 때마다 강제로 레이아웃 계산을 시도하게 되므로 단일 paragraph마다 layout 계산이 일어나게 된다.

이는 다음과 같이 수정할 수 있다.

1
2
3
4
5
6
7
function resizeAllParagraphsToMatchBlockWidth() {
let width = box.offsetWidth;
for (var i = 0; i < paragraphs.length; i++) {
// Now write.
paragraphs[i].style.width = width + "px";
}
}

reflow를 강제로 발생시켜 애니메이션 실행하기

브라우저는 비슷한 style변화를 모아서 반영하기 때문에 원하는대로 애니메이션이 작동하지 않을 때가 있다. 이럴 때는 강제로 reflow를 발생시켜 해결할 수 있다.

1
2
3
4
5
6
7
8
<h2>no force reflow</h2>
<div class="wrapper">
<div class="item"></div>
</div>
<h2>force reflow</h2>
<div class="wrapper">
<div class="item2"></div>
</div>

아래는 css이다. 애니메이션을 해제하는 클래스를 정의하였다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
.wrapper {
width: 500px;
height: 50px;
position: relative;
border: 1px solid black;
}
.item,
.item2 {
height: 100%;
width: 50px;
background: red;
position: absolute;
left: 0;
-webkit-transition: left 500ms linear;
transition: left 500ms linear;
}
.notransition {
-webkit-transition: none !important;
-moz-transition: none !important;
-o-transition: none !important;
transition: none !important;
}

제대로 작동되지 않는 경우

브라우저의 작동원리로 인해 애니메이션이 제대로 작동하지 않는 경우이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
const $wrapper = document.querySelector(".wrapper");
const $item = document.querySelector(".item");
const $item2 = document.querySelector(".item2");

$item.addEventListener("click", function () {
const itemWidth = $item.clientWidth;
const wrapperWidth = $wrapper.clientWidth;
const itemRightLimit = wrapperWidth - itemWidth;
$item.classList.add("notransition");
$item.style.left = `${itemRightLimit}px`;
$item.classList.remove("notransition");
$item.style.left = "50px";
});

코드만 보았을 때 item의 애니메이션 동작은 다음과 같은 순서로 일어나야 한다.

  1. 애니메이션 해제(notransition 추가)
  2. 박스가 부모박스 오른쪽 끝에 붙도록 left 수정
  3. 애니메이션 설정(notransition 제거)
  4. left50px로 수정

실제로는 다음과 같이 동작한다.

  1. Reflow(속성에 접근했기 때문)
  2. style 수정을 통합함
  3. 애니메이션 설정(notransition 제거)
  4. left50px로 수정(통합된 변경사항)
  5. Repaint

제대로 작동하는 경우

reflow를 강제로 발생시켜 제대로 작동하는 코드이다.

1
2
3
4
5
6
7
8
9
10
11
//...
$item2.addEventListener("click", function () {
const item2Width = $item2.clientWidth;
const wrapperWidth = $wrapper.clientWidth;
const itemRightLimit = wrapperWidth - item2Width;
$item2.classList.add("notransition");
$item2.style.left = `${itemRightLimit}px`;
$item2.clientLeft;
$item2.classList.remove("notransition");
$item2.style.left = "50px";
});

의도한 애니메이션은 item과 같다.

그리고 의도한대로 작동한다. item2의 동작 순서는 다음과 같다.

  1. Reflow
  2. 애니메이션 해제(notransition 추가)
  3. 빨간 박스가 부모박스 오른쪽 끝에 붙도록 left 수정
  4. Repaint
  5. Reflow
  6. 애니메이션 설정(notransition 제거)
  7. left50px로 수정
  8. Repaint

4와 5의 Repaint와 Reflow는 clientLeft속성을 조회하여 브라우저를 강제로 동기화했기 때문에 일어난다.

실제 작동하는 예시는 여기에서 예제를 확인할 수 있다.

Ref

What forces layout / reflow

WHAT IS A FORCED REFLOW AND HOW TO SOLVE IT?

Javascript로 애니메이션 제어하기 (Force Reflow)

댓글