올림픽 통계 서비스 최적화
- 2.1 애니메이션 최적화
- 2.2 컴포넌트 지연 로딩
- 2.3 컴포넌트 사전 로딩
- 2.4 이미지 사전 로딩
애니메이션이 버벅거리는 이유를 알려면, 브라우저가 화면을 어떻게 그리는지부터 이해해야 해.
브라우저 렌더링 파이프라인은 DOM -> CSSOM -> Render Tree -> Layout -> Paint -> Composite 순서로 돌아가. 여기서 CSS 속성을 바꾸면 어떤 단계부터 다시 실행되느냐에 따라 비용이 달라지거든. **리플로우(Reflow)**는 width, height, margin 같은 레이아웃 속성을 바꿀 때 발생해서 Layout 단계부터 전부 다시 실행되니까 가장 비싸. **리페인트(Repaint)**는 color, background 같은 시각적 속성만 바꿀 때 Paint부터 다시 실행되고. GPU 가속은 transform, opacity만 바꿀 때 Composite 단계만 실행돼서 가장 빨라.
애니메이션에 width를 쓰면 매 프레임마다 리플로우가 발생해. 60fps를 유지하려면 한 프레임당 16.67ms 안에 다 끝내야 하는데, 리플로우가 끼면 이 시간을 초과하기 쉽지. 그래서 width 대신 transform: scaleX()를 쓰는 거야. GPU가 처리하는 Composite 단계만 실행되니까 메인 스레드에 부담이 없거든.
/* 느림 - 매 프레임마다 리플로우 */
.bar { width: 0; transition: width 1s; }
.bar.active { width: 80%; }
/* 빠름 - GPU 가속, Composite만 실행 */
.bar { transform: scaleX(0); transition: transform 1s; }
.bar.active { transform: scaleX(0.8); }
will-change: transform 속성을 미리 선언해두면 브라우저가 해당 요소를 별도 레이어로 분리해서 합성 단계에서만 처리하도록 준비해. 다만 남발하면 메모리 낭비니까 실제 애니메이션이 일어나는 요소에만 써야 해.
1장에서도 코드 분할과 지연 로딩을 다뤘는데, 여기서는 좀 더 실전적인 상황이야. 모달 컴포넌트를 React.lazy()로 분리하면 초기 번들 사이즈가 줄어들지만, 사용자가 버튼을 눌렀는데 모달이 바로 안 열리고 로딩 스피너가 잠깐 보이는 문제가 생겨. 초기 로딩은 빨라졌는데 사용자 경험이 나빠진 거지.
이걸 해결하는 게 사전 로딩이야. 두 가지 전략이 있거든. 첫째, 사용자가 버튼 위에 마우스를 올리면(mouseenter 이벤트) 그 시점에 컴포넌트를 미리 로드하는 방법. 마우스를 올리고 클릭하기까지 보통 수백 ms의 시간이 있으니까 그 틈에 청크를 받아오는 거야. 둘째, 페이지의 핵심 콘텐츠가 다 로드된 뒤에 useEffect에서 나머지 컴포넌트를 백그라운드로 미리 받아오는 방식이지.
useEffect(() => {
import('./components/Modal');
}, []);
이미지도 마찬가지야. 모달 안에 큰 이미지가 있다면 JavaScript Image 객체로 사전 로딩할 수 있어. new Image()로 이미지 객체를 생성하고 src를 지정하면, 브라우저가 백그라운드에서 다운로드해서 캐시에 저장하거든. 나중에 실제로 같은 URL을 사용할 때 캐시에서 바로 가져와. 다만 너무 많은 이미지를 사전 로딩하면 현재 페이지에 필요한 리소스의 네트워크 대역폭을 뺏을 수 있으니까, 정말 필요한 이미지만 선별해야 해.
두 전략 다 핵심은 같아 — 사용자가 필요로 하기 직전에 미리 준비해두는 것. 타이밍 게임이야.
정리
2장 읽고 기억할 거 세 가지:
- 애니메이션은 transform과 opacity로. 리플로우를 피하고 GPU에 맡기면 60fps 유지가 가능해진다
- 지연 로딩과 사전 로딩은 세트. 지연 로딩만 하면 사용자 경험이 나빠질 수 있다. 타이밍을 제어해서 미리 준비해둬야 함
- 브라우저 렌더링 파이프라인을 이해해야 진짜 최적화가 된다. Layout -> Paint -> Composite, 이 세 단계를 모르면 감으로 최적화하게 된다