리플로우와 리페인트를 줄이는 CSS 전략

원정·2025년 5월 11일
28

성능 최적화

목록 보기
1/1
post-thumbnail

예전에 스터디에서 어떤 분이 리렌더링을 발생시키는 CSS 속성을 최소화하여 브라우저 렌더링 성능을 향상하는 방법이 있다고 얘기해준 적이 있다.
언제 한 번 공부해서 정리해야지 하고 미루고 있던 숙제를 늦게나마 풀어본다.

리렌더링이란?


클라이언트 단에서 성능 최적화에 가장 중요한 건 이미지 최적화와 리렌더링 최소화가 아닐까 싶다.

여기서 말할 리렌더링은 리액트에서 말하는 리렌더링과는 다르다.
리액트의 리렌더링은 컴포넌트 재실행되어 가상 DOM이 갱신되는 과정을 말한다.
이 역시 성능에 큰 영향을 미치고 최소화하는 것이 중요하다.

하지만 이 글에서 다룰 리렌더링은 리플로우(Reflow)와 리페인트(Repaint), 즉 브라우저가 화면을 다시 계산하고 그리는 과정을 포괄하는 용어다.
보통 이 두 단계를 묶어 리렌더링이라고 부르지만, 공식적인 렌더링 엔진 용어는 아니라고 한다.

아무튼 리렌더링을 최소화한다는 건, 리플로우와 리페인트를 최소화한다는 말과 같다.
이 중에서도 리플로우는 레이아웃 자체를 다시 계산하는 가장 무거운 작업이기 때문에 특히 신경을 써야 한다.

리렌더링이 발생하지 않는 CSS 속성으로는 transform, opacity 등이 있다.
이 속성들은 GPU에서 처리하기 때문에 성능 부담을 낮춰준다.

리페인트를 발생시키는 속성으로는 color, background-color, visibility, border-color, outline, box-shadow 등이 있다.

마지막으로 리플로우를 발생시키는 속성으로는 width, height, padding, margin, border-width, position, top, right, bottom, left, display, flex, grid, inline-block, font-size, line-height, overflow, white-space 등이 있다.

속성을 성의없이 나열한 이유는 하나하나 암기하는 것보다 렌더링 이후에 어떤 속성이 변경되느냐에 따라 성능에 미치는 영향이 훨씬 크다고 느꼈기 때문이다.
처음 CSS를 적용할 때 어떤 속성이든 큰 문제는 없지만, 렌더링된 DOM이 동적으로 변화할 때 발생하는 불필요한 리플로우, 리페인트를 줄이는 게 핵심이다.

리렌더링은 결국 렌더링 이후, 화면을 다시 계산하고 그리는 과정이기 때문에 "어떤 CSS를 썼느냐"보다, "언제, 어떻게 바뀌느냐가 중요하다"는 건 어찌 보면 당연한 말이다.

처음에 이 주제에 접근할 땐 "어떤 속성을 써야 하지?"라고 접근했다가, 어떤 CSS 속성이 리렌더링을 유발하는지 아는 것도 중요하지만, 정작 중요한 건 그것들이 "렌더링 후에 동적으로 바뀔 때" 성능에 영향을 준다는 점이었다.

전략


1. 상위 요소의 변화는 모든 자식 요소에게 영향을 미친다.

자식 요소는 positionfixed 또는 absolute가 아니라면 부모 요소의 크기나 위치가 변경될 때마다 영향을 받는다.
즉, 상위 요소에 스타일을 변경하면 그 하위 전체가 리플로우 대상이 될 수 있다.

따라서 특정 부분만 변화가 필요한 상황이라면, 가능한 그 요소에만 직접 스타일을 주는 것이 좋다.

반면 fixed, absolute는 Normal Flow(문서의 기본 배치 흐름)에 벗어나기 때문에 주변 요소나 부모 레이아웃에 영향을 주지 않는다.
애니메이션이 들어가는 요소라면 positionfixedabsolute로 주고, 위치 변경은 top, left 대신, transform으로 처리하는 것이 좋다.

2. 레이아웃 계산을 강제하는 코드를 조심하자.

JavaScript에서 element.style을 이용해 직접 스타일을 변경할 때, 다음과 같이 스타일을 연달아 변경하면

el.style.width = '100px';
el.style.height = '50px';
el.style.margin = '10px';

브라우저는 이를 최적화해서 한 번에 처리할 수 있다.

하지만 스타일을 변경하는 중간에 레이아웃 정보를 읽는 작업이 들어가면 이야기가 달라진다.
offsetWidth, offsetHeight, clientWidth, clientHeight, scrollTop, scrollLeft, scrollHeight, getBoundingClientRect(), getComputedStyle(el)과 같은 속성들은 모두 브라우저가 현재 레이아웃을 계산해야만 알 수 있다.

따라서 아래와 같이 스타일을 조작하면서 중간에 레이아웃 정보를 읽는 경우

el.style.width = '100px';
const h = el.offsetHeight;
el.style.margin = '10px';

브라우저는 offsetHeight를 계산하기 위해 지금까지 적용된 스타일을 반영해 레이아웃을 강제로 리플로우를 발생시킨다.
이런 조작 → 읽기 → 또 조작 흐름이 반복되면 레이아웃 스레싱(layout thrashing)이 발생할 수 있다.

이를 방지하려면 읽기 작업과 쓰기 작업을 분리하거나, 스타일 변경은 모아서 한 번에 처리하는 것이 좋다.
또한 requestAnimationFrame을 활용해 다음 프레임에 반영되도록 조정하는 것도 좋은 방법이다.

requestAnimationFrame

requestAnimationFrame은 브라우저가 다음 프레임을 렌더링하기 직전에 콜백 함수를 실행시켜주는 API다.
여기서 말한 프레임은 단순한 DOM 조작을 넘어, 스타일, 애니메이션, 스크롤, 포커스 등 모든 시각적인 요소가 반영되는 단위를 말한다.

처음엔 "프레임을 렌더링한다"는 표현이 꽤 헷갈렸지만, 브라우저가 화면을 실제로 다시 그리는 시점이라고 이해하면 조금 더 감이 온다.

브라우저는 초당 60 프레임을 목표로 렌더링하고, transform과 같은 속성이 바뀌면 브라우저는 반영하기 위해 다음 프레임을 예약한다.
즉, transform은 DOM 자체를 바꾸진 않지만, 시각적인 변화가 생기기 때문에 프레임을 유발한다.

requestAnimationFrame은 프레임 타이밍에 맞춰 콜백을 실행하여 부드러운 애니메이션을 제공하고, 읽기와 조작을 분리하여 레이아웃 스레싱을 방지할 수 있다는 장점이 있다.

음... 사실 적으면서도 렌더링과 프레임에 대한 생각이 분리되지 않는다.
뭔가 같이 묶어서 생각하자니 별개같고, 별개로 생각하자니 엮인 부분이 많은 것 같다.
어벤져스와 X맨의 관계랄까.

굳이 나눠서 정의를 한다면, 렌더링은 변화에 대한 계산 과정, 프레임은 그걸 사용자에게 보여주는 타이밍이라고 볼 수 있다.
대충 몽말인지 알지?

3. 기타

3-1. 테이블 기반 레이아웃 사용을 자제하자.

<table>은 내부 셀 간 의존 관계가 복잡하여 작은 변화에도 전체 테이블이 리플로우된다.
단순히 레이아웃을 구성하는 목적이라면 flexgrid를 사용하는 것이 좋다.

3-2. CSS에서 JS 표현식을 자제하자.

이 항목은 CSS-in-JS 또는 런타임 기반의 스타일링 방식에서 해당된다.
${} 같은 표현식은 렌더링 시점마다 다시 계산될 수 있으므로, 불필요한 재계산을 유발하지 않도록 주의해야 한다.

3-3. 깊고 복잡한 선택자 사용을 자제하자.

div ul li span {...}처럼 구조가 깊은 선택자를 사용하면, 브라우저는 span을 찾은 뒤, 부모가 li, ul, div인지 역방향으로 계속 탐색한다.
이는 스타일 계산 성능에 영향을 줄 수 있으며, 유지보수 측면에서도 불리하다.
가능하면 선택자의 깊이를 단순하게 유지하고, 필요하다면 클래스를 명확하게 지정하는 것이 좋다.

profile
https://wonjung-jang.github.io/ 로 이동했습니다!

24개의 댓글

comment-user-thumbnail
2025년 5월 11일

오 div ul li span {...} 이렇게 사용하는 구조가 스타일 계산 성능에 영향을 줄 수 있다는 건 몰랐네요! 리플로우, 리페인트에 대한 건 많이 들어봤어도 이걸 개선하기 위한 방법은 깊이 있게 다뤄보지 않았던 것 같은데 대표적인 나쁜 사례와 대체 안에 대해서도 나와있어 좋았습니다👍

1개의 답글
comment-user-thumbnail
2025년 5월 11일

중첩해서 스타일을 사용하는 구조가 성능에 영향을 미친다니! 좋은 정보 알고 갑니다!

1개의 답글
comment-user-thumbnail
2025년 5월 11일

좋은 정보 감사합니다 :)

1개의 답글
comment-user-thumbnail
2025년 5월 12일

간과하기 좋은 부분들이 잘 정리되어 있는거 같아요:) 잘 읽었습니다!

1개의 답글
comment-user-thumbnail
2025년 5월 28일

렌더링 최적화는 정말 깊이 파면 끝이 없는 영역인 것 같아요. 좋은 글 감사합니다! 👍

1개의 답글
comment-user-thumbnail
2025년 5월 28일

css도 리렌더링에 영향을 미치고 , 자주사용했던 div ul li ..의 구조 또한 리플로우, 리페인트를 생각해야하는 개선사항이라는 것도 덕분에 알고 갑니다. ! 감사합니다

1개의 답글
comment-user-thumbnail
2025년 5월 31일

항상 스타일은 쉽게 놓칠 수 있는 부분인데, 이렇게 성능에 영향을 줄 수 있을지 몰랐습니다,,, 좋은 정보 알려주셔서 감사합니다!!

1개의 답글
comment-user-thumbnail
2025년 5월 31일

정말로 div ul li span {...}처럼 구조가 깊은 선택자를 사용할 수 밖에 없는 상황이 온다면 어떤식으로 개선을 할 수 있을까요?

1개의 답글
comment-user-thumbnail
2025년 5월 31일

와 정말 생각하지 못한 성능 최적화였습니다!
좋은 정보를 배우고 시간 생기면 다시 복기 해봐야겠네요 감사합니다!

1개의 답글
comment-user-thumbnail
2025년 5월 31일

헉 복잡한 선택자 조심해야겠네요... 🥲🥲🫠🫠

1개의 답글
comment-user-thumbnail
2025년 5월 31일

복잡한 선택자 사용에서 저렇게 동작하는지는 몰랐는데 간과하고 있던 부분을 알게되었습니다. 공유 감사합니다~

1개의 답글
comment-user-thumbnail
2025년 6월 1일

리플로우와 리페인트를 항상 주의하려고 하는데,
대략적으로 알고 있는 부분들이 명확하게 정리되어 있어 좋은 것 같습니다!
보통 리액트로 개발을 하지만, 자바스크립트로 개발을 할 때는 어떻게 진행해야 할지 한 번 고민해보게 되네요
좋은 글 공유 감사합니다!

1개의 답글