February 13, 2020
React, Vue, Angular 같은 Library 혹은 Framework로 개발할 경우 빌드 결과물인 index.html은 자동으로 생성되기 때문에 직접 볼 필요가 없을 수도 있습니다.
하지만, 최종 html의 구성 요소를 잘 파악하고 있다면 성능 개선이나 디버깅하는 상황에서 용이하게 활용할 수 있습니다.
link tag
script tag
link
tag는 웹 페이지와 외부 리소스의 관계를 나타냅니다. css를 가져올 때 많이 사용하지만, 사이트 아이콘, sitemap 그리고 script를 가져올 때도 사용됩니다.
기본 속성 외에 media
속성을 추가로 정의할 수 있습니다. media
속성을 적어줄 경우, 이 조건을 만족하는 경우에만 css를 불러옵니다.
<link href="print.css" rel="stylesheet" media="print">
<link href="mobile.css" rel="stylesheet" media="screen and (max-width: 600px)">
옵션에 따라 다른 리소스보다 먼저 로드 되거나 미리 연결을 맺게 할 수 있습니다.
<link rel="preload">
는 현재 해당 리소스가 필요하며, 가능한 빠르게 가져오도록 설정하는 것입니다.
대표적인 사용 사례는 font입니다.
<link rel="preload" as="font" crossorigin="crossorigin" type="font/woff2" href="myfont.woff2">
preload 없이 font를 요청할 경우 텍스트 렌더링이 지연될 수 있습니다. font요청은 DOM, CSSOM 트리 생성 이후에 시작되기 때문입니다. 브라우저가 화면을 그리는 순서는 다음과 같습니다.
브라우저가 모든 CSS 콘텐츠가 수신된 후 CSSOM을 생성하고 이를 DOM 트리와 결합하여 렌더링 트리를 생성합니다.
Rendering 트리 생성 이후 바로 보여야 하는 컨텐츠의 경우 위 상황으로 인해 font가 늦게 적용되어 보일 수 있습니다.
브라우저마다 구현 방식이 다릅니다. 자세한 내용은 Web Font Optimization 을 참고해주세요.
이런 이유로, 필수적으로 사용되는 리소스는 preload를 통해 요청함으로써 우선순위를 높여야 합니다. font를 preload
로 요청할 경우 CSSOM 생성이 완료되는 것을 기다리지 않고 font를 요청합니다.
만약 preload
를 통해 가져온 리소스를 3초 이내로 사용하지 않을 경우 Chrome Dev Tools에서는 다음과 같은 경고를 보여줍니다.
<link rel="preconnect">
는 HTTP 요청이 server에 전달되기 전에 미리 연결을 맺어두도록 설정하는 것입니다.
다른 Domain에 리소스를 요청하는 상황에서는 해당 서버의 응답 속도를 보장할 수 없습니다. 특히, 보안 연결이 필요할 경우 데이터를 받아오는 작업보다 DNS Lookup, Redirection, TCP Handshake 등 connection을 맺는 작업이 더 오래 걸릴 수 있습니다.
preconnect
는 이런 연결을 미리 맺어 둔다는 뜻입니다. 사용법은 다음과 같습니다.
<link rel="preconnect" href="https://example.com">
실제로는 다음 동작들이 수행됩니다.
preconnect를 이용하면 round trip을 제거할 수 있고, 이 결과로 소요 시간을 많이 줄일 수 있습니다.
같은 도메인의 Google CSS와 font를 동시에 요청할 수 있어, 결과적으로 3개의 Round Trip을 제거할 수 있습니다.
<link rel="prefetch">
는 우선순위가 높은 리소스들을 모두 요청한 후 나머지 리소스들을 idle time에 가져와서 브라우저 캐시에 저장하는 것입니다.
따라서, 첫 페이지에 바로 필요한 리소스에 대해서는 적합하지 않습니다.
prefetch
에는 3가지 종류가 있습니다.
1. Link Prefetching
위에서 설명한 대로, prefetching
은 리소스를 fetch 하고 그 결과를 브라우저 캐시에 저장합니다.
“This technique has the potential to speed up many interactive sites, but won’t work everywhere. For some sites, it’s just too difficult to guess what the user might do next. For others, the data might get stale if it’s fetched too soon. It’s also important to be careful not to prefetch files too soon, or you can slow down the page the user is already looking at. - Google Developers”
어떤 자원을 prefetch
로 요청해야 할지 판단해서 사용해야 한다고 말하고 있습니다. 무분별하게 사용하면 현재 사용자가 보고 있는 페이지가 느려질 수 있기 때문입니다.
또한, 브라우저 지원 범위도 확인해서 사용해야 합니다.
2. DNS Prefetching
백그라운드에서 DNS Lookup을 수행하는 것입니다.
리소스가 필요할 때 DNS Lookup에 소요되는 시간을 없앰으로써 리소스를 더 빠르게 가져올 수 있습니다.
사용 방법은 다음과 같습니다.
<!-- Prefetch DNS for external assets -->
<link rel="dns-prefetch" href="//fonts.googleapis.com">
<link rel="dns-prefetch" href="//www.google-analytics.com">
<link rel="dns-prefetch" href="//cdn.domain.com">
3. Prerendering
prerendering
은 필요할 수도 있는 리소스를 미리 요청한다는 점에서 prefetch와 같습니다.
차이점은 preredering
은 실제로 전체 페이지를 백그라운드에서 렌더링한다는 점입니다.
필요 없는 자원에 대해 prerendering을 요청할 경우 bandwith 낭비가 발생합니다.
Note. crossorigin
외부 리소스를 요청할 때, crossorigin
항목이 없다면 로드된 리소스를 무시하고 새로 가져온 다른 속성이 적용됩니다. 외부 도메인에서 요청 시 필요한 정보이며 가능한 값은 다음과 같습니다.
HTML을 parsing 하는 과정에서 script tag를 만나면 해당 작업이 block 됩니다. css는 화면 렌더링에 필수적인 요소일 가능성이 높기 때문에 head
에 위치시키지만, ‘동작’과 관련된 JavaScript를 load 하느라 Rendering을 지연시키는 것은 좋은 사용자 경험이라고 할 수 없습니다.
이런 이유로 대부분 script tag를 </body>
바로 앞에 선언합니다. 하지만 이렇게 사용하는 것 말고도, async나 defer를 사용하는 방법도 있습니다.
위 그림에서 알 수 있듯 script tag는 HTML Parsing을 block 합니다.
따라서, head에 script tag를 위치시킬 경우 사용자가 흰 화면을 보는 시간이 늘어나게 됩니다. 이를 막기 위해 body tag 제일 하단에 위치 시킵니다.
async와 defer를 지원하지 않는 브라우저에서 이 방법으로 대응합니다.
async 속성은 head
에 위치하지 않으면 아무 의미가 없습니다.
사용하지 않은 것과 똑같이 동작
script를 비동기로 요청하고 fetch가 완료되면 HTML Parsing을 중지하고 script를 실행합니다. 실행이 완료된 후 다시 Parsing 작업이 진행됩니다.
async와 같이 script를 비동기로 가져오고 HTML Parsing 이후에 실행됩니다. HTML의 parsing을 막지 않아서, 화면이 빠르게 렌더링 될 수 있습니다.
방금 살펴 본 두 속성은 Single Page Application(이하 SPA)에서는 어떻게 적용될까요?
Client Side Rendering을 하는 SPA는 JS Parsing을 통해 화면을 Rendering 합니다. 두 가지 JS 파일이 있다고 가정해보겠습니다.
src
폴더 하위의 JS 파일(화면을 그리기 위한 용도)<!DOCTYPE html>
<html lang="ko">
<head>
<title>SPA - script</title>
</head>
<body>
<div id="wrap"></div>
<script src="external.js"></script>
<script src="app.js"></script>
</body>
</html>
external.js
를 요청한 후 바로 app.js
를 요청합니다. external.js
에 대한 로드가 완료되면 즉시 실행되고 HTML Parsing이 중단됩니다.app.js
역시 로드가 완료되었을 때 HTML Parsing이 끝나지 않았다면 중단되고 script가 실행됩니다.
이렇게 두 가지 이상의 스크립트를 요청할 때 async는 순서가 보장되지 않으므로 DOM 제어와 관련 없는 스크립트에 대해서만 사용하는 것이 좋습니다.
defer 역시 두 가지 스크립트를 비동기로 요청합니다. 다만, defer
는 vendor.js
와 app.js
를 비동기로 요청하지만, 실행 순서는 보장됩니다.
defer
나 async
를 사용하면 스크립트 요청 시간을 줄일 수 있습니다. 하지만, SPA에서는 HMTL Parsing 자체에는 많은 시간이 소요되지 않습니다. 따라서, 위 두 가지를 사용한다고 해서 비약적인 성능 개선을 기대하기는 어려울 수도 있습니다.
위 예제에서 화면이 표현되는 시점은 app.js 실행 이후이고, SPA의 HTML은 매우 간단한 구조이기 때문입니다.
오히려, DOM을 제어하는 스크립트를 실행 순서가 보장되지 않는 async
로 요청할 경우 오류가 발생할 수 있습니다. 의존성이 없는 GA 같은 통계에 관한 스크립트 등에 사용하는 것이 적합합니다.