JavaScript

JavsScript 리소스 우선 순위, fetchpriority, preload, prefetch, preconnect 깊게 알아보자!

남희정 2023. 9. 3. 21:46

포스팅의 발단

HTML 파일 아래 <script>태그가 길게 늘어져있다. 스크립트를 많이 부르는 상황이 왜 걱정될까? 요청이 많아질수록 요청으로 오가는 데이터의 양이 적을지라도 매번 3-handshake를 매번 해야하는 것은 비효율적이다. 한 파일로 합치는 것도 어느 정도 유리할 수 있다. 하지만 실질적으로 길이에 상관없게끔 만들어버릴수도 있다. 먼저 요청할 것을 정해버리는 것이다. 페이지의 로딩을 최적화 할 수 있는 방법을 정확하게 알아보자. 😃

 

Parser-Blocking Scripts, JavaScript

JavaScript는 파서 차단 리소스(Parser-Blocking Resource)이다. 기본적으로 파싱을 중지시킨다.

HTML을 파싱하면서 <script> 태그를 만나게 된다면 DOM 생성을 중지시키고 JS 엔진에게 제어 권한을 넘겨 JS를 실행시킨 후 나머지 HTML을 파싱한다.

스크립트가 페이지에서 무엇을 수행할지 모르기 때문에 브라우저는 최악의 시나리오를 가정하고 파서를 차단한다.

DOM의 특정 엘리먼트와 JS가 인터렉션되는 비즈니스 로직이 있다면 이것은 문제가 될 수 있고, UX적으로도 좋지 않다.

DOM 렌더 과정을 막지 않고 비동기적으로 불러오는 방식으로 개발자가 컨트롤할 방법은 뭐가 있을까? 🤔

 

일반적인 스크립트의 로드 순서

async

 

async 스크립트의 로드 순서

 

<script src="example.js" async></script>

 

✶ 스크립트를 비동기적으로 로드하도록 지정한다.

✶ 스크립트를 불러오는 과정에서 DOM 렌더를 차단하지 않도록 보장한다.
오직 파일을 불러오는 것만 병렬적으로 실행한다는 것이 중요.

✶ 실행되는 시점은 다운로드가 완료되는 순서에 따라 다를 수 있으므로 스크립트 간의 의존성이 있는 경우 조심해야 된다.

✶ 파일의 로딩을 마치게 되면 즉시 DOM 렌더를 멈추고 async 방식으로 불러온 스크립트 파일의 해석을 시작한다.

실행 순서가 보장되지 않음.

✶ 독립적으로 동작하는 스크립트 파일에 사용된다.

DOMContentLoaded 이벤트 콜백으로 load를 보장할 수 없음.

 

⭕️ async 스크립트는 DOM에 직접 접근하지 않거나, 다른 스크립트에 의존적이지 않은 스크립트들을 독립적으로 실행해야 할 때 효과적이다.

 

defer

 

defer 스크립트의 로드 순서

<script src="example.js" defer></script>

 

 

✶ 스크립트 파일을 다운로드하면 HTML 파싱은 계속 진행되지만 실행은 페이지 로딩이 완료된 후에 순차적으로 수행된다.

 스크립트가 페이지 요소에 접근하거나 수정해야 하는 경우에 유용하다.

모든 DOM이 로드된 후에야 실행된다. 

✶ 스크립트의 실행 순서는 스크립트가 나타나는 순서대로 보장된다.

✶ 페이지 초기화 및 상호작용 스크립트에 사용된다. 

DOMContentLoaded 이벤트 발생 전 실행된다. 

외부 스크립트에만 유효하다. (src가 없으면 defer 속성은 무시된다.)

 

⭕️ defer 스크립트는 DOM의 모든 엘리먼트에 접근할 수 있고 실행 순서도 보장하기 때문에 가장 범용적으로 사용할 수 있는 속성이다. 스크립트 파일 간 의존성이 있는 경우에도 정답이 될 수 있다.

 

🌟사전 연결로 Load 속도 개선하기 🌟 

preload

<link rel="preload" href="resource_url" as="resource_type">
<head>
  ...
  <link rel="preload" href="styles.css" as="style" />
  <link rel="preload" href="ui.js" as="script" />
  ...
</head>

 

 

브라우저에게 특정 리소스(script, stylesheet, font 등)를 사전 로드하도록 지시하는 데에 사용된다. 주로 큰 리소스나 중요한 리소스의 로딩을 최적화하는 데에 사용되며 페이지 성능을 향상 시키는 데에 도움을 줄 수 있다. 

 

브라우저는 미리 로드된 리소스를 캐시하므로 필요시 즉시 사용할 수 있다.

✔️ preload는 선언적 fetch다. Document의 onload 이벤트를 막지 않으면서 브라우저가 자원을 요청하도록 강제할 수 있다.

✔️ Resource Hint(preconnect 및 prefetch)는 로딩할지 말지는 브라우저가 적합하다고 판단하는 대로 실행된다. 

 

Preload can decouple the load event from script parse time. If you haven&rsquo;t used it before, read &lsquo; Preload: What is it Good For? - by Yoav Weiss

✶ 사전 로딩은 실행과 분리된다 

브라우저는 일반적으로 preload된 JavaScript파일을 실행하지 않는다. 다운로드만 하고, 실행하지 않음.

파일을 다운로드 하는 시점과 실행하는 시점을 분리할 수 있으므로 웹페이지의 초기 로딩 과정을 최적화하는 데에 도움이 된다.

✶ Interactive 시간을 개선

코드를 미리 다운로드 해놓기 때문에 실행시 필요한 시간을 단축하여 사용자 경험을 향상 시킴.

✶ as 속성 사용

- script, style, font, image, video, document, fetch(XHR/fetch 요청)

리소스 유형이 설정되어 있지 않으면 브라우저는 해당 리소스를 사용하지 않음.

✶ 중복 리소스 참조(double fetch)

preload는 브라우저가 반드시 리소스를 가져오게 만든다. 리소스를 중복 참조하면 중복된 개수만큼 리소스를 가져오기 때문에 중복해서 참조하지 않도록 해야됨.

<link rel="preload" href="fonts/Roboto-regular.woff2" as="font" crossorigin>

폰트 리소스는 crossorigin 을 설정해주어야 한다.

✶ 반드시 사용되는 리소스에만 사용

preload는 현재 페이지에서 반드시 사용되는 리소스에만 사용되어야 한다.

 

⚠️ 초기 로드시 사용되지 않은 preload는 3초 내에 Chrome에서 콘솔 경고를 트리거 한다.

사용되지 않은 preload 경고 메세지

 

개발자가 성능 향상을 위해 preload를 사용하여 다른 자원들을 cache 하려 했던 것으로 보이는데, preload된 자원이 사용되고 있지 않다면 성능에서의 이점이 없는데 이 기능을 사용하고 있기 때문에 경고로 알려준다.

 

CSS 및 여러 리소스에도 사용 가능

⚠️ preload link들을 어떤 형태로든 사용해도 되지만 짚고 넘어가야할 부분이 있다. 많은 서버들은 HTTP header form 내의 preload link를 본 후 http/2 server push를 시작한다. 의도치않게 push들을 trigger하지 않도록 HTTP header 대신 preload link tags를 사용하거나 header에 ‘nopush’ atttribute를 추가함으로써 원하지 않는 push를 보내는 것을 방지할 수 있다.

Link: </css/style.css>; rel="preload"; as="style" nopush

 

prefetch

<link rel="prefetch" href="app.js" as="script">

링크에 대한 관계를 빨리 파악할 수 있도록 도움을 준다.

navigation boundaries(사용자가 웹 애플리케이션에서 다른 뷰나 페이지로 이동할 때의 경계)에서 사용될 것이라 생각되는 자원들은 prefetch를 사용한다.

✶ prefetch된 리소스는 preload된 리소스보다 훨씬 낮은 우선순위로 로드되므로 현재 페이지에 대한 사용자 경험은 부정적인 영향을 받지 않는다.

<link rel="prefetch" href="page-2.html">

⚠️ 재귀적으로 동작하지 않는다.

위의 코드와 같이 prefetch를 사용한다면, page-2.html이라는 HTML 리소스를 가져올 수 있지만 page-2.html에서 사용되는 CSS 등의 리소스들은 가져오지 않는다.

 

구글에서 광범위하게 사용되고 있다. 검색 결과 페이지에서 목적 페이지를 렌더링하는 데에 걸리는 시간을 줄일 목적으로 쓰였다.

 

dns-prefetch

<link rel="dns-prefetch" href="https://fonts.googleapis.com/" />

✶ 리소스가 요청되기 전에 도메인 이름을 미리 확인하도록 설정.

✶ 나중에 로드되는 파일이나, 방문하려는 링크의 대상

말그대로 DNS(Domain name system) 조회 시간을 줄여주는 것이다. 조회 방법은 생각보다 까다로울 수 있다. 자세한 건 이걸 참고 [DNS 동작 방식]

 교차 출처 도메인의 리소스를 참조할 때마다 <head> 요소에 삽입해주어야한다.

 HTTP link field를 사용하여 HTTP 헤더로 지정할 수 있다.

Link: <https://fonts.googleapis.com/>; rel=dns-prefetch

preconnect와 같이 사용하는 것도 추천한다. TCP, TLS 과정도 포함되어 교차 출처 요청으로 인한 지연을 더욱 줄일 수 있음.

<link rel="preconnect" href="https://fonts.googleapis.com/" crossorigin />
<link rel="dns-prefetch" href="https://fonts.googleapis.com/" />

⚠️ DNS 조회에만 효과적이므로 사이트나 도메인을 가리키는 데에 사용하지 말 것.

⚠️ 모든 도메인을 미리 연결하려 하지 말 것. 가장 중요한 연결을 생각해보기.

 

preconnect

<link rel="preconnect" href="https://example.com">

현재 페이지에서 외부 도메인의 리소스를 참고하는 것을 브라우저에게 알려 미리 외부 도메인과 연결을 설정할 수 있게 한다.

브라우저가 사이트에 필요한 연결을 미리 예상할 수 있게 된다. 브라우저는 필요한 소켓을 미리 설정할 수 있기 때문에 실제로 서버에서 리소스를 로드할 때 필요한 DNS, TCP, TLS 왕복에 필요한 시간을 절약할 수 있게 되는 것이다.

 

⚠️ 외부 도메인과 연결을 구축하기 때문에 많은 CPU 시간을 차지할 수 있고 보안 연결의 경우 더욱 추가된다. 10초 이내로 브라우저가 닫힌다면, 이전의 모든 연결 작업은 낭비가 되는 것이기 때문에 빨리 닫힐 수 있는 페이지에는 사용하지 않는 게 좋다.

 

정확한 경로(요청 URL)를 알 수 없을 때 

리소스를 가져오지 않아도 서버에 미리 연결하여 연결에 필요한 시간을 절약할 수 있다.

 

➡️ link 태그를 문서에 추가하는 대신 link HTML 응답과 함께 HTTP 응답 헤더를 사용할 수도 있다.

Link: <https://www.nami.com>; rel="preconnect", </script.js>; rel="preload"t; as="script"

 

위의 것을 잘 활용하기 위해선 기본적인 리소스 우선순위를 먼저 숙지해야 할 것이다.

리소스 우선순위 (Chrome)

Resource Fetch Prioritization and Scheduling in Chromium

Chrome은 resource를 2단계로 로드한다.

첫 번째는 Tight mode, 그 이후는 로딩 단계이다. 본문(Body)이 문서에 연결될 때 까지(모든 head 내부 스크립트가 실행된 후) 낮은 우선순위 리소스를 로딩하는 걸 제한한다. 낮은 우선순위 리소스는 해당 리소스가 발견된 시점에 처리 중인 요청이 2개 미만인 경우에만 로드된다.

 

<link rel="preload" href="/image.png" as="image" fetchpriority="high">

각 리소스 유형에는 기본 우선순위가 있으며, fetchpriority 속성을 high(높음), low(낮음) 또는 auto(기본값)로 설정하여 그 우선순위를 개발자가 사용하여 조절할 수 있다. 

같은 우선 순위 레벨의 리소스는 발견된 순서대로 우선순위를 부여받는다.

 

◉ : fetchpriority="auto" (기본값)

⬆ : fetchpriority="high" (높음)

⬇ : fetchpriority="low" (낮음)

 

Resource Fetch Prioritization and Scheduling in Chromium

“as” attribute를 사용하여 preloading되는 리소스들은 요청되는 리소스의 타입과 같은 우선권을 부여받는다. 예를 들어, preload as =“style”은 가장 높은 우선권을 갖는 반면 as=“script”는 low 혹은 medium priority를 갖는다.

✶ "as"가 없으면 XHR처럼 동작한다.

✶ early는 미리 로드되지 않은 이미지가 요청되기 전에 요청되는 것으로 정의된다.

✶ 첫 이미지(문서 내 앞쪽 이미지)전에 요청되는 Blocking script는 Medium.

✶ 첫 이미지 이후로 요청되는 Blocking  script는 Low.

✶ (문서 내 위치에 상관없이) async/defer/injected script는 Lowest

✶ 이미지들은 낮은 우선권으로 시작하고 layout 완료시 뷰포트 내에서 발견되면 우선권이 올라간다.

자원이 어떤 우선권으로 로딩되었는지 궁금하다면 DevTools Network section의 Timeline/Performance에서 찾을 수 있다.

DevTools Network 영역에서 Priority 확인하기

 

실질적인 케이스는 프로젝트에 적용해서 테스트 해보겠다! 뚜렷한 변화가 있다면 블로그에 올릴 예정.

preload나 prefetch될 때 태그 옵션에 따라 캐싱되는 위치가 달라지는 것도 추가적으로 올리겠다.

Next.js의 라우팅과도 연관 지을 수 있어서 이것 또한 추가하겠다. 확실히 렌더링과 관련있어서 뻗어나가는 가지가 많은 듯하다.


 

[Preload critical assets to improve loading speed]

[Resource Hint]

[스크립트의 실행 시점을 조절하는 Async와 Defer 속성]

[필수 원본에 미리 연결]

[Resource Fetch Prioritization and Scheduling in Chromium]

[Preload key requests]

[<script>: The Script element]

[Preload, Prefetch And Priorities in Chrome]

[Browser Resource Hints: preload, prefetch, and preconnect]

[Exploring Differences Between HTTP Preload vs HTTP/2 Push]

[[Browser] 리소스 우선순위 - preload, preconnect, prefetch]

[Using dns-prefetch]