![[실전 성능 09] 화면 그리기를 막는 것들](https://img.thenullpage.com/posts/5763/5763_1_69f817.webp)
지난 편까지 이미지와 폰트 같은 콘텐츠 자원을 다뤘다. 이번 편은 층위를 바꿔서, 콘텐츠를 그리는 과정 자체를 막아 세우는 자원을 다룬다. 브라우저가 화면을 그리려는데 스타일이나 스크립트가 앞을 가로막으면 아무것도 못 그리고 기다렸다. 이 렌더링 블로킹을 걷어내는 것이 로딩을 앞당기는 큰 지렛대였다.
1. 브라우저가 그림을 그리는 순서
브라우저가 화면을 그리는 과정을 이해하니 문제가 보였다. 브라우저는 문서를 받아 위에서 아래로 읽어 내려가며 화면을 구성하는데, 중간에 스타일 파일이나 스크립트를 만나면 그것을 처리하느라 잠시 멈췄다. 특히 문서 머리에 있는 자원들이 이 멈춤을 자주 일으켰다. 그 자원이 처리될 때까지 브라우저는 아직 아무 화면도 그리지 못했다.
스타일 파일이 렌더링을 막는 이유는 명확했다. 브라우저는 요소가 어떤 모습일지 모르면 그릴 수 없는데, 그 모습을 정하는 게 스타일이었다. 그래서 스타일이 다 도착하고 해석될 때까지 그리기를 미뤘다. 스타일이 무겁거나 여러 개면 그만큼 첫 그림이 늦어졌다. 이건 브라우저가 잘못 그리는 걸 막기 위한 합리적인 동작이었지만, 성능 관점에서는 병목이었다.
스크립트는 또 다른 방식으로 막았다. 브라우저는 스크립트가 문서를 바꿀 수도 있다고 보고, 스크립트를 만나면 문서 읽기를 멈추고 그것을 먼저 실행했다. 실행이 끝나야 다시 문서를 읽어 나갔다. 그래서 머리에 있는 큰 스크립트 하나가 그 아래 모든 콘텐츠의 등장을 지연시켰다. 스크립트가 파서를 막는 이 동작이 로딩을 크게 늦추는 원인이었다.
이 순서를 이해하고 나니 전략이 분명해졌다. 첫 화면을 그리는 데 꼭 필요한 것만 앞에 두고, 나머지는 그리기를 막지 않도록 뒤로 미루거나 실행을 지연시키는 것이었다. 브라우저가 일단 그림부터 그리게 하고, 부수적인 것들은 그림이 뜬 다음에 처리하도록 순서를 조정하는 게 핵심이었다. 사용자가 원하는 건 완벽하게 완성된 화면을 오래 기다리는 것이 아니라 뭐라도 빨리 보고 시작하는 것이었으니, 완성도보다 시작 시점을 앞당기는 쪽이 대개 옳았다.
2. 첫 화면에 필요한 것만 남기기
가장 먼저 한 일은 첫 화면을 그리는 데 정말 필요한 스타일이 무엇인지 가려내는 것이었다. 사이트 전체의 스타일을 다 담은 큰 파일을 머리에서 통째로 불러오면, 첫 화면과 무관한 스타일까지 다 처리되기를 기다려야 했다. 나는 첫 화면에 보이는 부분의 스타일만 추려서 문서 안에 직접 넣었다. 그러면 별도 다운로드 없이 즉시 그릴 수 있었다.
나머지 스타일은 그리기를 막지 않는 방식으로 나중에 불러왔다. 첫 화면이 뜬 다음 필요해지는 스타일들은 뒤늦게 도착해도 괜찮았다. 이렇게 스타일을 중요한 것과 나중 것으로 나누니, 브라우저가 첫 그림을 훨씬 빨리 그렸다. 당장 보이는 것만 우선 처리하고 나머지는 미룬다는 원칙이 여기서도 통했다.
이 작업의 어려움은 무엇이 첫 화면에 필요한지 정확히 가려내는 데 있었다. 너무 적게 넣으면 첫 화면이 스타일 없이 떴다가 뒤늦게 꾸며지며 흔들렸고, 너무 많이 넣으면 애초에 미루는 의미가 없었다. 나는 실제 첫 화면을 확인하며 그 경계를 조율했다. 로딩과 안정성이 부딪히는 지점이라 신중하게 다뤘다.
스타일 자체를 줄이는 것도 병행했다. 안 쓰는 스타일이 쌓여 있으면 처리할 양이 늘어 그리기가 늦어졌다. 나는 주기적으로 사용하지 않는 스타일을 걷어내서 파일을 가볍게 유지했다. 코드가 자라면서 죽은 스타일이 늘어나기 마련이라, 이 정리는 한 번으로 끝나지 않고 계속 이어가야 했다. 어떤 스타일이 아직 쓰이는지 자동으로 판별하기가 은근히 까다로워서, 나는 실제 화면을 확인해가며 조심스럽게 걷어냈고 확신이 없으면 남겨두는 쪽을 택했다.
3. 스크립트 실행 미루기
스크립트는 특히 조심해서 다뤘다. 머리에 있는 동기 스크립트는 그 아래 모든 콘텐츠의 등장을 막으니, 첫 화면과 무관한 스크립트는 반드시 뒤로 미뤘다. 브라우저에게 이 스크립트는 급하지 않으니 문서를 다 읽은 다음 실행하라고 표시해두면, 그리기를 막지 않고 콘텐츠가 먼저 떴다. 이 표시 하나로 로딩이 크게 당겨졌다.
스크립트를 미루는 방식에도 종류가 있었다. 어떤 스크립트는 문서 읽기와 나란히 다운로드하되 실행은 문서를 다 읽은 뒤로 미룰 수 있었고, 어떤 스크립트는 순서와 무관하게 준비되는 대로 실행해도 괜찮았다. 나는 각 스크립트의 성격에 맞게 미루는 방식을 골랐다. 서로 순서에 의존하는 스크립트는 순서를 지키게, 독립적인 것은 자유롭게 두었다.
당장 필요 없는 스크립트는 아예 나중에 불러오기도 했다. 사용자가 특정 기능을 쓸 때만 필요한 코드라면, 처음부터 다 받을 게 아니라 그 기능을 쓰려는 순간에 불러오면 됐다. 이렇게 하니 초기에 처리할 스크립트 양 자체가 줄었다. 첫 화면에 안 쓰는 코드는 첫 화면 로딩에 부담을 주지 않아야 한다는 생각이었다.
스크립트 미루기는 반응성에도 도움이 됐다. 앞서 반응성 편에서 다뤘듯, 초기에 스크립트가 몰리면 브라우저의 처리 통로가 막혀 사용자 입력에 반응이 늦었다. 스크립트 실행을 분산시키니 그 통로에 여유가 생겨 반응성도 함께 좋아졌다. 하나의 조치가 로딩과 반응성 두 지표를 동시에 챙긴 셈이었다.
4. 머리를 신성하게 지키기
이런 작업을 하며 나는 문서의 머리 부분을 신성한 영역으로 여기게 됐다. 머리에 들어가는 모든 자원은 첫 그림을 늦추는 세금이라고 생각하니, 무언가를 머리에 넣을 때마다 그게 정말 그럴 자격이 있는지 따지게 됐다. 무심코 추가한 스크립트 하나가 사이트 전체의 첫 화면을 늦출 수 있었다.
실제로 성능이 나빠지는 흔한 경로가 머리에 자원이 하나둘 쌓이는 것이었다. 분석 도구, 각종 위젯, 편의 기능이 하나씩 추가될 때마다 머리가 무거워졌고, 어느 순간 첫 화면이 눈에 띄게 늦어졌다. 각각은 작아 보여도 합쳐지면 큰 부담이었다. 나는 머리에 추가되는 것을 감시하고, 미룰 수 있는 것은 반드시 미뤘다.
제삼자가 제공하는 스크립트는 특히 경계했다. 내가 통제하지 못하는 외부 스크립트가 머리에서 그리기를 막으면, 내 사이트 성능이 남의 서버 속도에 좌우됐다. 나는 이런 외부 자원을 최대한 그리기를 막지 않는 방식으로 넣고, 그 서버가 느려도 내 첫 화면은 뜨도록 격리했다. 남의 문제로 내 사이트가 멈추지 않게 하는 것이 중요했다.
머리를 지키는 습관은 사이트가 커질수록 값졌다. 기능이 늘어나도 머리를 가볍게 유지하는 원칙이 있으니 첫 화면 속도가 잘 무너지지 않았다. 새 기능을 추가할 때마다 이게 머리에 들어가야 하는지 먼저 물었고, 대부분은 그럴 필요가 없었다. 이 질문 하나가 성능을 지키는 문지기 역할을 했다. 성능은 한 번 좋게 만들었다고 유지되는 게 아니라 방심하면 슬금슬금 나빠지는 것이라, 이런 습관적인 문지기가 없으면 시간이 지날수록 첫 화면이 다시 무거워졌다.
5. 걷어낸 만큼 빨라진다
렌더링 블로킹을 걷어내는 작업은 효과가 직접적이었다. 그리기를 막던 것을 치우니 첫 화면이 그만큼 빨리 떴다. 다른 최적화가 조금씩 개선하는 것과 달리, 블로킹 제거는 종종 초 단위로 로딩을 당겼다. 병목이 명확한 만큼 해소했을 때의 보상도 컸다. 나는 이 작업을 로딩 최적화의 우선순위 상단에 뒀다.
다만 걷어내기만 하면 되는 건 아니었다. 첫 화면에 필요한 것까지 미루면 오히려 화면이 늦게 완성되거나 흔들렸다. 핵심은 필요한 것과 아닌 것을 정확히 가르는 판단이었고, 그 판단은 실제 화면을 보고 측정하며 다듬을 수밖에 없었다. 나는 미루기 전후를 재서 정말 이득인지 확인하고 나서야 확정했다.
이 작업을 하며 성능이 결국 우선순위의 문제라는 걸 다시 느꼈다. 자원을 없앨 수는 없어도, 무엇을 먼저 처리하고 무엇을 나중으로 미룰지 정하는 것만으로 사용자 경험이 크게 달라졌다. 같은 자원이라도 순서를 바꾸니 결과가 달랐다. 성능 최적화의 상당 부분이 이 순서 조정이었다.
그런데 걷어내다 보니 근본적인 의문이 생겼다. 애초에 처리할 스크립트 양 자체가 너무 많은 게 아닐까 하는 것이었다. 미루는 것도 한계가 있고, 결국 코드의 총량을 줄여야 했다. 다음 편에서는 그 총량, 즉 번들 크기와 이를 잘게 나누는 코드 분할을 다루겠다. 미루기의 다음 단계인 셈이다.