개요
최신 웹 사이트에서 JavaScript(이하 스크립트)는 종종 HTML보다 "무거워"집니다. 다운로드 크기가 크고 처리 시간도 더 깁니다. 따라서 스크립트를 HTML 페이지에 불러올 때는 페이지의 로딩 속도에 악영향을 주지 않도록 주의해야 합니다.
외부 스크립트는 다음과 같이 HTML 페이지에 삽입합니다.
<script src="script.js"></script>
스크립트를 HTML 페이지의 어디에서 어떻게 불러오는지에 따라 로딩시간은 영향을 받게 됩니다. HTML 파서(parser)가 이 부분을 만나면 DOM을 생성하는 작업을 멈추고 스크립트 파일(script.js
)을 다운로드해 실행하는 동작이 먼저 수행됩니다.
일단 이것을 처리하는 것이 완료되어야 비로소 나머지 HTML 페이지에 대한 parsing 작업이 재개됩니다.
쉽게 예상할 수 있듯이 이런 동작은 페이지의 로딩시간에 큰 영향을 끼칩니다.
혹시 스크립트를 불러들이는데 시간이 오래 걸린다면 사용자는 스크립트의 처리가 완료될 때까지 빈 웹브라우저 화면만 볼 수밖에 없습니다. 때로는 사용자가 기다리지 못하고 떠나게 됩니다.
스크립트를 불러오는 위치
HTML에 스크립트를 추가할 수 있는 곳은 여러곳이 있습니다. 다음과 같이 <head>
태그 내부에서 외부 스크립트를 불러오는 것은 가장 기본적으로 사용되는 방법입니다.
<html>
<head>
<title>Title</title>
<script src="script.js"></script>
</head>
<body>
...
</body>
</html>
하지만 웹브라우저의 HTML 파서(parser)가 이 부분에 도달하면 스크립트를 먼저 처리한 후, 그 아래의 <body>
태그를 파싱(parse)합니다.
이것은 화면을 표시하기 위한 DOM을 생성하는 과정에서 지연이 발생하기 때문에 사용자 경험 측면에서 바람직한 상황이 아닙니다. 이에 대한 일반적인 해법은 DOM을 먼저 생성하도록 script
태그를 페이지의 하단, 즉 </body>
직전에 삽입하는 것입니다.
이렇게 하면 스트립트를 다운로드하고 실행하는 일이 모든 페이지가 파싱 되고 로딩된 후에 발생하므로 head
에 스크립트를 삽입하는 것에 비해서 화면의 렌더링 측면에서 큰 향상을 가져옵니다.
그런데 이 방법은 완벽한 해결책이 아닙니다. HTML 문서 자체가 아주 큰 경우를 가정해봅시다. 브라우저가 HTML 문서 전체를 다운로드한 다음에 스크립트를 다운로드하게 하면 페이지가 정말 느려질 겁니다.
네트워크 속도가 빠른 곳에서 페이지에 접속하고 있다면 이런 지연은 눈에 잘 띄지 않습니다. 하지만 아직도 네트워크 환경이 열악한 곳이 많습니다. 모바일 네트워크 접속이 느린 곳도 많습니다.
다행히도 이런 문제를 해결할 수 있는 <script>
속성(attributes)이 있습니다. 바로 HTML5에 추가된 async
와 defer
입니다.
Async와 Defer 속성의 사용
HTML5 규격에는 <script>
태그에서 사용할 수 있는 두 가지 새로운 부울(boolean) 속성인 async
와 defer
가 있습니다. async
와 defer
는 HTML 파싱과 스크립트 다운로딩(fetch)을 병렬로 진행하도록 합니다. async
는 스크립트가 사용가능 할때는 즉시 실행합니다. 반면 defer
는 전체 문서가 파싱(parsed) 된 후에 스크립트가 실행합니다.
async
와 defer
의 사용법은 다음과 같습니다.
<script async src="script.js"></script>
<script defer src="script.js"></script>
만약 둘 모두를 하나의 <script>
태그의 속성으로 명시되어 있다면 defer
만 지원하는 구식 브라우저의 경우를 제외하면 일반적으로 async
에 우선순위가 있습니다.
단, 이들 속성은 스크립트를 <head></head>
태그에 삽입했을 때만 의미가 있습니다. 앞서 본 것과 같이 페이지의 <body></body>
태그에 삽입한 <script>
에서는 의미가 없습니다. 이에 대해 상세히 살펴보겠습니다.
적용 케이스별 페이지 로딩 시나리오
※ 브라우저를 이용하여 진행한 시험 결과는 Async Defer — JavaScript Loading Strategies에서 제공하는 시험 페이지를 이용하여 확인해보았습니다.
※ 원글의 내용과 달리 대부분의 페이지 로딩 시간은 Image 파일의 다운로드에 좌우되어 스크립트의 위치/속성 적용에 따른 속도 차이를 확인할 근거는 없었습니다.
※ 구글의 Chrome(버전 83.0.4103.116(공식 빌드) (64비트)) 브라우저를 이용하여 동작을 직접 확인해보았습니다.
범례
- DCL(DOMContentLoaded) : HTML과 CSS 파싱이 끝나는 시점, 렌더 트리를 구성할 준비가 된(DOM 및 CSSOM 구성이 끝난) 상황이다.
- FP(First Paint) : 흰 화면에서 화면에 무언가가 처음으로 그려지기 시작하는 순간이다.
- FCP(First Contentful Paint) : 텍스트나 이미지가 출력되기 시작하는 순간이다.
- FMP(First Meaningful Paint) : 사용자에게 의미 있는 콘텐츠가 그려지기 시작하는 첫 순간이다. 콘텐츠를 노출하는데 필요한 CSS, 자바스크립트 로드가 시작되고 스타일이 적용되어 주요 콘텐츠를 읽을 수 있다.
- LCP(Largest Contentful Paint) : 가장 큰 내용 요소가 로드되는데 걸리는 시간이다.
- L(load) : HTML 상에 필요한 모든 리소스가 로드된 시점이다.
defer 또는 async 없이 <head>
에 스크립트 삽입
전통적인 방식으로 defer
또는 async
를 사용하지 않고 head
태그에서 스크립트 삽입 코드를 추가했을 때, 브라우저가 페이지를 처리하는 과정은 다음 그림과 같습니다.
파싱은 스크립트를 가져와서 실행이 완료될 때까지 중단됩니다. 스크립트의 실행이 완료되면 다시 진행됩니다.
크롬에서의 동작
- HTML head에서 스크립트 다운로드 시작, 스크립트 실행이 완료될 때까지 Parsing 중단 (DCL 시점)
- 스크립트들(file1.js, file2.js, file3.js)은 동시 다운로드(fetch)
- 스크립트의 실행은 HTML 문서에 기술된 대로 file1.js, file2.js, file3.js 실행됨
- DOM 생성전에 DOM을 조작하는 스크립트가 실행되어 오류가 발생
defer 또는 async 없이 </body>
직전에 스크립트 삽입
페이지 랜더링을 막지 않도록 </body>
태그 직전에 스크립트를 삽입한다면 페이지의 처리과정은 다음 그림과 같을 것입니다.
스크립트 처리를 위해 중단 없이 파싱이 완료된 후, 스크립트를 다운로드하고, 실행하게 됩니다. 파싱이 스크립트의 다운로드 전에 완료되므로 이전 예시와 달리 페이지가 사용자에게 빠르게 표시됩니다.
이 둘의 그래프에서 주의 깊게 볼 점은 HTML의 렌더링과 스크립트의 실행이 완료되는 시간은 동일하다는 점입니다. 다만 <head>
태그에 삽입한 스크립트는 렌더링이 중단되기 때문에 페이지의 방문자에게 빈 화면을 더 오랫동안 띄우게 될 것입니다.
크롬에서의 동작
- HTML body 마지막 부분에서 스크립트 다운로드 시작, 스크립트 실행이 완료될 때까지 Parsing 중단 (DCL 시점)
- 스크립트들(file1.js, file2.js, file3.js)은 동시 다운로드(fetch)
- 스크립트의 실행은 HTML 문서에 기술된 대로 file1.js, file2.js, file3.js 실행됨
async를 사용하며 <head>
에 스크립트 삽입
스크립트를 head
태그에 async
속성과 함께 사용한 경우는 다음과 같습니다.
비동기 방식으로 스크립트를 다운로드하고 다운로드가 모두 완료되면 HTML 파싱을 멈추고 스크립트를 실행합니다. 그리고 다시 파싱을 이어나갑니다.
크롬에서의 동작
- HTML head 부분에서 스크립트 다운로드 시작
- 스크립트들(file1.js, file2.js, file3.js)은 동시 다운로드(fetch)
- 스크립트 다운로딩으로 HTML 파싱이 중단되지 않음 (DCL 시점이 스크립트 다운로딩 완료 전에 표시됨)
- 스크립트 실행은 준비된 순서로 file2.js, file3.js, file1.js 실행됨
- 경우에 따라 DOM 생성전에 DOM을 조작하는 스크립트가 실행되어 오류가 발생
defer를 사용하며 <head>
에 스크립트 삽입
스크립트를 head
태그에 defer
속성과 함께 사용한 경우는 다음과 같습니다.
스크립트의 다운로딩은 HTML 파싱을 중단시키지 않고, HTML 파싱이 완료된 후 실행됩니다.
앞서 보았던 것과 같이 파싱이 중단되지 않고 완료됩니다. 또한 스크립트의 패치(다운로딩)가 파싱과 동시에 진행되어 패치에 소요되는 시간만큼 전체 시간도 줄어드는 것을 확인할 수 있습니다.
크롬에서의 동작
- HTML head 부분에서 스크립트 다운로드 시작
- 스크립트들(file1.js, file2.js, file3.js)은 병렬 다운로드(fetch)
- 스크립트 다운로딩으로 HTML 파싱이 중단되지 않으나 위 그림에는 표시되지 않음. (확대하여 확인 가능)
- 스크립트의 실행은 HTML 문서에 기술된 대로 file1.js, file2.js, file3.js 실행됨 (콘솔 로그 기준)
※ defer
속성으로 파싱이 완료된 후에도 스크립트가 로딩되어 실행될 때까지 DCL이 발생하지 않습니다.
로딩 시나리오 요약
파싱의 중단
일반 스크립트(classic script)와 달리 async
와 defer
속성이 있는 스크립트의 다운로드(패치)는 HTML 파싱을 중단시키지 않습니다.
모든 스크립트의 실행은 HTML 파싱을 중단시킵니다. (defer
는 파싱 후에 실행되도록 강제되어 중단시킬 수가 없을 뿐입니다.)
렌더링 블록킹
async
나 defer
모두 렌더링 차단에 대한 어떠한 것도 보장하지 않습니다. 이것은 사용자와 스크립트에 달려 있습니다. (예를 들어, 스크립트가 onLoad
이후에 실행되도록 하는 것).
실행 순서의 보장
async
속성이 추가된 스크립트는 패치되는 순서에 따라 임의로 실행되는데 반하여 defer
속성과 일반 스크립트(classic script)는 페이지에 명시된 순서대로 실행이 됩니다.
결론
일반적으로, 외부 스크립트를 로딩할 경우, 스크립트가 렌더링을 위해서 무언가 하지 않는 이상 항상 async
또는 defer
를 선택해 사용해야 합니다.
async
는 스크립트를 로딩 중간에라도 일찍 실행하는 것이 중요한 경우 사용합니다.- 좀 더 중요도가 떨어지는 스크립트는
defer
를 사용합니다.
참고
'기타' 카테고리의 다른 글
[자작 NAS] 3. TLS 인증서 발급 (0) | 2020.08.29 |
---|---|
[자작 NAS] 2. 무료 도메인 신청 및 DNS 설정 (0) | 2020.08.28 |
JavaScript로 PWA 만들기 - 캐시 업데이트 반영하기 (0) | 2020.06.17 |
유튜브 영상 오디오 추출하기 (0) | 2020.05.07 |
PythonCode_입력장치 제어 (0) | 2020.02.23 |