일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |
- 컴포넌트 생명주기
- 두 포인터
- SW EA
- 정적타입언어
- 슬라이딩 윈도우
- next14
- 마진 상쇄
- react18
- 리액트
- webpack5
- 이벤트 생명주기
- 누적합
- vscode
- 분할정복
- 수학
- 값복사
- 렌더링 최적화
- 레이아웃 스래싱
- 공백찾기
- 이분탐색
- 브루트포스
- BFS
- 즉시실행함수
- 백준
- react
- 레퍼런스복사
- 재귀
- 동적타입언어
- 구현
- webpack
- Today
- Total
D.JOUNG
레이아웃 스래싱, 예제로 이해하기 본문
개요
최근 렌더링 최적화를 공부하면서 리플로우와 리페인팅 방지에 각별히 신경을 쓰게 됐다. 리팩토링하는 코드 내에서 getBoundingClientRect() 메소드를 자주 호출하는 부가 있는데, getBoundingClientRect()는 아래 포스팅에 정리되어있는 리플로우 유발 메소드 중 하나다.
https://gist.github.com/paulirish/5d52fb081b3570c81e3a
What forces layout/reflow. The comprehensive list.
What forces layout/reflow. The comprehensive list. - what-forces-layout.md
gist.github.com
그런데 성능탭을 열어 타임라인을 살펴봐도 성능에 문제가 될 정도로 리플로우가 발생하고 있는 영역은 없었다. 내가 잘못된 정보를 접했던걸가? 의문을 느끼며 몇차례 검색을 반복하다가 아래의 글을 발견했다.
https://yrnana.dev/post/2021-03-25-offsettop-reflow/
OffsetTop을 읽으면 reflow가 발생한다? | nana.log
offsetTop을 읽는 것 자체가 무조건 reflow를 발생시키지는 않는다. 하지만 offsetTop을 읽기 위해서 브라우저가 렌더링 큐에 쌓인 모든 작업을 수행하면서 reflow를 발생시킬 수 있다.
yrnana.dev
읽어보니 나와 거의 비슷한 고민을 하셨다. 내용을 정리하자면, 브라우저는 최적화를 위해 리플로우가 발생했을 경우 변경 사항을 최대한 한 번에 몰아서 처리하도록 만들어졌다. box.style.width = 400px 같은 명령으로 리플로우가 필요한 작업 건이 생겼을 때, 내부 큐에 해당 작업을 저장했다가 한번의 리플로우로 일괄 처리한다는 모양이다.
하지만 box.style.width = 400px 명령 이후 바로 box.getBoundingClientRect() 같은 메소드를 사용하면, getBoudingClientRect() 처럼 최신 계산값을 제공해야하는 메소드들은 큐에 저장된 변경 작업을 곧장 실행하여 최신 계산 값을 리턴하게 된다
위와 같은 원인으로 렌더링 성능이 느려지는 현상을 레이아웃 스레싱 이라고 부른다. 레이아웃 스레싱은 ‘강제 동기식 레이아웃’ 현상이 반복적으로 발생하는 상태를 의미하기도 한다.
‘강제 동기식 레이아웃’이란?
JS 파일에서 DOM 요소의 위치나 크기 값을 변경 후 계산 값을 바로 가져오려 하면 강제 리플로우(레이아웃)가 발생하는데, 이것을 강제 동기식 레이아웃이라고 한다. 단순히 설명만 들어서는 무슨 현상인지 감이 잘 안잡힌다. 예제와 함께 살펴보자.
레이아웃 스레싱은 주로 for 문 내에서 여러개 요소 크기를 한번에 변경할 때 자주 발생한다. 예를 들면, 아래와 같은 코드에서는 레이아웃 스레싱이 발생하고 있다.
function resizeAllParagraphsToMatchBlockWidth () {
// Puts the browser into a read-write-read-write cycle.
for (let i = 0; i < paragraphs.length; i++) {
paragraphs[i].style.width = `${box.offsetWidth}px`;
}
}
offsetWidth는 getBoundingClientRect() 처럼 최신 계산 결과를 중시하는 프로퍼티다. offsetWidth를 호출할 경우 브라우저는 box의 offsetWidth 값을 첫번째 paragraphs의 width에 반영한다.
여기까지는 문제가 없지만, 그 다음 반복에서 offsetWidth는 이전에 바뀐 paragraphs 요소에 대한 계산 결과를 리플로우 시킨 다음, 최신 계산이 이루어진 상태에서의 결과를 현재 paragraphs.style.width에 넣어준다.
이런 식으로 paragrapths.length 값 만큼 리플로우가 반복되는 구조인 것이다.
개선안은 아래와 같다. offsetWidth를 반복문 전 한번만 실행시킨다. 아래 코드는 반복문이 전부 끝난 후 모든 재계산 결과가 한 번의 리플로우로 일괄 처리된다.
// Read.
const width = box.offsetWidth;
function resizeAllParagraphsToMatchBlockWidth () {
for (let i = 0; i < paragraphs.length; i++) {
// Now write.
paragraphs[i].style.width = `${width}px`;
}
}
Chrome for developer 페이지의 런타임 성능 분석 페이지의 예제
아래 페이지는 크롬 개발자도구 성능탭 사용법을 설명하는데, 예제로 렌더링 최적화 시뮬레이션 코드를 제공한다. 네모 div를 무수히 많이 추가하면 애니메이션이 버벅이기 시작하다가 '최적화' 버튼을 누르면 거짓말처럼 정상화된다. 렌더링 최적화 관련해 자료를 찾다 보면 해당 코드가 transform 속성을 사용했을 것이라고 적당히 짐작하다가, 코드를 뜯어보고 '어? 아니네?' 했다는 포스팅이 종종 보인다. 해당 예제는 transform의 t자도 쓰지 않았으며, 레이아웃 스래싱을 해결함으로써 성능을 개선했다.
https://developer.chrome.com/docs/devtools/performance?hl=ko
런타임 성능 분석 | Chrome DevTools | Chrome for Developers
Chrome DevTools에서 런타임 성능을 평가하는 방법을 알아보세요.
developer.chrome.com
아래 코드는 예제 코드 전문 중에서 애니메이션 동작을 처리하는 부분만 가져온 것이다. 네모 Div의 개수만큼 for문을 반복하면서 각 네모 Div의 top 값을 변경해주는 코드다. 기본 로직은 동일하지만, 최적화 코드에서는 offsetTop 대신 style.top 값을 가져오고 있다는 점이 다르다.
offsetTop 프로퍼티는 getBoundingClientRect() 처럼 사용자에게 최신 데이터를 계산해주려고 하기 때문에, 대기 중인 리플로우 작업이 있으면 강제 동기식 레이아웃을 발생시켜 누적된 리플로우 작업을 바로 반영해버린다. 이렇게 되면 위에서 간단히 살펴봤던 예시처럼 네모 Div의 개수만큼 리플로우가 발생하는 코드가 되는 것이다.
app.update = function (timestamp) {
for (var i = 0; i < app.count; i++) {
var m = movers[i];
if (!app.optimize) { // 최적화X 로직
var pos = m.classList.contains('down') ?
m.offsetTop + distance : m.offsetTop - distance;
if (pos < 0) pos = 0;
if (pos > maxHeight) pos = maxHeight;
m.style.top = pos + 'px';
//(중략)
} else { // 최적화O 로직
var pos = parseInt(m.style.top.slice(0, m.style.top.indexOf('px')));
m.classList.contains('down') ? pos += distance : pos -= distance;
if (pos < 0) pos = 0;
if (pos > maxHeight) pos = maxHeight;
m.style.top = pos + 'px';
//(중략)
}
}
frame = window.requestAnimationFrame(app.update);
}
가장 좋은 방법은 transform 속성을 사용해 리플로우는 물론이고 리페인팅 단계까지 생략해버리는 것이겠지만, 뭐든 만능이 아닌 만큼 불가피하게 리플로우를 유발하는 속성을 사용하게 된다면 레이아웃 스래싱에 주의할 필요가 있겠다.
'웹 > javascript' 카테고리의 다른 글
자바스크립트(javascript) 버블링, 캡쳐링 단계에서 이벤트 실행하기 (1) | 2024.03.27 |
---|---|
javascript 정규식(정규 표현식) 사용하기 (1) | 2024.02.04 |