브라우저 미리 로드 스캐너가 무엇인지, 성능에 어떤 도움이 되는지, 그리고 이러한 스캐너를 차단하는 방법을 알아보세요.
페이지 속도 최적화에서 간과되는 한 가지 측면은 브라우저 내부에 관한 약간의 지식입니다. 브라우저는 특정 최적화를 통해 개발자가 할 수 없는 방식으로 성능을 향상하지만, 단, 이러한 최적화가 의도치 않게 방해되지 않아야 합니다.
이해해야 할 내부 브라우저 최적화 중 하나는 브라우저 미리 로드 스캐너입니다. 이 게시물에서는 미리 로드 스캐너의 작동 방식과 더 중요한 사항으로 방해가 되지 않도록 예방하는 방법을 다룹니다.
미리 로드 스캐너란 무엇인가요?
모든 브라우저에는 원시 마크업을 토큰화하여 객체 모델로 처리하는 기본 HTML 파서가 있습니다. 이 모든 과정은 파서가 <link>
요소로 로드된 스타일시트나 async
또는 defer
속성 없이 <script>
요소로 로드된 스크립트와 같은 차단 리소스를 찾으면 일시중지할 때까지 순조롭게 진행됩니다.
CSS 파일의 경우 스타일이 적용되기 전에 페이지의 스타일이 적용되지 않은 버전이 잠시 표시될 수 있는 스타일 지정이 없는 콘텐츠 플래시(FOUC)를 방지하기 위해 렌더링이 차단됩니다.
또한 브라우저에서 defer
또는 async
속성이 없는 <script>
요소를 발견하면 페이지의 파싱 및 렌더링을 차단합니다.
그 이유는 기본 HTML 파서가 작업을 수행하는 동안 특정 스크립트가 DOM을 수정하는지 여부를 브라우저가 확실하게 알 수 없기 때문입니다. 이것이 파싱 및 렌더링 차단의 효과가 한계가 되도록 문서의 끝부분에서 JavaScript를 로드하는 것이 일반적입니다.
브라우저가 파싱과 렌더링을 모두 차단해야 하는 좋은 이유입니다. 하지만 이러한 중요한 단계 중 하나를 차단하면 다른 중요한 리소스의 검색이 지연되어 프로그램이 지연될 수 있으므로 바람직하지 않습니다. 다행히 브라우저는 미리 로드 스캐너라는 보조 HTML 파서를 통해 이러한 문제를 완화하기 위해 최선을 다합니다.
미리 로드 스캐너의 역할은 추측입니다. 즉, 기본 HTML 파서가 리소스를 발견하기 전에 먼저 가져올 리소스를 찾기 위해 원시 마크업을 검사합니다.
미리 로드 스캐너가 작동 중인지 확인하는 방법
미리 로드 스캐너가 존재하는 이유는 렌더링과 파싱이 차단되었기 때문입니다. 이러한 두 가지 성능 문제가 발생한 적이 없다면 미리 로드 스캐너가 그다지 유용하지 않을 것입니다. 웹페이지가 미리 로드 스캐너를 활용하는지 여부를 판단하기 위한 핵심은 이러한 차단 현상에 따라 다릅니다. 이렇게 하려면 미리 로드 스캐너가 작동하는 위치를 알아내기 위한 요청에 인위적인 지연을 도입하면 됩니다.
스타일시트가 포함된 기본 텍스트와 이미지가 있는 이 페이지를 예로 들 수 있습니다. CSS 파일은 렌더링과 파싱을 모두 차단하므로 프록시 서비스를 통해 스타일시트에 인위적인 2초 지연을 발생시킵니다. 이러한 지연으로 인해 미리 로드 스캐너가 작동하는 네트워크 워터폴에서 더 쉽게 확인할 수 있습니다.
폭포식 차트에서 볼 수 있듯이 사전 로드 스캐너는 렌더링 및 문서 파싱이 차단된 경우에도 <img>
요소를 감지합니다. 이 최적화가 없으면 브라우저가 차단 기간에 기회주의적으로 정보를 가져올 수 없으며, 더 많은 리소스 요청이 동시가 아니라 연속적이 됩니다.
이 장난감 예시를 살펴보았으니, 이제 미리 로드 스캐너를 무력화할 수 있는 실제 패턴과 이 문제를 해결하기 위해 어떤 조치를 취할 수 있는지 살펴보겠습니다.
삽입된 async
스크립트
<head>
에 다음과 같은 인라인 JavaScript가 포함된 HTML이 있다고 가정해 보겠습니다.
<script>
const scriptEl = document.createElement('script');
scriptEl.src = '/yall.min.js';
document.head.appendChild(scriptEl);
</script>
삽입된 스크립트는 기본적으로 async
이므로 이 스크립트가 삽입되면 마치 async
속성이 적용된 것처럼 동작합니다. 즉, 최대한 빨리 실행되고 렌더링을 차단하지 않습니다. 최적화된 것 같죠? 그러나 이 인라인 <script>
가 외부 CSS 파일을 로드하는 <link>
요소 뒤에 온다고 가정하면 최적화되지 않은 결과를 얻게 됩니다.
여기서 일어난 일을 분석해 보겠습니다.
- 0초가 되면 기본 문서가 요청됩니다.
- 1.4초에 탐색 요청의 첫 번째 바이트가 도착합니다.
- 2.0초에 CSS 및 이미지가 요청됩니다.
- 파서가 스타일시트를 로드하지 못하도록 차단되고
async
스크립트를 삽입하는 인라인 JavaScript가 2.6초 후에 이 스타일시트 후에 오기 때문에 스크립트에서 제공하는 기능은 가능한 한 빨리 사용할 수 없습니다.
이는 스타일시트 다운로드가 완료된 후에만 스크립트 요청이 발생하므로 최적화되지 않습니다. 이렇게 하면 스크립트가 최대한 빨리 실행되지 않습니다. 반면 <img>
요소는 서버에서 제공하는 마크업에서 검색할 수 있으므로 미리 로드 스캐너에 의해 발견됩니다.
그렇다면 스크립트를 DOM에 삽입하는 대신 async
속성과 함께 일반 <script>
태그를 사용하면 어떻게 될까요?
<script src="/yall.min.js" async></script>
결과는 다음과 같습니다.
rel=preload
을(를) 사용하면 이러한 문제를 해결할 수 있다는 유혹이 들 수도 있습니다. 이 방법은 확실히 효과가 있지만 부작용이 있을 수 있습니다. 어쨌든 <script>
요소를 DOM에 삽입하지 않음으로써 피할 수 있는 문제를 해결하기 위해 rel=preload
를 사용하는 이유는 무엇일까요?
'수정사항' 미리 로드 새로운 문제가 발생합니다. 첫 두 데모의 async
스크립트는 <head>
에 로드되어 있지만 이 스크립트도 'Low'(낮음)에 로드됩니다. 스타일시트는 '가장 높음'으로 로드됩니다. 우선순위가 있습니다. async
스크립트가 미리 로드된 마지막 데모에서 스타일시트는 여전히 'Highest'(최고)로 로드됩니다. 스크립트의 우선순위가 '높음'으로 승급되었습니다.
리소스의 우선순위가 높아지면 브라우저는 리소스에 더 많은 대역폭을 할당합니다. 즉, 스타일시트의 우선순위가 가장 높더라도 스크립트의 우선순위가 높아져 대역폭 경합이 발생할 수 있습니다. 이는 연결이 느리거나 리소스가 상당히 큰 경우에 영향을 미치는 요인일 수 있습니다.
대답은 간단합니다. 시작 시 스크립트가 필요한 경우 미리 로드 스캐너를 DOM에 삽입하여 무력화해서는 안 됩니다. 필요에 따라 <script>
요소 배치뿐만 아니라 defer
및 async
와 같은 속성으로 실험합니다.
JavaScript를 사용한 지연 로드
지연 로드는 데이터를 절약하는 좋은 방법이며 이미지에 자주 적용됩니다. 하지만 '스크롤 없이 볼 수 있는 부분'에 있는 이미지에 지연 로드가 잘못 적용되는 경우도 있습니다.
이로 인해 미리 로드 스캐너와 관련된 리소스 검색 가능성에 문제가 발생할 수 있으며, 이미지에 대한 참조를 탐색, 다운로드, 디코딩, 표시하는 데 걸리는 시간이 불필요하게 지연될 수 있습니다. 다음 이미지 마크업을 예로 들겠습니다.
<img data-src="/sand-wasp.jpg" alt="Sand Wasp" width="384" height="255">
data-
접두사를 사용하는 것은 JavaScript 기반 지연 로더에서 일반적인 패턴입니다. 이미지가 뷰포트로 스크롤되면 지연 로더가 data-
접두사를 제거합니다. 즉, 위 예시에서 data-src
는 src
이 됩니다. 이 업데이트는 브라우저에 리소스를 가져오도록 메시지를 표시합니다.
이 패턴은 시작 시 표시 영역에 있는 이미지에 적용하기 전에는 문제가 되지 않습니다. 미리 로드 스캐너는 src
(또는 srcset
) 속성과 같은 방식으로 data-src
속성을 읽지 않으므로 이미지 참조는 이전에 검색되지 않습니다. 더 안 좋은 점은 지연 로더 JavaScript가 다운로드, 컴파일, 실행한 이후까지 이미지 로드가 지연된다는 점입니다.
표시 영역의 크기에 따라 이미지 크기에 따라 최대 콘텐츠 페인트 (LCP)의 후보 요소가 될 수 있습니다. 미리 로드 스캐너가 이미지 리소스를 미리 추측할 수 없는 경우(페이지의 스타일시트가 렌더링을 차단하는 시점에) LCP가 발생합니다.
해결 방법은 이미지 마크업을 변경하는 것입니다.
<img src="/sand-wasp.jpg" alt="Sand Wasp" width="384" height="255">
이는 시작 시 표시 영역에 있는 이미지에 가장 적합한 패턴입니다. 미리 로드 스캐너가 이미지 리소스를 더 빠르게 검색하고 가져오기 때문입니다.
이 단순화된 예의 결과는 느린 연결에서 LCP를 100밀리초 개선한 것입니다. 이는 큰 개선이 되지 않는 것처럼 보일 수 있지만, 솔루션이 빠른 마크업 수정이며 대부분의 웹페이지가 이 예제 세트보다 더 복잡하다고 생각할 때입니다. 즉, LCP 후보는 다른 많은 리소스와 대역폭을 두고 경쟁해야 할 수 있으므로 이와 같은 최적화가 점점 더 중요해지고 있습니다.
CSS 배경 이미지
브라우저 미리 로드 스캐너는 마크업을 스캔합니다. background-image
속성에서 참조하는 이미지 가져오기가 포함될 수 있는 CSS와 같은 다른 리소스 유형은 스캔하지 않습니다.
HTML과 마찬가지로 브라우저는 CSS를 CSSOM이라는 자체 객체 모델로 처리합니다. CSSOM이 구성될 때 외부 리소스가 검색되면 이러한 리소스는 미리 로드 스캐너가 아니라 검색 시점에 요청됩니다.
페이지의 LCP 후보가 CSS background-image
속성이 있는 요소라고 가정해 보겠습니다. 다음은 리소스가 로드될 때 발생하는 상황입니다.
이 경우 미리 로드 스캐너가 패배한 것이 아니라 관련이 없는 것입니다. 그렇더라도 페이지의 LCP 후보가 background-image
CSS 속성에서 가져온 것이라면 해당 이미지를 미리 로드하는 것이 좋습니다.
<!-- Make sure this is in the <head> below any
stylesheets, so as not to block them from loading -->
<link rel="preload" as="image" href="lcp-image.jpg">
이 rel=preload
힌트는 작지만 브라우저가 이미지를 더 빨리 발견하는 데 도움이 됩니다.
rel=preload
힌트를 사용하면 LCP 후보가 더 빨리 발견되어 LCP 시간이 줄어듭니다. 힌트가 이 문제를 해결하는 데 도움이 되지만 이미지 LCP 후보가 CSS에서 로드되어야 하는지 여부를 평가하는 것이 더 나을 수 있습니다. <img>
태그를 사용하면 미리 로드 스캐너가 이미지를 검색하도록 허용하면서 표시 영역에 적절한 이미지를 로드하는 것을 더 세밀하게 제어할 수 있습니다.
너무 많은 리소스 인라인 처리
인라인 처리는 리소스를 HTML 내부에 배치하는 방법입니다. base64 인코딩을 사용하여 <style>
요소의 스타일시트, <script>
요소의 스크립트, 기타 거의 모든 리소스를 인라인으로 삽입할 수 있습니다.
리소스에 대해 별도의 요청이 발행되지 않으므로 리소스를 인라인 처리하는 것이 다운로드 속도가 빠를 수 있습니다. 문서에 바로 삽입되어 바로 로드됩니다. 하지만 다음과 같은 중요한 단점이 있습니다.
- HTML을 캐시하지 않고 HTML 응답이 동적인 경우에만 캐시할 수 없는 경우 인라인 리소스가 캐시되지 않습니다. 인라인 리소스는 재사용할 수 없으므로 성능에 영향을 미칩니다.
- HTML을 캐시할 수 있더라도 인라인 리소스는 문서 간에 공유되지 않습니다. 이 경우 전체 원본에서 캐시하고 재사용할 수 있는 외부 파일에 비해 캐싱 효율성이 떨어집니다.
- 너무 많이 인라인으로 삽입하면 미리 로드 스캐너가 문서의 후반부에서 리소스를 검색하는 것을 지연시킬 수 있습니다. 이는 추가 인라인 콘텐츠를 다운로드하는 데 시간이 더 오래 걸리기 때문입니다.
이 페이지를 예로 들어보겠습니다. 특정 조건에서 LCP 후보는 페이지 상단의 이미지이며 CSS는 <link>
요소에 의해 로드된 별도의 파일에 있습니다. 또한 이 페이지는 CSS 리소스에서 별도의 파일로 요청되는 4개의 웹 글꼴을 사용합니다.
이제 CSS 및 모든 글꼴이 base64 리소스로 인라인되면 어떻게 될까요?
이 예시에서 인라인 처리의 영향은 LCP와 일반적인 성능에 부정적인 영향을 미칩니다. 인라인이 없는 페이지의 버전은 약 3.5초 동안 LCP 이미지를 그립니다. 모든 항목을 인라인으로 표시하는 페이지는 단 7초가 될 때까지 LCP 이미지를 그리지 않습니다.
여기에는 미리 로드 스캐너 외에도 여러 요소가 작용합니다. base64는 바이너리 리소스에 비효율적인 형식이므로 글꼴 삽입은 좋은 전략이 아닙니다. 또 다른 요소는 CSSOM에서 필요로 판단하지 않는 한 외부 글꼴 리소스가 다운로드되지 않는다는 점입니다. 이러한 글꼴이 base64로 인라인되면 현재 페이지에 필요한지 여부와 관계없이 다운로드됩니다.
미리 로드를 통해 개선할 수 있을까요? 물론입니다. LCP 이미지를 미리 로드하고 LCP 시간을 줄일 수는 있지만, 인라인 리소스로 캐시할 수 없는 HTML을 부풀리면 다른 부정적인 성능 결과가 발생합니다. 콘텐츠가 포함된 첫 페인트 (FCP)도 이 패턴의 영향을 받습니다. 인라인 처리된 내용이 없는 페이지 버전에서는 FCP가 약 2.7초입니다. 모든 것이 인라인 처리된 버전에서는 FCP가 약 5.8초입니다.
HTML, 특히 base64로 인코딩된 리소스에 인라인할 때는 매우 주의해야 합니다. 일반적으로 리소스가 매우 작은 경우를 제외하고는 권장되지 않습니다. 인라인을 너무 많이 삽입하면 문제가 발생할 수 있으므로 인라인은 가능한 한 적게 사용합니다.
클라이언트 측 JavaScript로 마크업 렌더링
JavaScript는 페이지 속도에 큰 영향을 미칩니다. 개발자들은 상호작용을 제공하기 위해 Gemini를 사용할 뿐만 아니라, 콘텐츠 자체를 제공하기 위해 Gemini를 사용하는 경향이 있습니다. 이렇게 하면 어떤 면에서 개발자 환경이 개선됩니다. 그러나 개발자가 얻는 이점이 항상 사용자의 이점으로 이어지지는 않습니다.
미리 로드 스캐너를 무력화할 수 있는 패턴은 클라이언트 측 JavaScript로 마크업을 렌더링하는 것입니다.
마크업 페이로드가 포함되고 브라우저에서 JavaScript에 의해 완전히 렌더링되는 경우, 해당 마크업의 모든 리소스는 사실상 미리 로드 스캐너에 표시되지 않습니다. 이로 인해 중요한 리소스의 탐색이 지연되어 LCP에 확실히 영향을 미칩니다. 이러한 예의 경우 JavaScript를 표시할 필요가 없는 동등한 서버 렌더링 환경에 비해 LCP 이미지 요청이 상당히 지연됩니다.
이 내용은 이 도움말의 주제와는 약간 다르지만 클라이언트에서 마크업을 렌더링하면 미리 로드 스캐너를 우회하는 것 이상의 효과가 있습니다. 우선, 필요하지 않은 환경을 구현하기 위해 JavaScript를 도입하면 불필요한 처리 시간이 발생하여 다음 페인트에 대한 상호작용 (INP)에 영향을 미칠 수 있습니다. 클라이언트에서 극도로 많은 양의 마크업을 렌더링하면 서버에서 전송하는 동일한 양의 마크업을 생성할 때보다 긴 작업이 생성될 가능성이 높습니다. 이는 JavaScript와 관련된 추가 처리 외에도 브라우저가 서버에서 마크업을 스트리밍하고 긴 작업을 제한하는 경향이 있는 방식으로 렌더링을 청크로 나누기 때문입니다. 반면 클라이언트에서 렌더링한 마크업은 단일 모놀리식 작업으로 처리되며 이는 페이지의 INP에 영향을 줄 수 있습니다.
이 시나리오에 대한 해결 방법은 다음 질문에 대한 답변에 따라 달라집니다. 클라이언트에서 렌더링되지 않고 서버에서 페이지의 마크업을 제공할 수 없는 이유가 있나요? 이 질문에 대한 답이 '아니요'인 경우 가능하면 SSR (서버 측 렌더링) 또는 정적으로 생성된 마크업을 고려해야 합니다. 미리 로드 스캐너가 중요한 리소스를 미리 검색하고 편의적으로 가져올 수 있기 때문입니다.
페이지에서 페이지 마크업의 일부에 기능을 연결하기 위해 JavaScript가 필요하다면 SSR을 사용하여 기본 자바스크립트나 하이드레이션을 사용하면 됩니다.
미리 로드 스캐너가 도움이 되도록 지원
미리 로드 스캐너는 시작 시 페이지를 더 빠르게 로드하는 데 도움이 되는 매우 효과적인 브라우저 최적화입니다. 중요한 리소스를 사전에 찾는 능력을 저해하는 패턴을 피하면 스스로 개발을 간소화할 뿐만 아니라 일부 웹 vitals를 비롯한 다양한 측정항목에서 더 나은 결과를 제공하는 더 나은 사용자 환경을 조성할 수 있습니다.
이 게시물에서 기억해야 할 내용은 다음과 같습니다.
- 브라우저 미리 로드 스캐너는 보조 HTML 파서로, 기본 페이지보다 먼저 가져올 수 있는 리소스를 더 빨리 가져올 수 있는 리소스를 발견하기 위해 차단된 경우 기본 HTML 파서보다 먼저 검사합니다.
- 초기 탐색 요청 시 서버에서 제공한 마크업에 없는 리소스는 미리 로드 스캐너에서 검색할 수 없습니다. 미리 로드 스캐너를 우회하는 방법에는 다음이 포함되나 이에 국한되지 않습니다.
- JavaScript를 사용하여 DOM에 리소스(스크립트, 이미지, 스타일시트 또는 서버의 초기 마크업 페이로드에 더 적합한 리소스) 삽입
- JavaScript 솔루션을 사용하여 페이지 상단 이미지 또는 iframe을 지연 로드합니다.
- JavaScript를 사용하여 문서 하위 리소스에 대한 참조를 포함할 수 있는 마크업이 클라이언트에서 렌더링됩니다.
- 미리 로드 스캐너는 HTML만 스캔합니다. LCP 후보 등 중요한 애셋에 대한 참조를 포함할 수 있는 다른 리소스(특히 CSS)의 콘텐츠는 검사하지 않습니다.
어떤 이유로든 미리 로드 스캐너의 로드 성능 향상 기능에 부정적인 영향을 미치는 패턴을 피할 수 없다면 rel=preload
리소스 힌트를 사용하는 것이 좋습니다. rel=preload
를 사용하는 경우 실험실 도구에서 테스트하여 원하는 효과가 있는지 확인합니다. 마지막으로 리소스를 너무 많이 미리 로드하지 마세요. 모든 항목에 우선순위를 지정하면 아무것도 우선순위가 지정되지 않기 때문입니다.
리소스
- 스크립트 삽입 '비동기 스크립트' 유해한 것으로 간주됨
- 브라우저 프리로더로 페이지 로드 속도를 높이는 방법
- 중요한 애셋을 미리 로드하여 로드 속도 개선하기
- 인지되는 페이지 속도 개선을 위해 조기에 네트워크 연결 설정
- 최대 콘텐츠 렌더링 시간 최적화
Unsplash의 Mohammad Rahmani 님 제공 히어로 이미지