読み込みスクリプトの暗い海に浸る

画像

ほんの数時間前、ページへのスクリプトのロードに関する現状についてのすばらしい記事がHTML5 Rocksに掲載されました。 その翻訳をお見せします。 修正はプライベートメッセージに送信できます。



はじめに



この記事では、JavaScriptをブラウザーにロードして実行する方法を説明します。



待って、戻ってきて! 私はこれが普通で単純に聞こえることを知っていますが、これはブラウザで起こることを覚えておいてください。 これらの癖を知ることは、スクリプトをロードするための最速で破壊的な方法を選択するのに役立ちます。 お急ぎの場合は、記事の最後にあるクイックリファレンスガイドに直接進んでください。



まず第一に、これは、 仕様がスクリプトをロードおよび実行するさまざまな方法を定義する方法です。



スクリプロトのロードに関するWHATWGの記述



すべてのWHATWG仕様と同様に、この仕様は一見、スクラブルファクトリーのクラスター爆弾の影響のように見えます。 しかし、それを5回読んで、目から血を拭いた後、あなたはそれを非常に面白いと感じ始めます:



私の最初のスクリプト接続



<script src="//other-domain.com/1.js"></script> <script src="2.js"></script>
      
      





ああ、至福のシンプルさ。 この場合、ブラウザーは両方のスクリプトを並行してダウンロードし、指定された順序を保持して、できるだけ早くそれらを実行します。 「2.js」は「1.js」が実行されるまで実行されません(または実行できません)。「1.js」は前のスクリプトまたはスタイルが実行されるまで実行されません。 など



残念ながら、ブラウザはこれ以上のページレンダリングをブロックしますが、これはすべて起こります。 「Webの1世紀」以来、これはDOM APIによるものであり、パーサーが使用するコンテンツに、たとえばdocument.write



使用して、文字列を追加できます。 最新のブラウザは引き続きドキュメントをバックグラウンドでスキャンおよび解析し、必要なサードパーティコンテンツ(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に「遅延」を導入しました。基本的に、次のように述べています。 この約束を破ったら、あなたに合った方法で私を罰することができます。」 この属性はHTML4導入され 、他のブラウザーでも登場しました。



上記の例では、ブラウザーはDOMContentLoadedがDOMContentLoaded



直前に両方のスクリプトを同時にダウンロードして実行し、順序が保持されます。



羊の工場のクラスター爆弾のように、延期は毛むくじゃらの混乱になりました。 「src」と「defer」に加えて、スクリプトタグと動的にロードされるスクリプトに加えて、スクリプトを追加するための6つのパターンがあります。 当然、ブラウザは実行する順序に同意しませんでした。 Mozilla は、この問題について 2009年に素晴らしい説明をしました



WHATWGは、deferが動的に追加されたスクリプトやsrcを持たないスクリプトに影響を与えないと宣言することにより、この動作を明示的にしました。 それ以外の場合、ドキュメントを解析した後、「defer」を含むスクリプトを指定された順序で実行する必要があります。



IEに感謝します! (大丈夫、今皮肉と)


1つは与えた-別のものが撮影されました。 残念ながら、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は、新しい属性「async」を提供しました。これは、document.writeを使用していないことを前提としていますが、ドキュメントの解析が完了するのを待っていません。 ブラウザは両方のスクリプトを同時にダウンロードし、できるだけ早く実行します。



残念ながら、できるだけ早く実行しようとするため、「2.js」は「1.js」よりも早く実行される場合があります。 互いに依存していない場合、これは素晴らしいことです。 たとえば、「1.js」が「2.js」とは関係のない追跡スクリプトである場合。 しかし、「1.js」が「2.js」に依存するjQueryのCDNコピーである場合、クラスター爆弾のような...あなたのページはエラーで覆われます...わかりません...ここでは何も思いつきませんでした。



JavaScriptライブラリが必要なことは知っています!



Holy Grailには、ページのレンダリングをブロックせずにすぐにロードし、追加した順にできるだけ早く実行する一連のスクリプトが含まれています。 残念ながら、HTMLはあなたを嫌っており、これを許可しません。



この問題は、JavaScriptをさまざまな方法で使用して解決されました。 一部のメソッドでは、すべてをコールバックでラップするためにJavaScriptを変更する必要がありました。コールバックは、ライブラリが正しい順序で呼び出します(たとえば、 RequireJS )。 他のユーザーは、並列読み込みにXHRを使用し、次に正しい順序でeval()



を使用しました。これは、ブラウザーにCORSヘッダーとサポートがない限り、別のドメインのスクリプトでは機能しません。 最新のLabJSで行われたように、スーパーマジックハックを使用したものもありました。



ハッキングはあらゆる方法でブラウザをtrickしてリソースをロードさせ、ダウンロードの最後にイベントを発生させましたが、実行を開始しませんでした。 LabJSでは、スクリプトは最初に誤ったMIMEタイプで追加されました。たとえば
 .      ,   ,     mime-,    ,             .     ,   ,  ,  HTML5 ,         . 
      



, , JavaScript- , . , ? , ? ? ? .



DOM !

, HTML5, .

The async IDL attribute controls whether the element will execute asynchronously or not. If the element's "force-async" flag is set, then, on getting, the async IDL attribute must return true, and on setting, the "force-async" flag must first be unset

" ":

[ '//other-domain.com/1.js', '2.js' ].forEach(function(src) { var script = document.createElement('script'); script.src = src; document.head.appendChild(script); });








. , , mime-, , . , , , HTML5 , .



, , JavaScript- , . , ? , ? ? ? .







DOM !

, HTML5, .

The async IDL attribute controls whether the element will execute asynchronously or not. If the element's "force-async" flag is set, then, on getting, the async IDL attribute must return true, and on setting, the "force-async" flag must first be unset

" ":

[ '//other-domain.com/1.js', '2.js' ].forEach(function(src) { var script = document.createElement('script'); script.src = src; document.head.appendChild(script); });




. , , mime-, , . , , , HTML5 , .



, , JavaScript- , . , ? , ? ? ? .







DOM !

, HTML5, .

The async IDL attribute controls whether the element will execute asynchronously or not. If the element's "force-async" flag is set, then, on getting, the async IDL attribute must return true, and on setting, the "force-async" flag must first be unset

" ":

[ '//other-domain.com/1.js', '2.js' ].forEach(function(src) { var script = document.createElement('script'); script.src = src; document.head.appendChild(script); });




. , , mime-, , . , , , HTML5 , .



, , JavaScript- , . , ? , ? ? ? .







DOM !

, HTML5, .

The async IDL attribute controls whether the element will execute asynchronously or not. If the element's "force-async" flag is set, then, on getting, the async IDL attribute must return true, and on setting, the "force-async" flag must first be unset

" ":

[ '//other-domain.com/1.js', '2.js' ].forEach(function(src) { var script = document.createElement('script'); script.src = src; document.head.appendChild(script); });




. , , mime-, , . , , , HTML5 , .



, , JavaScript- , . , ? , ? ? ? .







DOM !

, HTML5, .

The async IDL attribute controls whether the element will execute asynchronously or not. If the element's "force-async" flag is set, then, on getting, the async IDL attribute must return true, and on setting, the "force-async" flag must first be unset

" ":

[ '//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では実現できない動作との組み合わせが提供されます。 非同期のスクリプトによって明示的に設定されたスクリプトは、純粋なHTMLの最初の例と同じように、実行キューに追加されます。 ただし、動的に作成されると、ドキュメントの解析外で実行され、読み込み中にレンダリングをブロックしません(非同期スクリプトの読み込みと同期XHRを混同しないでください)。



上記のスクリプトはページの先頭に埋め込まれ、段階的なレンダリングを中断することなくダウンロードキューをできるだけ早く開始し、指定した順序でできるだけ早く実行を開始する必要があります。 「2.js」は「1.js」に自由にダウンロードできますが、「1.js」が正常にダウンロードおよび実行されるか、これを実行できないまで実行されません。 やった! 非同期読み込みが実行されます!



このメソッドを使用したスクリプトの読み込みは、async属性がサポートされているすべての場所でサポートされますが、 Safari 5.0(5.1ではすべて問題ありません)は例外です。 さらに、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でのみサポートされており、スクリプトを2回ダウンロードするように宣言する必要があります。1つ目はリンク要素に、2つ目はスクリプトにあります。



この記事は私を落ち込ませます



状況は憂鬱であり、憂鬱に感じるはずです。 スクリプトを迅速かつ非同期にロードし、同時に実行順序を制御する繰り返しのない宣言的な方法はまだありません。



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の読み込みステータスを監視する場合にのみ、これを達成するための宣言的な方法はありません。 Enhancement-10.jsの実装は1-9でブロックされるため、async = falseでもこの問題は解決しません。 実際、ハッキングせずにこれを達成できるブラウザは1つだけです...



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



と同じ欠陥がありasync=false







十分だ! スクリプトをダウンロードするにはどうすればよいですか?



わかった、わかった。 レンダリングをブロックせず、複製を必要とせず、優れたブラウザサポートを備えた方法でスクリプトをロードする場合は、これをお勧めします。

 <script src="//other-domain.com/1.js"></script> <script src="2.js"></script>
      
      





これです。 body要素の最後。 はい、ウェブ開発者であるということは、シシフスの王のようなものです(ギリシャ神話に言及するための100の流行に敏感なポイント!)。 HTMLとブラウザの制限により、これ以上の改善はできません。



JavaScriptモジュールは、スクリプトをモジュール形式で記述する必要がある場合でも、スクリプトをロードし、実行順序を制御するための宣言的なノンブロッキングな方法を提供することで私たちを救うことを願っています。



義wu、私たちが今使えるものがもっとあるに違いない?



ボーナスポイントのために、パフォーマンスについて真剣に考え、複雑さや重複を恐れないのであれば、いくつかの考慮されたトリックを組み合わせることができます。



最初に、プリローダーのサブリソース宣言を追加します。

 <link rel="subresource" href="//other-domain.com/1.js"> <link rel="subresource" href="2.js">
      
      





次に、ドキュメントの冒頭で、 async=false



を使用してJavaScriptを使用してスクリプトを読み込み、readystateに基づいてIEのスクリプトに置き換えます。

 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" ])
      
      





スクリプトを接続するだけの場合と比べて、余分なバイトが必要ですか? BBCが行うように、JavaScriptを使用して条件付きでスクリプトを既にロードしている場合 、これらのダウンロードを早期に実行することも役立ちます。 それ以外の場合、おそらくそうではありませんが、本体の端で接続するという単純な方法に固執します。



これで、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>
      
      





仕様には 、「一緒にダウンロードし、ダウンロードした順序で実行する」とあります。

レッドゾーンブラウザー回答: 「非同期」とは何ですか? スクリプトをダウンロードしなかったかのようにダウンロードします。

残りのブラウザーは応答します:はい、いいです。



非同期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からの返信: 「非同期」とは何なのかわかりませんが、JSを介して追加されたスクリプトを追加さ​​れた順に実行することがありました。

Safari 5.0の返信: 「非同期」は理解していますが、JSでfalseに設定する方法がわかりません。 スクリプトが到着したら、任意の順序でスクリプトを実行します。

IE <10の返信: 「非同期」についてわかりませんが、 「onreadystatechange」を使用する回避策あります。

他のレッドゾーンブラウザーが応答します。 「非同期」がわかりません。スクリプトが到着したときに、任意の順序で実行します。

他の回答:私はあなたの友達です、私たちは教科書のようにそれをします。



All Articles