JS、CSS以及img對DOMContentLoaded事件的影響
前端的純技術(shù)就是對規(guī)范的認(rèn)知
什么是DOMContentLoaded事件?
首先想到的是查看W3C的HTML5規(guī)范,DOMContentLoaded事件在什么時候觸發(fā):
Once the user agent stops parsing the document, the user agent must run the following steps:
1. Set the current document readiness to “interactive” and the insertion point to undefined.
Pop all the nodes off the stack of open elements.
2. If the list of scripts that will execute when the document has finished parsing is not empty, run these substeps:
2.1 Spin the event loop until the first script in the list of scripts that will execute when the document has finished parsing has its “ready to be parser-executed” flag set and the parser's Document has no style sheet that is blocking scripts.
2.2 Execute the first script in the list of scripts that will execute when the document has finished parsing.
2.3 Remove the first script element from the list of scripts that will execute when the document has finished parsing (i.e. shift out the first entry in the list).
2.4 If the list of scripts that will execute when the document has finished parsing is still not empty, repeat these substeps again from substep 1.
3. Queue a task to fire a simple event that bubbles named DOMContentLoaded at the Document.
規(guī)范總是那么的晦澀,但至少有一點是可以明確了的,就是在JS(不包括動態(tài)插入的JS)執(zhí)行完之后,才會觸發(fā)DOMContentLoaded事件。
接下來看看MDN上有關(guān)DOMContentLoaded事件的文檔:
The DOMContentLoaded event is fired when the document has been completely loaded and parsed, without waiting for stylesheets, images, and subframes to finish loading
Note: Stylesheet loads block script execution, so if you have a<script>
after a<link rel="stylesheet" ...>
, the page will not finish parsing – and DOMContentLoaded will not fire – until the stylesheet is loaded.
這么看來,至少可以得出這么一個理論:DOMContentLoaded事件本身不會等待CSS文件、圖片、iframe加載完成。
它的觸發(fā)時機(jī)是:加載完頁面,解析完所有標(biāo)簽(不包括執(zhí)行CSS和JS),并如規(guī)范中所說的設(shè)置 interactive
和執(zhí)行每個靜態(tài)的script標(biāo)簽中的JS,然后觸發(fā)。
而JS的執(zhí)行,需要等待位于它前面的CSS加載(如果是外聯(lián)的話)、執(zhí)行完成,因為JS可能會依賴位于它前面的CSS計算出來的樣式。
實踐是檢驗真理的唯一標(biāo)準(zhǔn)
實驗1:DOMContentLoaded事件不直接等待CSS文件、圖片的加載完成
index.html:
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title></title> <link rel="stylesheet" type="text/css" href="./css/main.css" rel="external nofollow" rel="external nofollow" > </head> <body> <p>Content</p> <img src="./img/chrome-girl.jpg"> </body> </html>
圖一
如果頁面中沒有script標(biāo)簽,DOMContentLoaded事件并沒有等待CSS文件、圖片加載完成。
Chrome開發(fā)者工具的Timeline面板可以幫我們記錄下瀏覽器的一舉一動。圖一中紅色小方框中的藍(lán)線,表示DOMContentLoaded事件,它右邊的紅線和綠線分別表示load事件和First paint,鼠標(biāo)hover在這些線露出灰色方框下面的一小部分時就會出現(xiàn)帶有說明文字的tips(這交互夠反人類的對吧?。?/p>
實驗2:DOMContentLoaded事件需要等待JS執(zhí)行完才觸發(fā)
index.html:
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title></title> <script type="text/javascript"> console.timeStamp('Inline script before link in head'); window.addEventListener('DOMContentLoaded', function(){ console.timeStamp('DOMContentLoaded event'); }); </script> <link rel="stylesheet" type="text/css" href="./css/main.css" rel="external nofollow" rel="external nofollow" > <script type="text/javascript"> console.timeStamp('Inline script after link in head'); </script> </head> <body> <p>Content</p> <img src="./img/chrome-girl.jpg"> <script type="text/javascript" src="./js/main.js"></script> </body> </html>
main.js:
console.timeStamp('External script after link in body');
圖二
如果頁面中靜態(tài)的寫有script標(biāo)簽,DOMContentLoaded事件需要等待JS執(zhí)行完才觸發(fā)。
而script標(biāo)簽中的JS需要等待位于其前面的CSS的加載完成。
console.timeStamp() 可以向Timeline中添加一條記錄,并對應(yīng)上方的一條黃線。
從圖二中可以看出,在CSS之前的JS立刻得到了執(zhí)行,而在CSS之后的JS,需要等待CSS加載完后才執(zhí)行,比較明顯的是main.js早就加載完了,但還是要等main.css加載完才能執(zhí)行。而DOMContentLoaded事件,則是在JS執(zhí)行完后才觸發(fā)?;瑒覶imeline面板中表示展示區(qū)域的滑塊,如圖三,放大后即可看到表示DOMContentLoaded事件的藍(lán)線(之前跟黃線和綠線靠的太近了),當(dāng)然,通過 console.timeStamp() 向TimeLine中添加的記錄也可證明其觸發(fā)時間。
圖三
現(xiàn)代瀏覽器會并發(fā)的預(yù)加載CSS, JS,也就是一開始就并發(fā)的請求這些資源,但是,執(zhí)行CSS和JS的順序還是按原來的依賴順序(JS的執(zhí)行要等待位于其前面的CSS和JS加載、執(zhí)行完)。先加載完成的資源,如果其依賴還沒加載、執(zhí)行完,就只能等著。
實驗3:img何時開始解碼、繪制?
從圖三中我們可以發(fā)現(xiàn)一個有趣的地方:img的請求老早就發(fā)出了,但延遲了一段時間才開始解碼。如圖二、圖三中的紅框所示,截圖中只框出了一部分表示解碼的記錄,而實際上這些表示解碼的記錄一直持續(xù)到img加載結(jié)束,如圖四所示,img是一邊加載一邊解碼的:
圖三
現(xiàn)代瀏覽器會并發(fā)的預(yù)加載CSS, JS,也就是一開始就并發(fā)的請求這些資源,但是,執(zhí)行CSS和JS的順序還是按原來的依賴順序(JS的執(zhí)行要等待位于其前面的CSS和JS加載、執(zhí)行完)。先加載完成的資源,如果其依賴還沒加載、執(zhí)行完,就只能等著。
實驗3:img何時開始解碼、繪制?
從圖三中我們可以發(fā)現(xiàn)一個有趣的地方:img的請求老早就發(fā)出了,但延遲了一段時間才開始解碼。如圖二、圖三中的紅框所示,截圖中只框出了一部分表示解碼的記錄,而實際上這些表示解碼的記錄一直持續(xù)到img加載結(jié)束,如圖四所示,img是一邊加載一邊解碼的:
圖四
抱著“猜想——驗證”的想法,我猜想這是因為img這個資源是否需要展現(xiàn)出來,需要等 所有的JS和CSS的執(zhí)行完 才知道,因為main.js可能會執(zhí)行某些DOM操作,比如刪除這個img元素,或者修改其src屬性,而CSS可能會將其 display: none
。
圖五
圖六
圖七
圖五中沒有JS和CSS,img的數(shù)據(jù)一接收到就馬上開始解碼了。
圖六中沒有JS,但img要等到CSS加載完才開始解碼。
圖七的代碼跟圖六的代碼唯一的區(qū)別是CSS把img給 display: none;
,這使得img雖然請求了,但根本沒有進(jìn)行解碼。
這說明,img是否需要解碼、繪圖(paint)出來,確實需要等CSS加載、執(zhí)行完才能知道。也就是說,CSS會阻塞img的展現(xiàn)!那么JS呢?
圖八
圖八對應(yīng)的代碼:
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title></title> <script type="text/javascript"> console.timeStamp('Inline script in head'); window.addEventListener('DOMContentLoaded', function(){ console.timeStamp('DOMContentLoaded event'); }); </script> </head> <body> <p>Content</p> <img src="./img/chrome-girl.jpg"> <script type="text/javascript" src="./js/main.js"></script> </body> </html>
非常令人驚訝,在有JS而沒有CSS的頁面中,img居然能夠在收到數(shù)據(jù)后就立刻開始解碼、繪圖(paint),也就是說,JS并沒有阻塞img的展現(xiàn)!這跟我們以前理解的JS會阻塞img資源的傳統(tǒng)觀念不太一樣,看來Chrome對img的加載和展現(xiàn)做了新的優(yōu)化。
我們常用的jQuery的 $(document).ready() 方法,就是對DOMContentLoaded事件的監(jiān)聽(當(dāng)然,其內(nèi)部還會通過模擬DOMContentLoaded事件和監(jiān)聽onload事件來提供降級方案)。通常推薦在DOMContentLoaded事件觸發(fā)的時候為DOM元素注冊事件。所以盡快的讓DOMContentLoaded事件觸發(fā),就意味著能夠盡快讓頁面可交互:
減小CSS文件體積,把單個CSS文件分成幾個文件以并行加載,減少CSS對JS的阻塞時間
次要的JS文件,通過動態(tài)插入script標(biāo)簽來加載(動態(tài)插入的script標(biāo)簽不阻塞DOMContentLoaded事件的觸發(fā))
CSS中使用的精靈圖,可以利用對img的預(yù)加載,放在html中跟CSS文件一起加載
在做實驗的過程中,感覺Chrome開發(fā)者工具的Timeline面板非常強(qiáng)大,瀏覽器的一舉一動都記錄下來。以前我們前端開發(fā)要想理解、探索瀏覽器的內(nèi)部行為,或者摸著石頭過河的做黑盒測試,或者事倍功半的研究瀏覽器源碼,唯一高效點的做法就是學(xué)習(xí)別人的研究經(jīng)驗,看老外的文章,但瀏覽器的發(fā)展日新月異(比如這次實驗發(fā)現(xiàn)的JS不阻塞img的展現(xiàn)),別人的經(jīng)驗始終不是最新、最適合的,關(guān)鍵是要結(jié)合自己的業(yè)務(wù)、需求場景,有針對性的做分析和優(yōu)化。
PS.
以上測試環(huán)境為windows/chrome,并用Fiddler模擬慢速網(wǎng)絡(luò)
相關(guān)文章
JavaScript中檢測數(shù)據(jù)類型的四種方法
這篇文章主要給大家分享的是JavaScript中檢測數(shù)據(jù)類型的四種方法,有 typeof、instanceof、constructor、Object.prototype.toString.call(),下面文章詳細(xì)介紹內(nèi)容,需要的小伙伴可以參考一下2022-01-01JavaScript學(xué)習(xí)筆記之ES6數(shù)組方法
ES6給數(shù)組添加了一些新特性,而這些新特性到目前為止完全可以運用到自己的業(yè)務(wù)層。在這一節(jié)中將總結(jié)有關(guān)于ES6給數(shù)組提供一些新特性的使用方法2016-03-03微信小程序?qū)崿F(xiàn)基于三元運算驗證手機(jī)號/姓名功能示例
這篇文章主要介紹了微信小程序?qū)崿F(xiàn)基于三元運算驗證手機(jī)號/姓名功能,涉及三元運算符的判定及字符串正則驗證相關(guān)操作技巧,需要的朋友可以參考下2019-01-01如何手動實現(xiàn)一個 JavaScript 模塊執(zhí)行器
這篇文章主要介紹了如何手動實現(xiàn)一個 JavaScript 模塊執(zhí)行器,幫助大家更好的理解和使用JavaScript,感興趣的朋友可以了解下2020-10-10JavaScript出現(xiàn)setTimeout倒計時誤差的原因分析
setTimeout?倒計時誤差的出現(xiàn)主要與?JavaScript?的事件循環(huán)機(jī)制和計時器的執(zhí)行方式有關(guān),本文就來和大家講講具體出現(xiàn)的原因以及解決方法,需要的可以參考一下2023-06-06