소개
이 도움말에서는 브라우저에 JavaScript를 로드하고 실행하는 방법을 설명합니다.
아니요, 잠깐만요. 다시 돌아오세요. 지루하고 단순해 보이지만 이 작업은 이론적으로 간단한 작업이 기존 방식으로 인해 버그가 되는 브라우저에서 이루어진다는 점을 기억하세요. 이러한 특성을 알면 가장 빠르고 방해가 되지 않는 스크립트 로드 방법을 선택할 수 있습니다. 시간이 부족한 경우 빠른 참조로 건너뛰세요.
먼저 사양에서 스크립트를 다운로드하고 실행하는 다양한 방법을 정의하는 방법은 다음과 같습니다.

모든 WHATWG 사양과 마찬가지로 처음에는 스크래블 공장에서 클러스터 폭탄이 터진 후의 모습처럼 보이지만 5번째 읽고 눈에서 피를 닦아내면 꽤 흥미롭습니다.
첫 번째 스크립트 포함
<script src="//other-domain.com/1.js"></script>
<script src="2.js"></script>
아, 행복한 단순함. 여기서 브라우저는 두 스크립트를 동시에 다운로드하고 순서를 유지하면서 최대한 빨리 실행합니다. '2.js'는 '1.js'가 실행될 때까지 실행되지 않으며 (또는 실행되지 않을 때까지), '1.js'는 이전 스크립트 또는 스타일시트가 실행될 때까지 실행되지 않습니다.
안타깝게도 이 모든 작업이 진행되는 동안 브라우저는 페이지의 추가 렌더링을 차단합니다. 이는 document.write
와 같이 파서가 처리하는 콘텐츠에 문자열을 추가할 수 있는 '웹의 첫 번째 시대'의 DOM API 때문입니다. 최신 브라우저는 백그라운드에서 계속 문서를 스캔하거나 파싱하고 필요한 외부 콘텐츠 (js, 이미지, css 등)의 다운로드를 트리거하지만 렌더링은 계속 차단됩니다.
따라서 성능 전문가들은 스크립트 요소를 문서 끝에 배치하는 것이 좋습니다. 이렇게 하면 콘텐츠가 최대한 적게 차단되기 때문입니다. 안타깝게도 브라우저가 모든 HTML을 다운로드할 때까지 스크립트가 표시되지 않으며, 그때쯤에는 CSS, 이미지, iframe과 같은 다른 콘텐츠가 다운로드되기 시작합니다. 최신 브라우저는 이미지보다 JavaScript를 우선시할 만큼 똑똑하지만 더 나은 방법이 있습니다.
IE님, 감사합니다. (아니요, 비꼬는 게 아닙니다.)
<script src="//other-domain.com/1.js" defer></script>
<script src="2.js" defer></script>
Microsoft는 이러한 성능 문제를 인식하고 Internet Explorer 4에 '지연'을 도입했습니다. 이는 기본적으로 'document.write
과 같은 것을 사용하여 파서에 항목을 삽입하지 않겠다고 약속합니다. 이 약속을 어기면 원하는 대로 처벌해도 됩니다." 이 속성은 HTML4에 포함되어 다른 브라우저에 표시되었습니다.
위의 예에서 브라우저는 두 스크립트를 동시에 다운로드하고 DOMContentLoaded
이 실행되기 직전에 실행하여 순서를 유지합니다.
양 공장의 클러스터 폭탄처럼 '지연'은 엉망이 되었습니다. 'src' 및 'defer' 속성, 스크립트 태그와 동적으로 추가된 스크립트 간에 스크립트를 추가하는 6가지 패턴이 있습니다. 물론 브라우저가 실행해야 하는 순서에 동의하지 않았습니다. 2009년 당시 Mozilla에서 이 문제에 관한 훌륭한 글을 작성했습니다.
WHATWG는 동적으로 추가되었거나 'src'가 없는 스크립트에 'defer'가 영향을 미치지 않는다고 선언하여 동작을 명시적으로 만들었습니다. 그렇지 않으면 지연된 스크립트는 문서가 파싱된 후 추가된 순서대로 실행되어야 합니다.
IE님, 감사합니다. (이제 비꼬는 거예요)
주기도 하고 빼앗기도 합니다. 안타깝게도 IE4~9에는 스크립트가 예상치 못한 순서로 실행될 수 있는 심각한 버그가 있습니다. 다음과 같이 진행됩니다.
1.js
console.log('1');
document.getElementsByTagName('p')[0].innerHTML = 'Changing some content';
console.log('2');
2.js
console.log('3');
페이지에 단락이 있다고 가정하면 예상되는 로그 순서는 [1, 2, 3]이지만 IE9 이하에서는 [1, 3, 2]가 표시됩니다. 특정 DOM 작업으로 인해 IE는 현재 스크립트 실행을 일시중지하고 계속하기 전에 다른 대기 중인 스크립트를 실행합니다.
그러나 IE10 및 기타 브라우저와 같이 버그가 없는 구현에서도 전체 문서가 다운로드되고 파싱될 때까지 스크립트 실행이 지연됩니다. 어쨌든 DOMContentLoaded
를 기다릴 예정이라면 편리할 수 있지만 성능을 극대화하려면 리스너를 추가하고 부트스트랩을 더 일찍 시작할 수 있습니다.
HTML5로 문제 해결
<script src="//other-domain.com/1.js" async></script>
<script src="2.js" async></script>
HTML5에서는 document.write
를 사용하지 않을 것이라고 가정하지만 문서가 파싱될 때까지 기다리지 않고 실행하는 새로운 속성 'async'를 제공합니다. 브라우저는 두 스크립트를 동시에 다운로드하고 최대한 빨리 실행합니다.
안타깝게도 최대한 빨리 실행되므로 '2.js'가 '1.js'보다 먼저 실행될 수 있습니다. 독립적인 경우 괜찮습니다. '1.js'가 '2.js'와 관련이 없는 추적 스크립트일 수 있습니다. 하지만 '1.js'가 '2.js'가 종속된 jQuery의 CDN 사본인 경우 페이지가 오류로 뒤덮입니다. 마치… 뭐랄까… 이건 설명할 방법이 없네요.
필요한 게 뭐지? JavaScript 라이브러리!
가장 중요한 것은 렌더링을 차단하지 않고 즉시 다운로드된 스크립트 세트를 추가된 순서대로 최대한 빨리 실행하는 것입니다. 안타깝게도 HTML은 이를 허용하지 않습니다.
이 문제는 JavaScript의 여러 버전으로 해결되었습니다. 일부 라이브러리는 JavaScript를 변경하여 라이브러리가 올바른 순서로 호출하는 콜백으로 래핑해야 했습니다 (예: RequireJS). 다른 방법은 XHR을 사용하여 동시에 다운로드한 다음 올바른 순서로 eval()
를 사용하는 것이었습니다. 이 방법은 CORS 헤더가 있고 브라우저에서 이를 지원하지 않는 한 다른 도메인의 스크립트에는 작동하지 않았습니다. 일부는 LabJS와 같은 초강력 해킹을 사용하기도 했습니다.
이 해킹은 완료 시 이벤트를 트리거하지만 실행하지 않는 방식으로 브라우저를 속여 리소스를 다운로드하도록 하는 것을 포함합니다. LabJS에서는 스크립트가 잘못된 mime 유형(예: <script type="script/cache" src="...">
)으로 추가됩니다. 모든 스크립트가 다운로드되면 올바른 유형으로 다시 추가되어 브라우저가 캐시에서 바로 가져와 순서대로 즉시 실행할 수 있습니다. 이는 편리하지만 지정되지 않은 동작에 의존했으며 HTML5에서 브라우저가 인식할 수 없는 유형의 스크립트를 다운로드해서는 안 된다고 선언했을 때 중단되었습니다. LabJS는 이러한 변경사항에 적응했으며 이제 이 도움말의 메서드를 조합하여 사용합니다.
그러나 스크립트 로더에는 자체적인 성능 문제가 있습니다. 라이브러리의 JavaScript가 다운로드되고 파싱될 때까지 기다려야 관리하는 스크립트를 다운로드할 수 있습니다. 또한 스크립트 로더는 어떻게 로드하나요? 스크립트 로더에 무엇을 로드할지 알려주는 스크립트를 어떻게 로드할까요? 누가 파수꾼을 지켜보나요? 왜 알몸인가요? 모두 어려운 질문입니다.
기본적으로 다른 스크립트를 다운로드하기 전에 추가 스크립트 파일을 다운로드해야 한다면 성능 면에서 이미 패배한 것입니다.
DOM으로 문제 해결
답은 실제로 HTML5 사양에 있지만 스크립트 로드 섹션 하단에 숨겨져 있습니다.
'지구인'으로 번역해 보겠습니다.
[
'//other-domain.com/1.js',
'2.js'
].forEach(function(src) {
var script = document.createElement('script');
script.src = src;
document.head.appendChild(script);
});
동적으로 생성되어 문서에 추가된 스크립트는 기본적으로 비동기식입니다. 렌더링을 차단하지 않으며 다운로드되는 즉시 실행되므로 잘못된 순서로 나올 수 있습니다. 하지만 비동기 방식이 아님을 명시적으로 표시할 수 있습니다.
[
'//other-domain.com/1.js',
'2.js'
].forEach(function(src) {
var script = document.createElement('script');
script.src = src;
script.async = false;
document.head.appendChild(script);
});
이렇게 하면 스크립트에 일반 HTML로는 얻을 수 없는 다양한 동작이 포함됩니다. async가 명시적으로 false이므로 스크립트는 첫 번째 일반 HTML 예에서 추가된 것과 동일한 실행 대기열에 추가됩니다. 그러나 동적으로 생성되므로 문서 파싱 외부에서 실행되므로 다운로드하는 동안 렌더링이 차단되지 않습니다. 비동기 스크립트 로드를 동기 XHR과 혼동하지 마세요. 이는 좋지 않습니다.
위의 스크립트는 페이지의 헤더에 인라인으로 포함되어야 하며, 점진적 렌더링을 중단하지 않고 최대한 빨리 스크립트 다운로드를 큐에 추가하고 지정된 순서대로 최대한 빨리 실행합니다. '2.js'는 '1.js'보다 먼저 다운로드할 수 있지만 '1.js'가 다운로드 및 실행에 성공하거나 실패할 때까지 실행되지 않습니다. 좋습니다. async-download이지만 ordered-execution입니다.
이 방법으로 스크립트를 로드하는 것은 Safari 5.0 (5.1은 괜찮음)을 제외하고 async 속성을 지원하는 모든 것에서 지원됩니다. 또한 async 속성을 지원하지 않는 버전은 동적으로 추가된 스크립트를 문서에 추가된 순서대로 편리하게 실행하므로 모든 버전의 Firefox 및 Opera가 지원됩니다.
스크립트를 로드하는 가장 빠른 방법이 맞나요? 그렇죠?
로드할 스크립트를 동적으로 결정하는 경우는 그렇습니다. 그렇지 않은 경우에는 그렇지 않을 수도 있습니다. 위의 예에서는 브라우저가 스크립트를 파싱하고 실행하여 다운로드할 스크립트를 찾아야 합니다. 이렇게 하면 미리 로드 스캐너에서 스크립트를 숨길 수 있습니다. 브라우저는 이러한 스캐너를 사용하여 다음에 방문할 가능성이 높은 페이지의 리소스를 찾거나 파서가 다른 리소스에 의해 차단된 동안 페이지 리소스를 찾습니다.
문서 헤더에 다음을 추가하여 검색 가능성을 다시 추가할 수 있습니다.
<link rel="subresource" href="//other-domain.com/1.js">
<link rel="subresource" href="2.js">
이렇게 하면 브라우저에 페이지에 1.js와 2.js가 필요하다고 알립니다. link[rel=subresource]
는 link[rel=prefetch]
와 유사하지만 시맨틱스가 다릅니다. 안타깝게도 현재 Chrome에서만 지원되며 로드할 스크립트를 링크 요소를 통해 한 번, 스크립트에서 한 번 더 선언해야 합니다.
수정사항: 원래는 이러한 항목이 미리 로드 스캐너에 의해 선택된다고 말했으나, 사실은 그렇지 않습니다. 이러한 항목은 일반 파서에 의해 선택됩니다. 그러나 프리로드 스캐너는 이를 선택할 수 있지만 아직 선택하지는 않습니다. 반면 실행 파일 코드에 포함된 스크립트는 프리로드할 수 없습니다. 댓글에서 오류를 지적해 주신 Yoav Weiss님께 감사드립니다.
이 도움말이 우울하게 느껴집니다.
우울한 상황이며 우울한 기분이 들 수 있습니다. 실행 순서를 제어하면서 스크립트를 빠르고 비동기식으로 다운로드하는 반복적이지 않으면서 선언적인 방법은 없습니다. HTTP2/SPDY를 사용하면 개별적으로 캐시할 수 있는 여러 개의 작은 파일로 스크립트를 전송하는 것이 가장 빠른 방법이 될 수 있을 정도로 요청 오버헤드를 줄일 수 있습니다. 상상해 보세요.
<script src="dependencies.js"></script>
<script src="enhancement-1.js"></script>
<script src="enhancement-2.js"></script>
<script src="enhancement-3.js"></script>
…
<script src="enhancement-10.js"></script>
각 개선 스크립트는 특정 페이지 구성요소를 처리하지만 dependencies.js의 유틸리티 함수가 필요합니다. 이상적으로는 모든 항목을 비동기식으로 다운로드한 다음 dependencies.js 다음에 어떤 순서든 관계없이 최대한 빨리 개선 스크립트를 실행하는 것이 좋습니다. 점진적 점진적 개선입니다. 안타깝게도 dependencies.js의 로드 상태를 추적하도록 스크립트 자체를 수정하지 않는 한 이를 선언적으로 실행할 방법은 없습니다. async=false도 이 문제를 해결하지 못합니다. enhancement-10.js 실행이 1~9에서 차단되기 때문입니다. 실제로 해킹 없이 이 작업을 할 수 있는 브라우저는 하나뿐입니다.
IE에 아이디어가 있습니다.
IE는 다른 브라우저와 다르게 스크립트를 로드합니다.
var script = document.createElement('script');
script.src = 'whatever.js';
이제 IE에서 'whatever.js' 다운로드를 시작하고 다른 브라우저는 스크립트가 문서에 추가될 때까지 다운로드를 시작하지 않습니다. IE에는 로드 진행률을 알려주는 'readystatechange' 이벤트와 'readystate' 속성도 있습니다. 이는 스크립트의 로드와 실행을 독립적으로 제어할 수 있으므로 매우 유용합니다.
var script = document.createElement('script');
script.onreadystatechange = function() {
if (script.readyState == 'loaded') {
// Our script has download, but hasn't executed.
// It won't execute until we do:
document.body.appendChild(script);
}
};
script.src = 'whatever.js';
문서에 스크립트를 추가할 시기를 선택하여 복잡한 종속 항목 모델을 빌드할 수 있습니다. IE는 버전 6부터 이 모델을 지원했습니다. 꽤 흥미롭지만 여전히 async=false
와 동일한 로더 탐색 가능성 문제가 있습니다.
그만! 스크립트를 로드하려면 어떻게 해야 하나요?
알겠습니다. 렌더링을 차단하지 않고, 반복되지 않으며, 우수한 브라우저 지원을 받는 방식으로 스크립트를 로드하려면 다음을 제안합니다.
<script src="//other-domain.com/1.js"></script>
<script src="2.js"></script>
그거. body 요소의 끝 예, 웹 개발자는 시시포스 왕과 매우 흡사합니다. 그리스 신화 언급으로 힙스터 포인트 100점!). HTML 및 브라우저의 제한으로 인해 더 나은 성능을 제공할 수 없습니다.
스크립트를 로드하고 실행 순서를 제어하는 선언적 비차단 방식을 제공하는 JavaScript 모듈이 스크립트를 모듈로 작성해야 하지만 이 문제를 해결해 주기를 바랍니다.
으악, 지금 더 나은 방법이 있을 텐데.
물론, 성능을 극대화하고자 하며 약간의 복잡성과 반복이 문제가 되지 않는다면 위의 몇 가지 트릭을 결합해 보세요.
먼저 미리 로드기에 대한 하위 리소스 선언을 추가합니다.
<link rel="subresource" href="//other-domain.com/1.js">
<link rel="subresource" href="2.js">
그런 다음 문서 헤드의 인라인에서 async=false
를 사용하여 JavaScript로 스크립트를 로드하고, IE의 readystate 기반 스크립트 로드로 대체하고, 지연으로 대체합니다.
var scripts = [
'1.js',
'2.js'
];
var src;
var script;
var pendingScripts = [];
var firstScript = document.scripts[0];
// Watch scripts load in IE
function stateChange() {
// Execute as many scripts in order as we can
var pendingScript;
while (pendingScripts[0] && pendingScripts[0].readyState == 'loaded') {
pendingScript = pendingScripts.shift();
// avoid future loading events from this script (eg, if src changes)
pendingScript.onreadystatechange = null;
// can't just appendChild, old IE bug if element isn't closed
firstScript.parentNode.insertBefore(pendingScript, firstScript);
}
}
// loop through our script urls
while (src = scripts.shift()) {
if ('async' in firstScript) { // modern browsers
script = document.createElement('script');
script.async = false;
script.src = src;
document.head.appendChild(script);
}
else if (firstScript.readyState) { // IE<10
// create a script and add it to our todo pile
script = document.createElement('script');
pendingScripts.push(script);
// listen for state changes
script.onreadystatechange = stateChange;
// must set src AFTER adding onreadystatechange listener
// else we'll miss the loaded event for cached scripts
script.src = src;
}
else { // fall back to defer
document.write('<script src="' + src + '" defer></'+'script>');
}
}
몇 가지 트릭과 축소를 거치면 362바이트 + 스크립트 URL이 됩니다.
!function(e,t,r){function n(){for(;d[0]&&"loaded"==d[0][f];)c=d.shift(),c[o]=!i.parentNode.insertBefore(c,i)}for(var s,a,c,d=[],i=e.scripts[0],o="onreadystatechange",f="readyState";s=r.shift();)a=e.createElement(t),"async"in i?(a.async=!1,e.head.appendChild(a)):i[f]?(d.push(a),a[o]=n):e.write("<"+t+' src="'+s+'" defer></'+t+">"),a.src=s}(document,"script",[
"//other-domain.com/1.js",
"2.js"
])
간단한 스크립트 포함과 비교하여 추가 바이트가 가치가 있나요? 이미 JavaScript를 사용하여 조건부로 스크립트를 로드하고 있다면(BBC의 경우와 같이) 이러한 다운로드를 더 일찍 트리거하는 것이 좋습니다. 그렇지 않은 경우에는 간단한 본문 끝 메서드를 사용하세요.
휴, 이제 WHATWG 스크립트 로드 섹션이 왜 그렇게 방대한지 알겠습니다. 음료가 필요합니다.
빠른 참조
일반 스크립트 요소
<script src="//other-domain.com/1.js"></script>
<script src="2.js"></script>
사양에 따르면: 함께 다운로드하고 대기 중인 CSS가 있으면 순서대로 실행하고 완료될 때까지 렌더링을 차단합니다. 브라우저: 예, 알겠습니다.
지연
<script src="//other-domain.com/1.js" defer></script>
<script src="2.js" defer></script>
사양에 따르면: 함께 다운로드하고 DOMContentLoaded 직전에 순서대로 실행합니다. 'src'가 없는 스크립트에서 'defer'를 무시합니다. IE 10 미만: 1.js 실행 중간에 2.js를 실행할 수 있습니다. 재미있지 않나요? 빨간색 브라우저: 이 '지연'이 무엇인지 모르겠으므로 스크립트가 없는 것처럼 로드합니다. 다른 브라우저: 좋습니다. 하지만 'src'가 없는 스크립트에서는 'defer'를 무시하지 않을 수도 있습니다.
비동기식
<script src="//other-domain.com/1.js" async></script>
<script src="2.js" async></script>
사양에 명시된 내용: 함께 다운로드하고 다운로드된 순서대로 실행합니다. 빨간색 브라우저: 'async'가 뭐야? 없는 것처럼 스크립트를 로드하겠습니다. 다른 브라우저: 예, 알겠습니다.
비동기 false
[
'1.js',
'2.js'
].forEach(function(src) {
var script = document.createElement('script');
script.src = src;
script.async = false;
document.head.appendChild(script);
});
사양에 명시된 내용: 함께 다운로드하고 모두 다운로드되는 즉시 순서대로 실행합니다. Firefox 3.6 미만, Opera: 이 'async'가 무엇인지 모르겠지만, JS를 통해 추가된 스크립트를 추가된 순서대로 실행합니다. Safari 5.0: 'async'는 이해하지만 JS로 'false'로 설정하는 것은 이해하지 못합니다. 스크립트가 도착하는 즉시 순서와 관계없이 실행합니다. IE 10 미만: 'async'에 관해 잘 모르지만 'onreadystatechange'를 사용하는 해결 방법이 있습니다. 기타 빨간색 브라우저: 이 'async'를 이해하지 못합니다. 스크립트가 도착하는 즉시 어떤 순서든지 실행합니다. 기타 모든 항목: 저는 고객님의 친구입니다. 규정을 준수하여 처리하겠습니다.