分離 JavaScript 下載和執行
不久前,我寫了一篇題為“關於腳本加載器的思考 1 ,其中我討論了我對繼續引入腳本加載器(如 LABjs 和 ControlJS)的想法。在那篇文章中,我還提到了我認為導致這些庫存在的主要問題。這個問題是開發人員無法將 JavaScript 文件的下載與執行分開來控制。
在與 Steve Souders 討論過 ControlJS 之後,我提出了一個關於在瀏覽器中延遲腳本執行模型的提案 2 .我與來自 Mozilla 的 Jonas Sicking 和 Boris Zbarsky 以及來自 WebKit 的 Tony Gentilcore 一起回顧了這個,除了我的之外,我們對實際用例和可能的解決方案進行了很好的討論。最終,共識是應該在 WHAT-WG 郵件列表中提出該問題以獲得更廣泛的意見,因此我發起了該線程 3 .但在深入討論之前,了解問題是有幫助的。
背景
傳統上,在下載外部 JavaScript 文件之後立即執行 JavaScript。這正是 <script>
元素在標記中起作用。還有一個不幸的副作用是 <script>
in 標記會導致瀏覽器阻止渲染(以及舊瀏覽器中的其他資源下載)。因為在至少下載整個 HTML 頁面之前,大多數 JavaScript 都不是必需的,所以添加 defer
屬性是第一次嘗試將 JavaScript 下載與執行分開。
回顧一下,添加 defer to a
<script>
導致 JavaScript 立即下載,但在加載整個 DOM 之前(在 DOMContentLoaded
之前)執行 )。用 defer
標記的多個腳本 保持執行順序。 defer
中最重要的部分 是下載外部 JavaScript 不會阻止渲染或下載其他資源。自 defer
僅在 Internet Explorer 中支持,很少使用。
開發人員發現使用 JavaScript 動態創建腳本元素會導致不同的行為。使用此模式下載外部 JavaScript 不會阻止渲染或其他下載,然後腳本會在下載後立即執行。以這種方式加載的多個腳本可能會或可能不會在瀏覽器中保留它們的執行順序(大多數沒有保留順序,Firefox 做到了)。
HTML5 引入了 async
<script>
上的屬性 啟用與動態腳本元素相同的使用模式。行為是一樣的:立即開始下載,不阻止渲染或其他下載,然後在下載完成後立即執行。執行順序明確不是 維護。
因此,對於如何加載腳本已經有了三種不同的名稱:常規、defer
, 和 async
.這三個都只是改變下載和執行外部 JavaScript 文件的時間和行為。這些很好地涵蓋了啟動下載的用例,但無法讓您確定何時執行腳本。
問題
儘管有多種加載 JavaScript 的選項,但仍然無法下載 JavaScript 文件並將其設置為在任意時間執行。您可以說立即執行,也可以推遲到 DOM 文檔完成,但您不能指定任何其他時間點來執行代碼。這導致開發人員不斷嘗試創造這種能力:
- Kyle Simpson 使用
type
“腳本/緩存”屬性強制 IE 下載但不執行 Internet Explorer 中的腳本。一旦進入緩存,就會使用相同的 URL 創建一個動態腳本元素。如果未在 JavaScript 文件中正確設置緩存標頭,這可能會導致雙重下載。 - Stoyan Stefanov 研究瞭如何使用圖像預緩存 JavaScript 和 CSS 4 . ControlJS 利用了這種技術。一旦進入緩存,就會使用相同的 URL 創建一個動態腳本元素。這具有相同的潛在缺點,涉及雙重下載。
- Gmail 移動團隊引入了一種在腳本註釋中提供 JavaScript 的技術,然後僅在必要時評估代碼 5 .唯一的缺點是您必須將代碼格式化為 HTML 內嵌的註釋,然後再進行 eval,這有點工作。
之所以有這麼多工程師試圖想出單獨下載和執行 JavaScript 的方法,是因為與阻止渲染和其他下載相關的性能影響。我們需要將 JavaScript 放到頁面上,但我們需要以不影響用戶體驗的方式進行。
請記住:這不僅僅是一個移動問題,也不僅僅是一個桌面問題,它是一個整體問題,涉及開發人員對將 JavaScript 加載到網頁中的控製程度。在 Yahoo! 工作期間,我的團隊研究了許多加載 JavaScript 的不同方式,並且研究仍在繼續。
考慮到這一切,我決定提出改善這種情況的建議。很多事情都是假設性地談論,但只有當一個具體的提議出現時,事情才會發生變化,這就是我從一開始的意圖。
要求
史蒂夫和我做的最有幫助的事情之一是概述了任何可以解決此問題的解決方案的一些基本要求:
- 必須將功能暴露給特徵檢測技術。
- 保證不會重複下載 JavaScript 文件。
- 不要禁止並行下載 JavaScript 文件。
考慮到這些要求,我開始著手我的第一個提案。
原始提案
我最初的提議
2
基於添加 noexecute
<script>
的屬性 元素,它通知瀏覽器不要執行外部文件而是下載它。您可以稍後通過調用新的 execute()
來執行代碼 方法。簡單例子:
var script = document.createElement("script");
script.noexecute = true;
script.src = "foo.js";
document.body.appendChild(script);
//later
script.execute();
noexecute
屬性也可以在 HTML 標記中指定,允許您稍後獲取對該元素的引用並調用 execute()
也在上面。在事件的更改、readyState
的形式化方面,圍繞該提案有大量額外的細節 ,以及如何處理腳本本身的各種狀態。
反應和替代方案
我從這個提案中得到的反應從“有趣”到“太複雜”不等。沒有人完全討厭它,這始終是一個好兆頭,但喜歡它的人數還不夠多,無法在不重新考慮的情況下繼續下去。與此同時,還有另外兩個提案正在流傳:
- 讓所有瀏覽器在處理動態腳本的方式上都表現得像 Internet Explorer。
src
即開始下載 屬性已分配,但在將腳本節點添加到文檔之前不會執行代碼。我指出這個的主要問題是沒有辦法檢測這個功能來區分瀏覽器行為。有人提出 Internet Explorer 是唯一支持readyState
的瀏覽器 在腳本節點上,它的值從“未初始化”開始,因此可以推斷出功能。我認識的很多人都討厭特徵推斷。 - 使用某些版本的
<link rel="prefetch">
下載 JavaScript 文件。我指出了這種方法的幾個問題,第一個是預取發生在用戶空閒時間,開發人員不知道什麼時候會發生。第二個問題是您仍然需要創建一個新的腳本節點並為其分配src
財產。這依賴於正確的緩存行為,並可能導致雙重下載。
公平地說,對我的提議也有很大的批評。我的提案中主要的不喜歡清單是:
- 使用
noexecute
時向後兼容性中斷 在標記中。 - 需要定義
readyState
和onreadystatechange
在HTMLScriptElement
. - 更改
noexecute
的加載事件的工作方式 僅限腳本。 - 添加
execute()
HTMLScriptElement
的方法 .這引發了許多問題,即在不同情況下調用此方法時會發生什麼。
WHAT-WG 郵件列表上的總體感覺是提案過於復雜,儘管總體方向看起來還不錯。
提案v2.1
在進行了一番深思熟慮之後,我決定專注於看似最簡單的解決方案:讓其他瀏覽器表現得像 Internet Explorer。正如 Kyle 所指出的,這已經被證明是可行的,並且 HTML5 規範允許這種行為。我開始重新定義我的提議,作為一種對這種行為進行編碼的方式,使開發人員能夠決定打開此功能以及進行功能檢測。結果是我稱之為 v2.1 的提案(因為我在 v2 之後做了一些重大修改)。
該提案將增強列表簡化為:
- 創建一個
preload
HTMLScriptElement
上的屬性 .這僅在 JavaScript 中使用時有效,在放入標記時無效。 - 當
preload
設置為 true,下載將在src
後立即開始 被分配到。 - 一個
onpreload
當文件成功下載並準備好執行時調用事件處理程序。 - 將腳本節點添加到文檔時執行腳本。
一個如何使用它的基本示例:
var script = document.createElement("script");
script.preload = true;
script.src = "foo.js"; //download begins here
script.onpreload = function(){
//script is now ready, if I want to execute, the following should be used:
document.body.appendChild(script);
};
之所以喜歡這個方案,是因為特徵檢測很明顯,直接對應會發生的行為:
var isPreloadSupported = (typeof script.preload == "boolean");
我喜歡這個比目前在 LABjs 中用於檢測 Internet Explorer 的特徵推斷要好得多:
var isPreloadSupported = (script.readyState == "uninitialized");
對我來說,這根本不表示存在預加載功能。它只表示 readyState
屬性存在且值為“未初始化”。這正是我在提議中試圖避免的代碼類型,這樣腳本加載器就可以停止嘗試推斷瀏覽器會做什麼,而是真正知道瀏覽器會做什麼。
該提案還保留了對 HTMLScriptElement
的更改 小而獨立,不影響現有定義。
注意:preload
的默認值也有可能 可能是 true 而不是 false,使 Internet Explorer 的行為成為支持此功能的瀏覽器中的默認行為。在這個問題上我可以選擇任何一種方式,但應該提到這種可能性。
等等
WHAT-WG 郵件列表上的對話仍在進行中。正如我在清單上所說,我真的不在乎最終的解決方案是什麼,不管它是否屬於我,只要它滿足我之前提出的三個要求。我認為很明顯,此功能對於完成從引入 async
開始的工作很重要 屬性。一旦我們更好地控制了 JavaScript 何時可以下載和執行,我們將能夠創建多種腳本加載技術。我希望我們能很快就如何最好地向前推進得出結論。
參考
- Nicholas C. Zakas 對腳本加載器的思考
- Nicholas C. Zakas 提出的延遲腳本執行提案
- WHAT-WG:分離腳本下載和執行的建議
- 在不執行的情況下預加載 JavaScript/CSS,作者:Stoyan Stefanov
- Gmail for Mobile HTML5 系列:減少啟動延遲,作者 Bikin Chiu
- Nicholas C. Zakas 提出的延遲腳本執行 v2.1 提案