이전 모듈에서는 주요 렌더링 경로에 관한 몇 가지 이론을 살펴보고 렌더링 차단 및 파서 차단 리소스가 페이지의 초기 렌더링을 지연시키는 방법을 알아보았습니다. 이제 이론적인 이론 중 일부를 이해했으므로 주요 렌더링 경로를 최적화하는 몇 가지 기법을 알아볼 준비가 되었습니다.
페이지가 로드될 때 여러 리소스가 HTML 내에서 참조됩니다. 이러한 리소스는 CSS를 통해 페이지의 모양과 레이아웃을 제공하고, 자바스크립트를 통한 상호작용을 제공합니다. 이 모듈에서는 이러한 리소스와 관련된 여러 가지 중요 개념과 리소스가 페이지 로드 시간에 미치는 영향을 다룹니다.
렌더링 차단
이전 모듈에서 설명한 대로 CSS는 CSS 객체 모델(CSSOM)이 구성될 때까지 브라우저가 콘텐츠를 렌더링하지 못하도록 차단하기 때문에 렌더링 차단 리소스입니다. 브라우저는 사용자 환경 관점에서 바람직하지 않은 스타일이 지정되지 않은 콘텐츠 플래시 (FOUC)를 방지하기 위해 렌더링을 차단합니다.
이전 동영상에는 스타일을 지정하지 않고 페이지를 볼 수 있는 간단한 FOUC가 있습니다. 그런 다음 페이지의 CSS가 네트워크에서 로드를 완료하면 모든 스타일이 적용되고, 스타일이 지정되지 않은 버전의 페이지가 스타일이 지정된 버전으로 즉시 대체됩니다.
일반적으로 FOUC는 일반적으로 표시되지 않지만 CSS가 다운로드되어 페이지에 적용될 때까지 브라우저가 페이지 렌더링을 차단하는 이유를 알 수 있도록 개념을 이해하는 것이 중요합니다. 렌더링 차단이 반드시 바람직한 것은 아니지만 CSS를 최적화된 상태로 유지하여 렌더링 지속 시간을 최소화하는 것이 좋습니다.
파서 차단
파서 차단 리소스는 HTML 파서(예: async
또는 defer
속성이 없는 <script>
요소)를 중단합니다. 파서가 <script>
요소를 만나면
브라우저가 스크립트를 평가하고 실행한 후에 HTML의 나머지 부분을
파싱해야 합니다. 이는 스크립트가 아직 생성되는 동안 DOM을 수정하거나 액세스할 수 있기 때문입니다.
<!-- This is a parser-blocking script: -->
<script src="/script.js"></script>
외부 자바스크립트 파일 (async
또는 defer
없이)을 사용하는 경우 파서는 파일이 발견된 시점부터 다운로드, 파싱, 실행될 때까지 차단됩니다. 인라인 자바스크립트를 사용하면 인라인 스크립트가 파싱되고 실행될 때까지 파서가 유사하게 차단됩니다.
미리 로드 스캐너
미리 로드 스캐너는 기본 HTML 파서가 리소스를 발견하기 전에 원시 HTML 응답을 스캔하여 리소스를 찾아 추측하는 방식으로 가져오는 보조 HTML 파서 형태의 브라우저 최적화입니다. 예를 들어 미리 로드 스캐너를 사용하면 CSS 및 자바스크립트와 같은 리소스를 가져오고 처리하는 동안 HTML 파서가 차단된 경우에도 브라우저가 <img>
요소에 지정된 리소스 다운로드를 시작할 수 있습니다.
미리 로드 스캐너를 활용하려면 서버에서 전송한 HTML 마크업에 중요한 리소스를 포함해야 합니다. 다음 리소스 로드 패턴은 미리 로드 스캐너로 검색할 수 없습니다.
background-image
속성을 사용하여 CSS에서 로드한 이미지 이러한 이미지 참조는 CSS에 있으며 미리 로드 스캐너로 검색할 수 없습니다.- JavaScript 또는 동적
import()
를 사용하여 로드된 모듈을 사용하여 DOM에 삽입된<script>
요소 마크업 형식의 동적으로 로드된 스크립트입니다. - 자바스크립트를 사용하여 클라이언트에서 렌더링된 HTML입니다. 이러한 마크업은 자바스크립트 리소스의 문자열 내에 포함되며 미리 로드 스캐너에서 검색할 수 없습니다.
- 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를 표시합니다.](https://web.developers.google.cn/static/learn/performance/optimize-resource-loading/image/fig-1.png?authuser=3&hl=ko)
사용하지 않는 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
를 대체할 수 있습니다. 스타일시트를 연속적으로 다운로드하는 @import
선언과 달리 <link>
요소를 사용하면 스타일시트를 동시에 다운로드할 수 있으며 전체 로드 시간이 줄어듭니다.
중요한 인라인 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
자바스크립트는 웹에서 대부분의 상호작용을 유도하지만 비용이 발생합니다. 자바스크립트를 너무 많이 포함하면 페이지 로드 중에 웹페이지의 응답 속도가 느려지고 심지어 상호작용 속도를 늦추는 응답성 문제가 발생할 수 있으며, 두 경우 모두 사용자에게 불편을 초래할 수 있습니다.
렌더링 차단 자바스크립트
defer
또는 async
속성 없이 <script>
요소를 로드하면 스크립트가 다운로드, 파싱, 실행될 때까지 브라우저에서 파싱과 렌더링이 차단됩니다. 마찬가지로 인라인 스크립트는 스크립트가 파싱되고 실행될 때까지 파서를 차단합니다.
async
및 defer
비교
async
및 defer
를 사용하면 HTML 파서를 차단하지 않고 외부 스크립트를 로드할 수 있으며 type="module"
가 포함된 스크립트 (인라인 스크립트 포함)가 자동으로 지연됩니다. 그러나 async
와 defer
에는 이해해야 하는 몇 가지 차이점이 있습니다.
async
로 로드된 스크립트는 다운로드 즉시 파싱 및 실행되는 반면 defer
로 로드된 스크립트는 HTML 문서 파싱이 완료될 때 실행됩니다. 이 스크립트는 브라우저의 DOMContentLoaded
이벤트와 동시에 발생합니다.
또한 async
스크립트는 비순차적으로 실행되는 반면 defer
스크립트는 마크업에 표시된 순서대로 실행됩니다.
클라이언트 측 렌더링
일반적으로 중요한 콘텐츠나 페이지의 LCP 요소를 렌더링하는 데 JavaScript를 사용하면 안 됩니다. 이를 클라이언트 측 렌더링이라고 하며 단일 페이지 애플리케이션 (SPA)에서 광범위하게 사용되는 기법입니다.
자바스크립트에서 렌더링된 마크업은 미리 로드 스캐너를 회피합니다. 클라이언트가 렌더링한 마크업 내에 포함된 리소스를 검색할 수 없기 때문입니다. 이로 인해 LCP 이미지와 같은 중요한 리소스의 다운로드가 지연될 수 있습니다. 브라우저는 스크립트가 실행되고 요소를 DOM에 추가한 후에만 LCP 이미지 다운로드를 시작합니다. 결과적으로 스크립트는 검색, 다운로드, 파싱된 후에만 실행될 수 있습니다. 이를 중요 요청 체인이라고 하며 피해야 합니다.
또한 자바스크립트를 사용하여 마크업을 렌더링하면 탐색 요청에 대한 응답으로 서버에서 다운로드한 마크업보다 장기 작업이 생성될 가능성이 높습니다. HTML의 클라이언트 측 렌더링을 광범위하게 사용하면 상호작용 지연 시간에 부정적인 영향을 줄 수 있습니다. 특히 페이지의 DOM이 매우 커서 JavaScript가 DOM을 수정할 때 상당한 렌더링 작업을 트리거하는 경우에 그렇습니다.
축소
CSS와 마찬가지로 자바스크립트를 축소하면 스크립트 리소스의 파일 크기가 줄어듭니다. 이렇게 하면 다운로드 속도가 빨라져 브라우저가 JavaScript를 더 빠르게 파싱하고 컴파일하는 프로세스를 진행할 수 있습니다.
또한 JavaScript 축소는 CSS와 같은 다른 애셋을 축소하는 것보다 한 단계 더 높습니다. JavaScript가 축소되면 공백, 탭, 주석 등의 항목이 제거될 뿐만 아니라 소스 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);
}
위의 자바스크립트 소스 코드를 잘못 작성한 경우 결과는 다음 코드 스니펫과 비슷할 수 있습니다.
// Uglified JavaScript production code:
export function injectScript(){const t=document.createElement("script");t.src="/js/scripts.js",t.type="module",document.body.appendChild(t)}
앞의 스니펫에서는 소스의 사람이 읽을 수 있는 변수 scriptElement
가 t
로 축약된 것을 확인할 수 있습니다. 많은 수의 스크립트에 적용하면 웹사이트의 프로덕션 자바스크립트가 제공하는 기능에 영향을 미치지 않고도 상당한 절감 효과를 얻을 수 있습니다.
번들러를 사용하여 웹사이트의 소스 코드를 처리하는 경우 프로덕션 빌드에 대해서는 자동 현지화가 실행되는 경우가 많습니다. 예를 들어 Terser와 같은 Uglifier는 구성 가능성이 높으므로 이 기능을 사용하면 축소 알고리즘의 강도를 조정하여 최대한 절약할 수 있습니다. 그러나 일반적으로 무글화 도구의 기본값만으로도 출력 크기와 기능 보존 간에 적절한 균형을 이룰 수 있습니다.
JavaScript 데모
학습한 내용 테스트
브라우저에서 여러 CSS 파일을 로드하는 가장 좋은 방법은 무엇인가요?
@import
선언입니다.<link>
요소브라우저 미리 로드 스캐너는 어떤 기능을 하나요?
<link rel="preload">
요소를 감지합니다.
자바스크립트 리소스를 다운로드할 때 브라우저에서 기본적으로 HTML 파싱을 일시적으로 차단하는 이유는 무엇인가요?
다음: 리소스 힌트로 브라우저 지원
<head>
요소에 로드된 리소스가 초기 페이지 로드와 다양한 측정항목에 어떤 영향을 미치는지 알았으니 이제 다음 문제로 넘어갑니다. 다음 모듈에서는 리소스 힌트를 살펴보고, 리소스 힌트를 통해 브라우저에 유용한 힌트를 제공하여 리소스가 없는 경우보다 더 빨리 리소스를 로드하고 교차 출처 서버에 대한 연결을 여는 방법을 알아봅니다.