브라우저 메인 스레드는 값비싼 자원이다
프론트엔드 성능 최적화의 진짜 병목은 네트워크나 번들 크기가 아니라 메인 스레드일 수 있다는 내용의 글입니다. 메인 스레드에서는 JavaScript 실행과 화면 그리기(스타일 계산, 레이아웃, 페인트)가 모두 처리되기 때문에, 이벤트 핸들러부터 렌더링까지 몰려 있는 상황에서는 화면이 버벅이는 잰크(jank)가 발생합니다. 상호작용이 많은 화면을 다룰 때 메인 스레드를 어떻게 관리할지가 핵심 과제가 됩니다.
'프론트엔드 최적화'라고 하면 무엇이 떠오르는가? 대부분의 사람들에게는 네트워크 요청 줄이기, 번들 크기 축소, 캐시 활용 같은 것들일 것이다. 그 외에도 리렌더링을 줄이거나 리소스 로딩 시점을 조정하는 정도일 것이다. 메인 스레드는 보통 논의 대상에 오르지 않으며, 그럴 만한 이유가 있다. 대부분의 화면에서는 메인 스레드가 문제가 되지 않기 때문이다. 하지만 상호작용이 많은 화면, 즉 데이터가 실시간으로 스트리밍되고 스크롤, 애니메이션, 입력이 서로 얽혀 있는 화면에서는 상황이 달라진다. 네트워크와 번들 크기를 아무리 절약해도, 메인 스레드가 막히는 순간 화면은 멈춰버린다.
아마 한 번쯤 스크롤이 가끔 버벅이거나, 버튼이 살짝 늦게 반응하거나, 검색창에 입력한 글자가 반 박자 늦게 나타나는 웹사이트를 경험해 봤을 것이다. 짜증 날 정도까지는 아니지만 어딘가 신경이 쓰이는 그런 느낌. 그런 잰크(jank)가 바로 막힌 메인 스레드의 모습이다. 개발자로서 이런 잰크를 만나면 보통 '내 코드가 느린 건가?'라며 알고리즘을 뜯어보거나 낭비되는 연산을 찾기 시작한다. 하지만 대부분의 경우 코드의 속도는 문제가 아니다. 코드가 느린 게 아니라, 그저 그 코드가 메인 스레드를 붙잡고 있을 뿐이다.
브라우저에는 여러 스레드가 있지만, 코드에서 접근할 수 있는 거의 모든 것이 메인 스레드에 집중되어 있다. 연산, 렌더링, 이벤트 처리, 네트워크 응답 처리, 그리고 프레임워크 내부 동작까지 모두 여기서 처리된다. 자원은 하나인데 해야 할 일은 산더미다. 브라우저의 메인 스레드는 값비싼 자원이다. 평소에는 문제를 일으키지 않지만, 야심찬 작업을 시도하는 순간 메인 스레드를 다루는 일이 가장 중요해진다. 이 글은 그 값비싼 자원을 어떻게 다룰 것인가에 관한 이야기다.
메인 스레드는 무엇을 하는가?
먼저 메인 스레드가 실제로 하는 일부터 살펴보자. 그 작업은 크게 두 가지 범주로 나뉜다.
첫 번째는 JavaScript 실행이다. 우리가 작성한 코드와 이벤트 핸들러, 타이머, 네트워크 응답 콜백, 프레임워크 내부 코드가 모두 여기서 실행된다. 이 태스크들은 화면 새로고침 주기와는 무관하게, 큐에 들어온 순서대로 틈이 날 때마다 실행된다.
두 번째는 화면 그리기다. DOM이나 스타일이 변경되어 화면을 업데이트해야 할 때, 브라우저는 대략 다음 순서의 단계를 거쳐 한 프레임을 만들어낸다.
- requestAnimationFrame 콜백 실행 - 프레임이 그려지기 직전에 실행되도록 등록된 JavaScript
- 스타일 계산 - 각 요소의 최종 CSS 값 계산
- 레이아웃(Layout) - 각 요소의 위치와 크기 계산 (reflow라고도 함)
- 페인트(Paint) - 무엇을 어떤 색으로 그릴지 설명하는 그리기 명령 생성
변경된 것이 없으면 이 단계들은 전부 건너뛰므로, 매 프레임마다 반드시 실행되는 것은 아니다. 최종 결과물을 받아 화면에 조립하는 마지막 합성(compositing) 단계만이 컴포지터 스레드(compositor thread)로 넘겨진다¹. 다시 말해, 화면을 그리는 파이프라인의 전반부 대부분이 메인 스레드의 책임인 것이다.