無論當(dāng)前 JavaScript 代碼是內(nèi)嵌還是在外鏈文件中,頁面的下載和渲染都必須停下來等待腳本執(zhí)行完成。JavaScript 執(zhí)行過程耗時越久,瀏覽器等待響應(yīng)用戶輸入的時間就越長。瀏覽器在下載和執(zhí)行腳本時出現(xiàn)阻塞的原因在于,腳本可能會改變頁面或 JavaScript 的命名空間,它們對后面頁面內(nèi)容造成影響。一個典型的例子就是在頁面中使用
document.write()
。例如清單 1
<html> <head> <title>Source Example</title> </head> <body> <p> <script type="text/javascript"> document.write("Today is " + (new Date()).toDateString()); </script> </p> </body> </html>
?
當(dāng)瀏覽器遇到
<script>
標(biāo)簽時,當(dāng)前
HTML
頁面無從獲知
JavaScript
是否會向
<p>
標(biāo)簽添加內(nèi)容,或引入其他元素,或甚至移除該標(biāo)簽。因此,這時瀏覽器會停止處理頁面,先執(zhí)行
JavaScript
代碼,然后再繼續(xù)解析和渲染頁面。同樣的情況也發(fā)生在使用
src
屬性加載
JavaScript
的過程中,瀏覽器必須先花時間下載外鏈文件中的代碼,然后解析并執(zhí)行它。在這個過程中,頁面渲染和用戶交互完全被阻塞了。
?
HTML 4 規(guī)范指出
<script>
標(biāo)簽可以放在 HTML 文檔的
<head>
或
<body>
中,并允許出現(xiàn)多次。Web 開發(fā)人員一般習(xí)慣在
<head>
中加載外鏈的 JavaScript,接著用
<link>
標(biāo)簽用來加載外鏈的 CSS 文件或者其他頁面信息。例如清單 2
<html> <head> <title>Source Example</title> <script type="text/javascript" src="script1.js"></script> <script type="text/javascript" src="script2.js"></script> <script type="text/javascript" src="script3.js"></script> <link rel="stylesheet" type="text/css" href="styles.css"> </head> <body> <p>Hello world!</p> </body> </html>
?
然而這種常規(guī)的做法卻隱藏著嚴(yán)重的性能問題。在清單 2 的示例中,當(dāng)瀏覽器解析到
<script>
標(biāo)簽(第 4 行)時,瀏覽器會停止解析其后的內(nèi)容,而優(yōu)先下載腳本文件,并執(zhí)行其中的代碼,這意味著,其后的 styles.css 樣式文件和
<body>
標(biāo)簽都無法被加載,由于
<body>
標(biāo)簽無法被加載,那么頁面自然就無法渲染了。因此在該 JavaScript 代碼完全執(zhí)行完之前,頁面都是一片空白。圖 1 描述了頁面加載過程中腳本和樣式文件的下載過程。
圖 1 JavaScript 文件的加載和執(zhí)行阻塞其他文件的下載
我 們可以發(fā)現(xiàn)一個有趣的現(xiàn)象:第一個 JavaScript 文件開始下載,與此同時阻塞了頁面其他文件的下載。此外,從 script1.js 下載完成到 script2.js 開始下載前存在一個延時,這段時間正好是 script1.js 文件的執(zhí)行過程。每個文件必須等到前一個文件下載并執(zhí)行完成才會開始下載。在這些文件逐個下載過程中,用戶看到的是一片空白的頁面。
?
從 IE 8、Firefox 3.5、Safari 4 和 Chrome 2 開始都允許并行下載 JavaScript 文件。這是個好消息,因為
<script>
標(biāo)簽在下載外部資源時不會阻塞其他
<script>
標(biāo) 簽。遺憾的是,JavaScript 下載過程仍然會阻塞其他資源的下載,比如樣式文件和圖片。盡管腳本的下載過程不會互相影響,但頁面仍然必須等待所有 JavaScript 代碼下載并執(zhí)行完成才能繼續(xù)。因此,盡管最新的瀏覽器通過允許并行下載提高了性能,但問題尚未完全解決,腳本阻塞仍然是一個問題。
?
由于腳本會阻塞頁面其他資源的下載,因此推薦將所有
<script>
標(biāo)簽盡可能放到
<body>
標(biāo)簽的底部,以盡量減少對整個頁面下載的影響。例如清單 3
<html> <head> <title>Source Example</title> <link rel="stylesheet" type="text/css" href="styles.css"> </head> <body> <p>Hello world!</p> <!-- Example of efficient script positioning --> <script type="text/javascript" src="script1.js"></script> <script type="text/javascript" src="script2.js"></script> <script type="text/javascript" src="script3.js"></script> </body> </html>
?
這段代碼展示了在 HTML 文檔中放置
<script>
標(biāo)簽的推薦位置。盡管腳本下載會阻塞另一個腳本,但是頁面的大部分內(nèi)容都已經(jīng)下載完成并顯示給了用戶,因此頁面下載不會顯得太慢。這是優(yōu)化 JavaScript 的首要規(guī)則:將腳本放在底部。
?
由于每個
<script>
標(biāo)簽初始下載時都會阻塞頁面渲染,所以減少頁面包含的
<script>
標(biāo)簽數(shù)量有助于改善這一情況。這不僅針對外鏈腳本,內(nèi)嵌腳本的數(shù)量同樣也要限制。瀏覽器在解析 HTML 頁面的過程中每遇到一個
<script>
標(biāo)簽,都會因執(zhí)行腳本而導(dǎo)致一定的延時,因此最小化延遲時間將會明顯改善頁面的總體性能。
這個問題在處理外鏈 JavaScript 文件時略有不同。考慮到 HTTP 請求會帶來額外的性能開銷,因此下載單個 100Kb 的文件將比下載 5 個 20Kb 的文件更快。也就是說,減少頁面中外鏈腳本的數(shù)量將會改善性能。
?
通常一個大型網(wǎng)站或應(yīng)用需要依賴數(shù)個 JavaScript 文件。您可以把多個文件合并成一個,這樣只需要引用一個
<script>
標(biāo)簽,就可以減少性能消耗。文件合并的工作可通過離線的打包工具或者一些實時的在線服務(wù)來實現(xiàn)。
需要特別提醒的是,把一段內(nèi)嵌腳本放在引用外鏈樣式表的
<link>
之后會導(dǎo)致頁面阻塞去等待樣式表的下載。這樣做是為了確保內(nèi)嵌腳本在執(zhí)行時能獲得最精確的樣式信息。因此,建議不要把內(nèi)嵌腳本緊跟在
<link>
標(biāo)簽后面。
?
減 少 JavaScript 文件大小并限制 HTTP 請求數(shù)在功能豐富的 Web 應(yīng)用或大型網(wǎng)站上并不總是可行。Web 應(yīng)用的功能越豐富,所需要的 JavaScript 代碼就越多,盡管下載單個較大的 JavaScript 文件只產(chǎn)生一次 HTTP 請求,卻會鎖死瀏覽器的一大段時間。為避免這種情況,需要通過一些特定的技術(shù)向頁面中逐步加載 JavaScript 文件,這樣做在某種程度上來說不會阻塞瀏覽器。
?
無阻塞腳本的秘訣在于,在頁面加載完成后才加載 JavaScript 代碼。這就意味著在
window
對象的
onload
事件觸發(fā)后再下載腳本。有多種方式可以實現(xiàn)這一效果。
?
HTML 4 為
<script>
標(biāo)簽定義了一個擴展屬性:
defer
。
Defer
屬性指明本元素所含的腳本不會修改 DOM,因此代碼能安全地延遲執(zhí)行。
defer
屬性只被 IE 4 和 Firefox 3.5 更高版本的瀏覽器所支持,所以它不是一個理想的跨瀏覽器解決方案。在其他瀏覽器中,
defer
屬性會被直接忽略,因此
<script>
標(biāo)簽會以默認的方式處理,也就是說會造成阻塞。然而,如果您的目標(biāo)瀏覽器支持的話,這仍然是個有用的解決方案。清單 4 是一個例子
<script type="text/javascript" src="script1.js" defer></script>
?
帶有
defer
屬性的
<script>
標(biāo)簽可以放置在文檔的任何位置。對應(yīng)的 JavaScript 文件將在頁面解析到
<script>
標(biāo)簽時開始下載,但不會執(zhí)行,直到 DOM 加載完成,即
onload
事件觸發(fā)前才會被執(zhí)行。當(dāng)一個帶有
defer
屬性的 JavaScript 文件下載時,它不會阻塞瀏覽器的其他進程,因此這類文件可以與其他資源文件一起并行下載。
?
任何帶有
defer
屬性的
<script>
元素在 DOM 完成加載之前都不會被執(zhí)行,無論內(nèi)嵌或者是外鏈腳本都是如此。清單 5 的例子展示了
defer
屬性如何影響腳本行為:
<html> <head> <title>Script Defer Example</title> </head> <body> <script type="text/javascript" defer> alert("defer"); </script> <script type="text/javascript"> alert("script"); </script> <script type="text/javascript"> window.onload = function(){ alert("load"); }; </script> </body> </html>
?
這段代碼在頁面處理過程中彈出三次對話框。不支持
defer
屬性的瀏覽器的彈出順序是:“defer”、“script”、“l(fā)oad”。而在支持
defer
屬性的瀏覽器上,彈出的順序則是:“script”、“defer”、“l(fā)oad”。請注意,帶有
defer
屬性的
<script>
元素不是跟在第二個后面執(zhí)行,而是在
onload
事件被觸發(fā)前被調(diào)用。
?
如果您的目標(biāo)瀏覽器只包括 Internet Explorer 和 Firefox 3.5,那么
defer
腳本確實有用。如果您需要支持跨領(lǐng)域的多種瀏覽器,那么還有更一致的實現(xiàn)方式。
?
HTML 5 為
<script>
標(biāo)簽定義了一個新的擴展屬性:
async
。它的作用和
defer
一樣,能夠異步地加載和執(zhí)行腳本,不因為加載腳本而阻塞頁面的加載。但是有一點需要注意,在有
async
的情況下,JavaScript 腳本一旦下載好了就會執(zhí)行,所以很有可能不是按照原本的順序來執(zhí)行的。如果 JavaScript 腳本前后有依賴性,使用
async
就很有可能出現(xiàn)錯誤。
?
文檔對象模型(DOM)允許您使用 JavaScript 動態(tài)創(chuàng)建 HTML 的幾乎全部文檔內(nèi)容。
<script>
元素與頁面其他元素一樣,可以非常容易地通過標(biāo)準(zhǔn) DOM 函數(shù)創(chuàng)建:
清單 6 通過標(biāo)準(zhǔn) DOM 函數(shù)創(chuàng)建<script>元素
var script = document.createElement ("script"); script.type = "text/javascript"; script.src = "script1.js"; document.getElementsByTagName("head")[0].appendChild(script);
?
新的
<script>
元素加載 script1.js 源文件。此文件當(dāng)元素添加到頁面之后立刻開始下載。此技術(shù)的重點在于:無論在何處啟動下載,文件的下載和運行都不會阻塞其他頁面處理過程。您甚至可以將這些代碼放在
<head>
部分而不會對其余部分的頁面代碼造成影響(除了用于下載文件的 HTTP 連接)。
?
當(dāng) 文件使用動態(tài)腳本節(jié)點下載時,返回的代碼通常立即執(zhí)行(除了 Firefox 和 Opera,他們將等待此前的所有動態(tài)腳本節(jié)點執(zhí)行完畢)。當(dāng)腳本是“自運行”類型時,這一機制運行正常,但是如果腳本只包含供頁面其他腳本調(diào)用調(diào)用的接 口,則會帶來問題。這種情況下,您需要跟蹤腳本下載完成并是否準(zhǔn)備妥善。可以使用動態(tài)
<script>
節(jié)點發(fā)出事件得到相關(guān)信息。
?
Firefox、Opera, Chorme 和 Safari 3+會在
<script>
節(jié)點接收完成之后發(fā)出一個
onload
事件。您可以監(jiān)聽這一事件,以得到腳本準(zhǔn)備好的通知:
清單 7 通過監(jiān)聽 onload 事件加載 JavaScript 腳本
var script = document.createElement ("script") script.type = "text/javascript"; //Firefox, Opera, Chrome, Safari 3+ script.onload = function(){ alert("Script loaded!"); }; script.src = "script1.js"; document.getElementsByTagName("head")[0].appendChild(script);
?
Internet Explorer 支持另一種實現(xiàn)方式,它發(fā)出一個
readystatechange
事件。
<script>
元素有一個
readyState
屬性,它的值隨著下載外部文件的過程而改變。
readyState
有五種取值:
- “uninitialized”:默認狀態(tài)
- “l(fā)oading”:下載開始
- “l(fā)oaded”:下載完成
- “interactive”:下載完成但尚不可用
- “complete”:所有數(shù)據(jù)已經(jīng)準(zhǔn)備好
微軟文檔上說,在
<script>
元素的生命周期中,
readyState
的這些取值不一定全部出現(xiàn),但并沒有指出哪些取值總會被用到。實踐中,我們最感興趣的是“l(fā)oaded”和“complete”狀態(tài)。Internet Explorer 對這兩個
readyState
值所表示的最終狀態(tài)并不一致,有時
<script>
元素會得到“l(fā)oader”卻從不出現(xiàn)“complete”,但另外一些情況下出現(xiàn)“complete”而用不到“l(fā)oaded”。最安全的辦法就是在
readystatechange
事件中檢查這兩種狀態(tài),并且當(dāng)其中一種狀態(tài)出現(xiàn)時,刪除
readystatechange
事件句柄(保證事件不會被處理兩次):
清單 8 通過檢查 readyState 狀態(tài)加載 JavaScript 腳本
var script = document.createElement("script") script.type = "text/javascript"; //Internet Explorer script.onreadystatechange = function(){ if (script.readyState == "loaded" || script.readyState == "complete"){ script.onreadystatechange = null; alert("Script loaded."); } }; script.src = "script1.js"; document.getElementsByTagName("head")[0].appendChild(script);
?
大多數(shù)情況下,您希望調(diào)用一個函數(shù)就可以實現(xiàn) JavaScript 文件的動態(tài)加載。下面的函數(shù)封裝了標(biāo)準(zhǔn)實現(xiàn)和 IE 實現(xiàn)所需的功能:
function loadScript(url, callback){ var script = document.createElement ("script") script.type = "text/javascript"; if (script.readyState){ //IE script.onreadystatechange = function(){ if (script.readyState == "loaded" || script.readyState == "complete"){ script.onreadystatechange = null; callback(); } }; } else { //Others script.onload = function(){ callback(); }; } script.src = url; document.getElementsByTagName("head")[0].appendChild(script); }
?
此函數(shù)接收兩個參數(shù):JavaScript 文件的 URL,和一個當(dāng) JavaScript 接收完成時觸發(fā)的回調(diào)函數(shù)。屬性檢查用于決定監(jiān)視哪種事件。最后一步,設(shè)置
src
屬性,并將
<script>
元素添加至頁面。此
loadScript()
函數(shù)使用方法如下:
loadScript("script1.js", function(){ alert("File is loaded!"); });
?
您可以在頁面中動態(tài)加載很多 JavaScript 文件,但要注意,瀏覽器不保證文件加載的順序。所有主流瀏覽器之中,只有 Firefox 和 Opera 保證腳本按照您指定的順序執(zhí)行。其他瀏覽器將按照服務(wù)器返回它們的次序下載并運行不同的代碼文件。您可以將下載操作串聯(lián)在一起以保證他們的次序,如下:
清單 11 通過 loadScript()函數(shù)加載多個 JavaScript 腳本
loadScript("script1.js", function(){ loadScript("script2.js", function(){ loadScript("script3.js", function(){ alert("All files are loaded!"); }); }); });
?
此代碼等待 script1.js 可用之后才開始加載 script2.js,等 script2.js 可用之后才開始加載 script3.js。雖然此方法可行,但如果要下載和執(zhí)行的文件很多,還是有些麻煩。如果多個文件的次序十分重要,更好的辦法是將這些文件按照正確的次 序連接成一個文件。獨立文件可以一次性下載所有代碼(由于這是異步進行的,使用一個大文件并沒有什么損失)。
?
動態(tài)腳本加載是非阻塞 JavaScript 下載中最常用的模式,因為它可以跨瀏覽器,而且簡單易用。
此技術(shù)首先創(chuàng)建一個 XHR 對象,然后下載 JavaScript 文件,接著用一個動態(tài)
<script>
元素將 JavaScript 代碼注入頁面。清單 12 是一個簡單的例子:
清單 12 通過 XHR 對象加載 JavaScript 腳本
var xhr = new XMLHttpRequest(); xhr.open("get", "script1.js", true); xhr.onreadystatechange = function(){ if (xhr.readyState == 4){ if (xhr.status >= 200 && xhr.status < 300 || xhr.status == 304){ var script = document.createElement ("script"); script.type = "text/javascript"; script.text = xhr.responseText; document.body.appendChild(script); } } }; xhr.send(null);
?
此代碼向服務(wù)器發(fā)送一個獲取 script1.js 文件的 GET 請求。
onreadystatechange
事件處理函數(shù)檢查
readyState
是不是 4,然后檢查 HTTP 狀態(tài)碼是不是有效(2XX 表示有效的回應(yīng),304 表示一個緩存響應(yīng))。如果收到了一個有效的響應(yīng),那么就創(chuàng)建一個新的
<script>
元素,將它的文本屬性設(shè)置為從服務(wù)器接收到的
responseText
字符串。這樣做實際上會創(chuàng)建一個帶有內(nèi)聯(lián)代碼的
<script>
元素。一旦新
<script>
元素被添加到文檔,代碼將被執(zhí)行,并準(zhǔn)備使用。
?
這種方法的主要優(yōu)點是,您可以下載不立即執(zhí)行的 JavaScript 代碼。由于代碼返回在
<script>
標(biāo)簽之外(換句話說不受
<script>
標(biāo)簽約束),它下載后不會自動執(zhí)行,這使得您可以推遲執(zhí)行,直到一切都準(zhǔn)備好了。另一個優(yōu)點是,同樣的代碼在所有現(xiàn)代瀏覽器中都不會引發(fā)異常。
?
此方法最主要的限制是:JavaScript 文件必須與頁面放置在同一個域內(nèi),不能從 CDN 下載(CDN 指"內(nèi)容投遞網(wǎng)絡(luò)(Content Delivery Network)",所以大型網(wǎng)頁通常不采用 XHR 腳本注入技術(shù)。
?
減少 JavaScript 對性能的影響有以下幾種方法:
-
將所有的
<script>
標(biāo)簽放到頁面底部,也就是</body>
閉合標(biāo)簽之前,這能確保在腳本執(zhí)行前頁面已經(jīng)完成了渲染。 -
盡可能地合并腳本。頁面中的
<script>
標(biāo)簽越少,加載也就越快,響應(yīng)也越迅速。無論是外鏈腳本還是內(nèi)嵌腳本都是如此。 - 采用無阻塞下載 JavaScript 腳本的方法:
-
使用
<script>
標(biāo)簽的 defer 屬性(僅適用于 IE 和 Firefox 3.5 以上版本); -
使用動態(tài)創(chuàng)建的
<script>
元素來下載并執(zhí)行代碼; - 使用 XHR 對象下載 JavaScript 代碼并注入頁面中。
通過以上策略,可以在很大程度上提高那些需要使用大量 JavaScript 的 Web 網(wǎng)站和應(yīng)用的實際性能。
?
學(xué)習(xí)
-
Steve Sounders 撰寫的
高性能網(wǎng)站建設(shè)指南
介紹了網(wǎng)站性能問題的現(xiàn)狀、產(chǎn)生的原因,以及改善或解決性能問題的原則、技術(shù)技巧和最佳實踐。
-
Steve Sounders 撰寫的
高性能網(wǎng)站建設(shè)進階指南
提供了提升網(wǎng)站性能的最佳實踐和實用建議。
-
參考了
雅虎特別性能小組
在 JavaScript 影響頁面下載性能方面的研究成果。
-
Steve Sounders 的個人博客
HIGH PERFORMANCE WEB SITES BLOG
-
“提升 web 應(yīng)用程序的性能”
(developerWorks,2011 年 8 月):找出瓶頸,加快客戶端內(nèi)容的速度。
-
“全面提升 Web 2.0 應(yīng)用程序的性能,第 3 部分: 瀏覽器渲染時間分析”
(developerWorks,2010 年 2 月):瀏覽器端頁面渲染時間的性能分析。
-
developerWorks Web development
專區(qū)
:通過專門關(guān)于 Web 技術(shù)的文章和教程,擴展您在網(wǎng)站開發(fā)方面的技能。
-
developerWorks Ajax 資源中心
:這是有關(guān) Ajax 編程模型信息的一站式中心,包括很多文檔、教程、論壇、blog、wiki 和新聞。任何 Ajax 的新信息都能在這里找到。
-
developerWorks Web 2.0 資源中心
,這是有關(guān) Web 2.0 相關(guān)信息的一站式中心,包括大量 Web 2.0 技術(shù)文章、教程、下載和相關(guān)技術(shù)資源。您還可以通過
Web 2.0 新手入門
欄目,迅速了解 Web 2.0 的相關(guān)概念。
-
查看
HTML5 專題
,了解更多和 HTML5 相關(guān)的知識和動向。
討論
加入 developerWorks 中文社區(qū) 。查看開發(fā)人員推動的博客、論壇、組和維基,并與其他 developerWorks 用戶交流。
?
原文: http://www.ibm.com/developerworks/cn/web/1308_caiys_jsload/index.html?ca=drs-
?
更多文章、技術(shù)交流、商務(wù)合作、聯(lián)系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號聯(lián)系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
