PERFORMANCE / Frontend • 2026. 02. 14

브라우저 렌더링 최적화: 60FPS를 사수하는 방법

사용자가 웹사이트에서 '부드럽다'고 느끼는 기준은 60FPS(Frames Per Second)입니다. 1초에 60번 화면이 갱신되어야 한다는 뜻이며, 이는 브라우저가 한 프레임을 그리는 데 주어진 시간이 단 16ms(1000ms / 60프레임)뿐임을 의미합니다. 실제로 브라우저의 오버헤드를 제외하면 개발자가 활용할 수 있는 시간은 약 10ms 내외입니다. 이 짧은 시간 안에 부드러운 애니메이션을 구현하기 위해 적용한 렌더링 최적화 기법을 공유합니다.

1. 중요 렌더링 경로(Critical Rendering Path)의 이해

브라우저가 HTML 문서를 읽어 화면에 픽셀을 뿌리기까지의 과정은 다음과 같은 단계를 거칩니다.

성능 최적화의 핵심은 이 단계 중 가장 비용이 많이 드는 Layout과 Paint 단계를 최대한 건너뛰는 것에 있습니다.

2. 레이아웃 스릴(Layout Thrashing)과 강제 동기 레이아웃

자바스크립트로 DOM을 수정할 때, 브라우저는 보통 스타일 변경을 큐에 쌓아두었다가 나중에 한꺼번에 처리합니다. 하지만 스타일을 변경한 직후 특정 요소를 읽으려고 하면(예: offsetWidth 호출), 브라우저는 정확한 값을 주기 위해 큐를 비우고 즉시 레이아웃을 재계산합니다. 이를 '강제 동기 레이아웃'이라 합니다.

나쁜 예 (Layout Thrashing 발생):
반복문 안에서 DOM의 크기를 읽고 쓰는 행위를 반복하면 매 루프마다 레이아웃 재계산이 일어나 성능이 급격히 저하됩니다.

// ❌ 최적화 전: 읽고 쓰기를 반복 (레이아웃 스릴)
boxes.forEach(box => {
  const containerWidth = document.getElementById("main").offsetWidth; // 매번 읽기(Layout 발생)
  box.style.width = containerWidth + "px"; // 매번 쓰기
});

// ✅ 최적화 후: 읽기를 한 번만 수행
const containerWidth = document.getElementById("main").offsetWidth; // 밖에서 한 번만 읽기
boxes.forEach(box => {
  box.style.width = containerWidth + "px"; // 쓰기만 수행
});
        

3. 하드웨어 가속(GPU) 활용하기

애니메이션을 구현할 때 top, left 속성을 사용하면 브라우저는 매 프레임마다 Layout -> Paint -> Composite 과정을 모두 다시 거쳐야 합니다. 반면 transform이나 opacity 속성을 사용하면 레이아웃과 페인트를 건너뛰고 Composite 단계만 거칩니다.

이를 통해 메인 스레드(Main Thread)의 부담을 줄이고 GPU(Graphics Processing Unit)가 일을 처리하게 함으로써 60FPS를 안정적으로 유지할 수 있습니다.


/* ❌ 성능 저하 유발 */
.box {
  transition: top 0.3s;
  top: 0;
}
.box:hover {
  top: 100px;
}

/* ✅ 하드웨어 가속 활용 */
.box {
  transition: transform 0.3s;
  transform: translateY(0);
  will-change: transform; /* 브라우저에게 미리 레이어 분리 힌트 제공 */
}
.box:hover {
  transform: translateY(100px);
}
        

4. will-change와 레이어 관리

will-change 속성은 브라우저에게 "이 요소는 곧 변경될 것이니 미리 최적화해둬"라고 알려주는 신호입니다. 이를 사용하면 해당 요소를 별도의 컴포지팅 레이어(Compositing Layer)로 분리합니다. 하지만 너무 많은 요소에 남용하면 메모리 점유율이 급증하여 오히려 기기가 느려질 수 있으므로, 애니메이션이 일어나는 시점에만 적용하고 끝난 뒤에는 제거하는 전략이 좋습니다.

5. requestAnimationFrame(rAF) 활용

setTimeout이나 setInterval은 브라우저의 렌더링 주기와 상관없이 실행되므로 프레임 드랍(Jank)을 유발할 수 있습니다. requestAnimationFrame은 브라우저가 화면을 갱신하기 직전에 콜백을 실행하도록 보장하므로 가장 부드러운 애니메이션을 보장합니다.


function animate() {
  // 애니메이션 로직
  requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
        

마치며: 체감 성능이 곧 사용자 경험이다

코드의 비즈니스 로직이 아무리 완벽해도, 화면이 뚝뚝 끊긴다면 사용자는 서비스의 품질을 의심하게 됩니다. 60FPS를 사수하는 것은 단순한 기술적 과제가 아니라 사용자에게 신뢰를 주는 과정입니다.

Chrome 개발자 도구의 Performance 탭과 Rendering 탭을 활용해 보세요. 'Paint Flashing'을 켜서 불필요한 영역이 다시 그려지고 있지는 않은지 확인하는 습관이 여러분의 웹 앱을 더욱 견고하게 만들어줄 것입니다.