리소스 로드 최적화

이전 모듈에서는 중요한 렌더링 경로의 일부 이론과 렌더링 차단 및 파서 차단 리소스가 페이지의 초기 렌더링을 지연시킬 수 있는 방법을 살펴봤습니다. 이제 이 이론을 이해했으므로 중요한 렌더링 경로를 최적화하는 기법을 알아볼 준비가 되었습니다.

페이지가 로드되면 CSS를 통해 페이지에 모양과 레이아웃을 제공하고 JavaScript를 통해 상호작용을 제공하는 많은 리소스가 HTML 내에서 참조됩니다. 이 모듈에서는 이러한 리소스와 리소스가 페이지 로드 시간에 미치는 영향과 관련된 여러 중요한 개념을 다룹니다.

렌더링 차단

이전 모듈에서 설명한 것처럼 CSS는 렌더링 차단 리소스입니다. CSS 객체 모델(CSSOM)이 구성될 때까지 브라우저가 콘텐츠를 렌더링하지 못하도록 차단하기 때문입니다. 브라우저는 사용자 환경 관점에서 바람직하지 않은 스타일이 지정되지 않은 콘텐츠의 깜박임 (FOUC)을 방지하기 위해 렌더링을 차단합니다.

위 동영상에는 스타일이 적용되지 않은 페이지가 표시되는 짧은 FOUC가 있습니다. 그런 다음 페이지의 CSS가 네트워크에서 로드되면 모든 스타일이 적용되고 스타일이 지정되지 않은 페이지 버전이 스타일이 지정된 버전으로 즉시 대체됩니다.

일반적으로 FOUC는 평소에 볼 수 있는 것이 아니지만 브라우저가 CSS가 다운로드되어 페이지에 적용될 때까지 페이지 렌더링을 차단하는 이유를 알 수 있도록 이 개념을 이해하는 것이 중요합니다. 렌더링 차단이 반드시 바람직하지 않은 것은 아니지만 CSS를 최적화하여 지속 시간을 최소화하는 것이 좋습니다.

파서 차단

파서 차단 리소스는 HTML 파서를 중단합니다(예: async 또는 defer 속성이 없는 <script> 요소). 파서가 <script> 요소를 발견하면 브라우저는 나머지 HTML 파싱을 진행하기 전에 스크립트를 평가하고 실행해야 합니다. 스크립트는 DOM이 아직 구성되는 동안 DOM을 수정하거나 액세스할 수 있으므로 이는 의도된 동작입니다.

<!-- This is a parser-blocking script: -->
<script src="/script.js"></script>

외부 JavaScript 파일 (async 또는 defer 없음)을 사용하면 파일이 발견된 시점부터 다운로드, 파싱, 실행될 때까지 파서가 차단됩니다. 인라인 JavaScript를 사용하는 경우 인라인 스크립트가 파싱되고 실행될 때까지 파서가 유사하게 차단됩니다.

미리 로드 스캐너

프리로드 스캐너는 기본 HTML 파서가 리소스를 검색하기 전에 원시 HTML 응답을 검색하여 리소스를 추측하여 가져오는 보조 HTML 파서 형태의 브라우저 최적화입니다. 예를 들어 프리로드 스캐너를 사용하면 HTML 파서가 CSS 및 JavaScript와 같은 리소스를 가져오고 처리하는 동안 차단되더라도 브라우저가 <img> 요소에 지정된 리소스 다운로드를 시작할 수 있습니다.

프리로드 스캐너를 활용하려면 서버에서 전송하는 HTML 마크업에 중요한 리소스가 포함되어야 합니다. 다음 리소스 로드 패턴은 프리로드 스캐너에서 검색할 수 없습니다.

  • background-image 속성을 사용하여 CSS로 로드된 이미지 이러한 이미지 참조는 CSS에 있으며 프리로드 스캐너에서 검색할 수 없습니다.
  • JavaScript를 사용하여 DOM에 삽입된 <script> 요소 마크업 형태의 동적으로 로드된 스크립트 또는 동적 import()을 사용하여 로드된 모듈
  • JavaScript를 사용하여 클라이언트에서 렌더링된 HTML입니다. 이러한 마크업은 JavaScript 리소스의 문자열 내에 포함되어 있으며 미리 로드 스캐너에서 검색할 수 없습니다.
  • CSS @import 선언

이러한 리소스 로드 패턴은 모두 늦게 발견된 리소스이므로 미리 로드 스캐너의 이점을 누릴 수 없습니다. 가능한 한 사용하지 마세요. 이러한 패턴을 피할 수 없는 경우 preload 힌트를 사용하여 리소스 검색 지연을 방지할 수 있습니다.

CSS

CSS는 페이지의 프레젠테이션과 레이아웃을 결정합니다. 앞서 설명한 것처럼 CSS는 렌더링 차단 리소스이므로 CSS를 최적화하면 전체 페이지 로드 시간에 상당한 영향을 미칠 수 있습니다.

압축

CSS 축소 파일을 사용하면 CSS 리소스의 파일 크기가 줄어들어 다운로드 속도가 빨라집니다. 이는 주로 소스 CSS 파일에서 공백 및 기타 보이지 않는 문자와 같은 콘텐츠를 삭제하고 결과를 새로 최적화된 파일에 출력하여 달성됩니다.

/* Unminified CSS: */

/* Heading 1 */
h1 {
  font-size: 2em;
  color: #000000;
}

/* Heading 2 */
h2 {
  font-size: 1.5em;
  color: #000000;
}
/* Minified CSS: */
h1,h2{color:#000}h1{font-size:2em}h2{font-size:1.5em}

가장 기본적인 형태의 CSS 축소는 웹사이트의 FCP를 개선할 수 있는 효과적인 최적화이며, 경우에 따라 LCP도 개선할 수 있습니다. 번들러와 같은 도구는 프로덕션 빌드에서 이 최적화를 자동으로 실행할 수 있습니다.

사용하지 않는 CSS 삭제

브라우저는 콘텐츠를 렌더링하기 전에 모든 스타일 시트를 다운로드하고 파싱해야 합니다. 파싱을 완료하는 데 필요한 시간에는 현재 페이지에서 사용되지 않는 스타일도 포함됩니다. 모든 CSS 리소스를 단일 파일로 결합하는 번들러를 사용하는 경우 사용자가 현재 페이지를 렌더링하는 데 필요한 것보다 더 많은 CSS를 다운로드할 수 있습니다.

현재 페이지에서 사용되지 않는 CSS를 확인하려면 Chrome DevTools의 범위 도구를 사용하세요.

Chrome DevTools의 커버리지 도구 스크린샷 하단 창에서 CSS 파일이 선택되어 있으며 현재 페이지 레이아웃에서 사용되지 않는 CSS가 상당량 표시됩니다.
Chrome DevTools의 범위 도구는 현재 페이지에서 사용되지 않는 CSS (및 JavaScript)를 감지하는 데 유용합니다. 페이지 렌더링을 지연시킬 수 있는 훨씬 큰 CSS 번들을 제공하는 대신 CSS 파일을 여러 페이지에서 로드할 여러 리소스로 분할하는 데 사용할 수 있습니다.

사용하지 않는 CSS를 삭제하면 다운로드 시간이 단축될 뿐만 아니라 브라우저에서 처리해야 하는 CSS 규칙 수가 줄어들어 렌더링 트리 구성도 최적화됩니다.

CSS @import 선언 방지

편리해 보일 수 있지만 CSS에서 @import 선언은 피해야 합니다.

/* Don't do this: */
@import url('style.css');

HTML에서 <link> 요소가 작동하는 방식과 마찬가지로 CSS의 @import 선언을 사용하면 스타일 시트 내에서 외부 CSS 리소스를 가져올 수 있습니다. 이 두 접근 방식의 주요 차이점은 HTML <link> 요소가 HTML 응답의 일부이므로 @import 선언에 의해 다운로드된 CSS 파일보다 훨씬 빨리 발견된다는 것입니다.

@import 선언이 검색되려면 이를 포함하는 CSS 파일을 먼저 다운로드해야 하기 때문입니다. 이로 인해 요청 체인이 발생하며, CSS의 경우 페이지가 처음 렌더링되는 시간이 지연됩니다. 또 다른 단점은 @import 선언을 사용하여 로드된 스타일 시트를 미리 로드 스캐너가 검색할 수 없으므로 늦게 발견되는 렌더링 차단 리소스가 된다는 것입니다.

<!-- Do this instead: -->
<link rel="stylesheet" href="style.css">

대부분의 경우 <link rel="stylesheet"> 요소를 사용하여 @import를 대체할 수 있습니다. <link> 요소를 사용하면 스타일 시트를 동시에 다운로드할 수 있으며 순차적으로 스타일 시트를 다운로드하는 @import 선언과 달리 전체 로드 시간을 줄일 수 있습니다.

중요한 인라인 CSS

CSS 파일을 다운로드하는 데 걸리는 시간이 페이지의 FCP를 늘릴 수 있습니다. 문서 <head>에 중요한 스타일을 인라인 처리하면 CSS 리소스에 대한 네트워크 요청이 제거되며, 올바르게 처리하면 사용자의 브라우저 캐시가 준비되지 않은 경우 초기 로드 시간을 개선할 수 있습니다. 나머지 CSS는 비동기식으로 로드하거나 <body> 요소의 끝에 추가할 수 있습니다.

<head>
  <title>Page Title</title>
  <!-- ... -->
  <style>h1,h2{color:#000}h1{font-size:2em}h2{font-size:1.5em}</style>
</head>
<body>
  <!-- Other page markup... -->
  <link rel="stylesheet" href="non-critical.css">
</body>

단점은 많은 CSS를 인라인으로 처리하면 초기 HTML 응답에 더 많은 바이트가 추가된다는 것입니다. HTML 리소스는 매우 오랫동안 또는 전혀 캐시할 수 없는 경우가 많으므로 인라인 CSS는 외부 스타일시트에서 동일한 CSS를 사용할 수 있는 후속 페이지에 대해 캐시되지 않습니다. 페이지의 성능을 테스트하고 측정하여 노력에 상응하는 효과가 있는지 확인하세요.

CSS 데모

자바스크립트

JavaScript는 웹의 대부분의 상호작용을 지원하지만 비용이 발생합니다. JavaScript를 너무 많이 전송하면 페이지 로드 중에 웹페이지의 응답이 느려질 수 있으며, 상호작용 속도를 늦추는 응답성 문제가 발생할 수도 있습니다. 이로 인해 사용자가 불편을 느낄 수 있습니다.

렌더링 차단 JavaScript

defer 또는 async 속성 없이 <script> 요소를 로드하면 스크립트가 다운로드되고 파싱되고 실행될 때까지 브라우저에서 파싱과 렌더링을 차단합니다. 마찬가지로 인라인 스크립트는 스크립트가 파싱되고 실행될 때까지 파서를 차단합니다.

asyncdefer 비교

asyncdefer를 사용하면 외부 스크립트가 HTML 파서를 차단하지 않고 로드될 수 있으며 type="module"가 있는 스크립트 (인라인 스크립트 포함)는 자동으로 지연됩니다. 하지만 asyncdefer에는 이해해야 할 몇 가지 중요한 차이점이 있습니다.

다양한 스크립트 로드 메커니즘을 보여주는 그림으로, async, defer, type=&#39;module&#39;과 같은 다양한 속성과 세 가지 모두의 조합을 기반으로 파서, 가져오기, 실행 역할을 자세히 설명합니다.
출처: https://html.spec.whatwg.org/multipage/scripting.html

async로 로드된 스크립트는 다운로드되면 즉시 파싱되고 실행되는 반면 defer로 로드된 스크립트는 HTML 문서 파싱이 완료되면 실행됩니다. 이는 브라우저의 DOMContentLoaded 이벤트와 동시에 발생합니다. 또한 async 스크립트는 순서대로 실행되지 않을 수 있지만 defer 스크립트는 마크업에 표시된 순서대로 실행됩니다.

클라이언트 측 렌더링

일반적으로 JavaScript를 사용하여 중요한 콘텐츠나 페이지의 LCP 요소를 렌더링하지 않는 것이 좋습니다. 이를 클라이언트 측 렌더링이라고 하며 단일 페이지 애플리케이션 (SPA)에서 광범위하게 사용되는 기술입니다.

JavaScript로 렌더링된 마크업은 클라이언트 렌더링 마크업 내에 포함된 리소스가 미리 로드 스캐너에서 검색되지 않기 때문에 미리 로드 스캐너를 우회합니다. 이로 인해 LCP 이미지와 같은 중요한 리소스의 다운로드가 지연될 수 있습니다. 브라우저는 스크립트가 실행되고 요소가 DOM에 추가된 후에만 LCP 이미지 다운로드를 시작합니다. 따라서 스크립트는 검색되고 다운로드되고 파싱된 후에만 실행될 수 있습니다. 이를 크리티컬 요청 체인이라고 하며 피해야 합니다.

또한 JavaScript를 사용하여 마크업을 렌더링하면 탐색 요청에 대한 응답으로 서버에서 다운로드한 마크업보다 긴 작업이 생성될 가능성이 높습니다. HTML의 클라이언트 측 렌더링을 광범위하게 사용하면 상호작용 지연 시간에 부정적인 영향을 미칠 수 있습니다. 이는 특히 페이지의 DOM이 매우 큰 경우에 해당하며, JavaScript가 DOM을 수정할 때 상당한 렌더링 작업이 트리거됩니다.

압축

CSS와 마찬가지로 JavaScript를 축소하면 스크립트 리소스의 파일 크기가 줄어듭니다. 이렇게 하면 다운로드가 더 빨라져 브라우저가 JavaScript를 파싱하고 컴파일하는 프로세스로 더 빠르게 이동할 수 있습니다.

또한 JavaScript 축소는 CSS와 같은 다른 애셋을 축소하는 것보다 한 단계 더 나아갑니다. JavaScript가 축소되면 공백, 탭, 주석과 같은 항목이 삭제될 뿐만 아니라 소스 JavaScript의 기호도 단축됩니다. 이 프로세스를 uglification이라고도 합니다. 차이점을 확인하려면 다음 JavaScript 소스 코드를 사용하세요.

// Unuglified JavaScript source code:
export function injectScript () {
  const scriptElement = document.createElement('script');
  scriptElement.src = '/js/scripts.js';
  scriptElement.type = 'module';

  document.body.appendChild(scriptElement);
}

위의 JavaScript 소스 코드를 uglify하면 결과는 다음 코드 스니펫과 비슷하게 표시될 수 있습니다.

// Uglified JavaScript production code:
export function injectScript(){const t=document.createElement("script");t.src="/js/scripts.js",t.type="module",document.body.appendChild(t)}

위 스니펫에서 소스의 사람이 읽을 수 있는 변수 scriptElementt로 단축된 것을 확인할 수 있습니다. 대규모 스크립트 모음에 적용하면 웹사이트의 프로덕션 JavaScript가 제공하는 기능에 영향을 주지 않고도 상당한 절감 효과를 얻을 수 있습니다.

번들러를 사용하여 웹사이트의 소스 코드를 처리하는 경우 프로덕션 빌드에 대해 uglification이 자동으로 실행되는 경우가 많습니다. Terser와 같은 Uglifier도 구성 가능성이 높아 uglification 알고리즘의 공격성을 조정하여 최대 절감액을 달성할 수 있습니다. 하지만 일반적으로 난독화 도구의 기본값은 출력 크기와 기능 보존 간의 적절한 균형을 유지하기에 충분합니다.

JavaScript 데모

학습한 내용 테스트

브라우저에서 여러 CSS 파일을 로드하는 가장 좋은 방법은 무엇인가요?

<link> 요소가 여러 개 있습니다.
CSS @import 선언입니다.

브라우저 사전 로드 스캐너는 어떤 역할을 하나요?

HTML 리소스에서 <link rel="preload"> 요소를 감지합니다.
DOM 파서가 리소스를 발견하기 전에 원시 마크업을 검사하여 리소스를 더 빨리 발견하는 보조 HTML 파서입니다.

브라우저가 JavaScript 리소스를 다운로드할 때 기본적으로 HTML 파싱을 일시적으로 차단하는 이유는 무엇인가요?

JavaScript 평가가 매우 CPU 집약적인 작업이기 때문입니다. HTML 파싱을 일시중지하면 CPU가 스크립트 로드를 완료하는 데 더 많은 대역폭을 사용할 수 있습니다.
스크립트가 DOM을 수정하거나 액세스할 수 있기 때문입니다.
스타일이 지정되지 않은 콘텐츠의 깜박임 (FOUC)을 방지합니다.

다음: 리소스 힌트로 브라우저 지원

이제 <head> 요소에 로드된 리소스가 초기 페이지 로드 및 다양한 측정항목에 미치는 영향을 파악했으므로 다음 단계로 넘어갈 시간입니다. 다음 모듈에서는 리소스 힌트를 살펴보고, 리소스 힌트가 브라우저에 유용한 힌트를 제공하여 브라우저가 리소스를 로드하고 교차 출처 서버에 대한 연결을 브라우저가 힌트 없이 하는 것보다 더 빨리 시작할 수 있는 방법을 알아봅니다.