게시일: 2013년 12월 31일
JavaScript를 사용하면 콘텐츠, 스타일 지정, 사용자 상호작용에 대한 응답 등 페이지의 거의 모든 측면을 수정할 수 있습니다. 하지만, JavaScript는 DOM 생성을 차단하고 페이지가 렌더링될 때 지연시킬 수도 있습니다. 최적의 JavaScript를 비동기로 설정하고 불필요한 JavaScript를 제거하여 미리 볼 수 있습니다.
요약
- JavaScript는 DOM 및 CSSOM을 쿼리하고 수정할 수 있습니다.
- JavaScript 실행은 CSSOM을 차단합니다.
- JavaScript는 명시적으로 비동기로 선언되지 않는 한 DOM 생성을 차단합니다.
JavaScript는 브라우저에서 실행되는 동적 언어이며 페이지 동작 방식의 거의 모든 측면을 변경할 수 있습니다. DOM 트리에서 요소를 추가 및 제거하여 콘텐츠를 수정할 수 있습니다. 각 요소의 CSSOM 속성을 수정할 수 있습니다. 사용자 입력을 처리할 수 있습니다. 기타 등등 이를 보여주기 위해 이전의 'Hello World' 예시를 변경하여 짧은 인라인 스크립트를 추가하면 어떻게 되는지 살펴보겠습니다.
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<link href="style.css" rel="stylesheet" />
<title>Critical Path: Script</title>
</head>
<body>
<p>Hello <span>web performance</span> students!</p>
<div><img src="awesome-photo.jpg" /></div>
<script>
var span = document.getElementsByTagName('span')[0];
span.textContent = 'interactive'; // change DOM text content
span.style.display = 'inline'; // change CSSOM property
// create a new element, style it, and append it to the DOM
var loadTime = document.createElement('div');
loadTime.textContent = 'You loaded this page on: ' + new Date();
loadTime.style.color = 'blue';
document.body.appendChild(loadTime);
</script>
</body>
</html>
JavaScript를 사용하면 DOM에 접근하고 숨겨진 span 노드에 대한 참조를 가져올 수 있습니다. 이러한 숨겨진 노드는 렌더링 트리에 표시되지 않지만, DOM에는 여전히 존재합니다. 그런 다음 참조를 가져오면 해당 텍스트를 변경하고(.textContent를 통해) 계산된 디스플레이 스타일 속성을 'none'에서 'inline'으로 재정의할 수도 있습니다. 이제 페이지에 'Hello interactive students!'가 표시됩니다.
JavaScript를 사용하면 DOM에서 새로운 요소를 생성, 추가, 제거하고 이 요소의 스타일을 지정할 수 있습니다. 기술적으로 볼 때, 전체 페이지는 요소를 하나씩 생성하고 이 요소의 스타일을 지정하는 하나의 커다란 자바스크립트 파일일 수 있습니다. 이 파일도 작동하기는 하지만 실제로는 HTML 및 CSS를 이용하는 것이 휠씬 더 쉽습니다. JavaScript 함수의 두 번째 부분에서 새 div 요소를 만들고, 텍스트 콘텐츠를 설정하고, 스타일을 지정하고, 본문에 추가합니다.
이를 사용하여 기존 DOM 노드의 콘텐츠와 CSS 스타일을 수정하고 완전히 새로운 노드를 문서에 추가했습니다. 이 페이지는 어떠한 디자인 상도 수상하지는 못하지만 JavaScript가 제공하는 강력한 성능과 유연성을 보여줍니다.
그러나 JavaScript는 많은 기능을 제공하지만 페이지를 렌더링하는 방법과 시기에 많은 추가적인 제한을 가합니다.
먼저, 이전 예시에서 인라인 스크립트가 페이지의 맨 아래 부근에 있는 것을 확인할 수 있습니다. 왜냐하면 직접 해봐야 하지만 <span>
요소 위로 스크립트를 이동하면 스크립트가 실패하고 문서에서 <span>
요소에 대한 참조를 찾을 수 없다고 불평합니다. 즉, getElementsByTagName('span')
는 null
을 반환합니다. 이는 스크립트가 문서에 삽입된 정확한 지점에서 실행된다는 중요한 속성을 보여줍니다. HTML 파서는 스크립트 태그를 만나면 DOM 생성 프로세스를 일시중지하고 JavaScript 엔진에 제어권을 양도합니다. JavaScript 엔진이 실행을 완료한 후 브라우저가 중단한 부분부터 다시 시작하여 DOM 생성을 재개합니다.
다시 말해, 스크립트 블록에서는 요소가 아직 처리되지 않았기 때문에 페이지의 뒷부분에서 어떠한 요소도 찾을 수 없습니다. 즉, 인라인 스크립트를 실행하면 DOM 생성이 차단되어 초기 렌더링도 지연됩니다.
페이지에 스크립트를 도입할 때의 또 다른 미묘한 속성은 스크립트가 DOM뿐만 아니라 CSSOM 속성도 읽고 수정할 수 있다는 점입니다. 실제로 이 예에서 span 요소의 표시 속성을 none에서 인라인으로 변경할 때 이렇게 하고 있습니다. 결과는? 이제 경합 상태가 생겼습니다.
스크립트를 실행하려고 할 때 브라우저가 CSSOM을 다운로드하고 빌드하는 작업을 완료하지 않았다면 어떻게 될까요? 이 답은 성능 면에서 그다지 좋지 않습니다. 브라우저는 CSSOM을 다운로드하고 구성을 완료할 때까지 스크립트 실행 및 DOM 생성을 지연시킵니다.
간단히 말해서, JavaScript는 DOM, CSSOM 및 JavaScript 실행 간에 여러 가지 새로운 종속성을 도입합니다. 이 때문에 브라우저가 화면에서 페이지를 처리하고 렌더링할 때 상당한 지연이 발생할 수 있습니다.
- 문서에서 스크립트의 위치는 중요합니다.
- 브라우저가 스크립트 태그를 만나면 이 스크립트가 실행 종료될 때까지 DOM 생성이 일시 중지됩니다.
- JavaScript는 DOM 및 CSSOM을 쿼리하고 수정할 수 있습니다.
- JavaScript 실행은 CSSOM이 준비될 때까지 일시 중지됩니다.
크게 말하자면, "주요 렌더링 경로 최적화" HTML, CSS 및 JavaScript 간의 종속성 그래프를 이해하고 최적화하는 것을 의미합니다.
파서 차단 대 비동기 JavaScript
기본적으로 JavaScript 실행은 '파서 차단'입니다. 브라우저가 문서에서 스크립트를 만나면 DOM 생성을 일시중지하고, JavaScript 런타임에 제어 권한을 넘겨 스크립트가 실행되도록 한 후 DOM 생성을 계속합니다. 앞의 예시에서 인라인 스크립트를 통해 이러한 동작이 실행되는 것을 확인했습니다. 사실, 실행을 지연시킬 추가 코드를 작성하지 않는 한, 인라인 스크립트는 항상 파서를 차단합니다.
스크립트 태그를 사용하여 포함된 스크립트는 어떨까요? 이전 예를 사용하여 코드를 별도의 파일로 추출합니다.
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<link href="style.css" rel="stylesheet" />
<title>Critical Path: Script External</title>
</head>
<body>
<p>Hello <span>web performance</span> students!</p>
<div><img src="awesome-photo.jpg" /></div>
<script src="app.js"></script>
</body>
</html>
app.js
var span = document.getElementsByTagName('span')[0];
span.textContent = 'interactive'; // change DOM text content
span.style.display = 'inline'; // change CSSOM property
// create a new element, style it, and append it to the DOM
var loadTime = document.createElement('div');
loadTime.textContent = 'You loaded this page on: ' + new Date();
loadTime.style.color = 'blue';
document.body.appendChild(loadTime);
<script> 태그를 사용하든 인라인 JavaScript 스니펫을 사용하든 간에 둘 다 동일한 방식으로 동작할 것으로 기대합니다. 두 경우 모두 브라우저가 일시중지되고 스크립트를 실행하기 전에 문서의 나머지 부분을 처리할 수 있습니다. 하지만 외부 자바스크립트 파일의 경우 브라우저가 잠시 멈추고 디스크, 캐시 또는 원격 서버에서 스크립트를 가져올 때까지 기다린 다음 중요한 렌더링에 수만 밀리초의 지연 시간을 살펴봤습니다
기본적으로 모든 JavaScript는 파서를 차단합니다. 브라우저는 스크립트가 페이지에서 무엇을 할지 모르기 때문에, 최악의 시나리오를 가정하고 파서를 차단합니다. 스크립트가 참조되는 바로 그 지점에서 이 스크립트를 실행할 필요가 없음을 브라우저에 신호로 알려준다면, 브라우저가 계속해서 DOM을 구성할 수 있고 준비가 끝난 후에 스크립트를 실행할 수 있습니다(예: 파일을 캐시나 원격 서버에서 가져온 후에 스크립트를 실행).
이를 위해 async
속성을 <script>
요소에 추가합니다.
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<link href="style.css" rel="stylesheet" />
<title>Critical Path: Script Async</title>
</head>
<body>
<p>Hello <span>web performance</span> students!</p>
<div><img src="awesome-photo.jpg" /></div>
<script src="app.js" async></script>
</body>
</html>
async 키워드를 스크립트 태그에 추가하면 스크립트가 사용 가능해질 때까지 기다리는 동안 DOM 생성을 차단하지 말라고 브라우저에 지시하는 것입니다. 이 경우 성능이 크게 향상됩니다.