任務(wù)
- CSS和JS在網(wǎng)頁(yè)中的放置順序是怎樣的?
- 解釋白屏和FOUC
- async和defer的作用是什么?有什么區(qū)別
- 簡(jiǎn)述網(wǎng)頁(yè)的渲染機(jī)制
- 從上面4個(gè)題目中隨機(jī)選擇一題寫成博客,投遞到饑人谷技術(shù)博客66 (可選題目)
1.CSS和JS在網(wǎng)頁(yè)中的放置順序是怎樣的
css樣式放在head中。
js放置在body標(biāo)簽內(nèi)的最后,script標(biāo)簽內(nèi)。外鏈用<script src=""></script>,內(nèi)部的用<script></script>
2.白屏問題
對(duì)于圖片和CSS, 在加載時(shí)會(huì)并發(fā)加載(如一個(gè)域名下同時(shí)加載兩個(gè)文件). 但在加載 JavaScript 時(shí),會(huì)禁用并發(fā),并且阻止其他內(nèi)容的下載. 所以把 JavaScript 放入頁(yè)面頂部也會(huì)導(dǎo)致 白屏 現(xiàn)象.
FOUC (Flash of Unstyled Content) 無樣式內(nèi)容閃爍
如果把樣式放在底部,對(duì)于IE瀏覽器,在某些場(chǎng)景下(點(diǎn)擊鏈接,輸入U(xiǎn)RL,使用書簽進(jìn)入等),會(huì)出現(xiàn) FOUC 現(xiàn)象(逐步加載無樣式的內(nèi)容,等CSS加載后頁(yè)面突然展現(xiàn)樣式).對(duì)于 Firefox 會(huì)一直表現(xiàn)出 FOUC .
將JS放在底部
- 腳本會(huì)阻塞后面內(nèi)容的呈現(xiàn)
- 腳本會(huì)阻塞其后組件的下載
對(duì)于圖片和CSS, 在加載時(shí)會(huì)并發(fā)加載(如一個(gè)域名下同時(shí)加載兩個(gè)文件). 但在加載JavaScript時(shí),會(huì)禁用并發(fā),并且阻止其他內(nèi)容的下載. 所以把JavaScript放入頁(yè)面頂部也會(huì)導(dǎo)致白屏現(xiàn)象。
3.async和defer的作用是什么?有什么區(qū)別
加載異步
<script src="script.js"></script>
沒有 defer 或 async,瀏覽器會(huì)立即加載并執(zhí)行指定的腳本,“立即”指的是在渲染該 script 標(biāo)簽之下的文檔元素之前,也就是說不等待后續(xù)載入的文檔元素,讀到就加載并執(zhí)行。
<script async src="script.js"></script>
有 async,加載和渲染后續(xù)文檔元素的過程將和 script.js 的加載與執(zhí)行并行進(jìn)行(異步)。
<script defer src="script.js"></script>
有 defer,加載后續(xù)文檔元素的過程將和 script.js 的加載并行進(jìn)行(異步),但 script.js 的執(zhí)行要在所有元素解析完成之后,DOMContentLoaded 事件觸發(fā)之前完成。
defer:腳本延遲到文檔解析和顯示后執(zhí)行,有順序
async:不保證順序
簡(jiǎn)述網(wǎng)頁(yè)的渲染機(jī)制
解析 HTML 標(biāo)簽, 構(gòu)建 DOM 樹
解析 CSS 標(biāo)簽, 構(gòu)建 CSSOM 樹
把 DOM 和 CSSOM 組合成 渲染樹 (render tree)
在渲染樹的基礎(chǔ)上進(jìn)行布局, 計(jì)算每個(gè)節(jié)點(diǎn)的幾何結(jié)構(gòu)
把每個(gè)節(jié)點(diǎn)繪制到屏幕上 (painting)

瀏覽器的渲染機(jī)制
Google Web Fundamentals 是一個(gè)非常優(yōu)秀的文檔,里面講到了跟web、瀏覽器、前端的方方面面。我總結(jié)一下其中的 Ilya Grigorik 寫的 Critical rendering path 瀏覽器渲染機(jī)制部分的內(nèi)容如下:
幾個(gè)概念
1、DOM:Document Object Model,瀏覽器將HTML解析成樹形的數(shù)據(jù)結(jié)構(gòu),簡(jiǎn)稱DOM。
2、CSSOM:CSS Object Model,瀏覽器將CSS代碼解析成樹形的數(shù)據(jù)結(jié)構(gòu)。
3、DOM 和 CSSOM 都是以 Bytes → characters → tokens → nodes → object model. 這樣的方式生成最終的數(shù)據(jù)。如下圖所示:
DOM 樹的構(gòu)建過程是一個(gè)深度遍歷過程:當(dāng)前節(jié)點(diǎn)的所有子節(jié)點(diǎn)都構(gòu)建好后才會(huì)去構(gòu)建當(dāng)前節(jié)點(diǎn)的下一個(gè)兄弟節(jié)點(diǎn)。
4、Render Tree:DOM 和 CSSOM 合并后生成 Render Tree,如下圖:
Render Tree 和DOM一樣,以多叉樹的形式保存了每個(gè)節(jié)點(diǎn)的css屬性、節(jié)點(diǎn)本身屬性、以及節(jié)點(diǎn)的孩子節(jié)點(diǎn)。
注意:display:none 的節(jié)點(diǎn)不會(huì)被加入 Render Tree,而 visibility: hidden 則會(huì),所以,如果某個(gè)節(jié)點(diǎn)最開始是不顯示的,設(shè)為 display:none 是更優(yōu)的。(具體可以看這里)
瀏覽器的渲染過程
Create/Update DOM And request css/image/js:瀏覽器請(qǐng)求到HTML代碼后,在生成DOM的最開始階段(應(yīng)該是 Bytes → characters 后),并行發(fā)起css、圖片、js的請(qǐng)求,無論他們是否在HEAD里。
注意:發(fā)起 js 文件的下載 request 并不需要 DOM 處理到那個(gè) script 節(jié)點(diǎn),比如:簡(jiǎn)單的正則匹配就能做到這一點(diǎn),雖然實(shí)際上并不一定是通過正則:)。這是很多人在理解渲染機(jī)制的時(shí)候存在的誤區(qū)。
Create/Update Render CSSOM:CSS文件下載完成,開始構(gòu)建CSSOM
Create/Update Render Tree:所有CSS文件下載完成,CSSOM構(gòu)建結(jié)束后,和 DOM 一起生成 Render Tree。
Layout:有了Render Tree,瀏覽器已經(jīng)能知道網(wǎng)頁(yè)中有哪些節(jié)點(diǎn)、各個(gè)節(jié)點(diǎn)的CSS定義以及他們的從屬關(guān)系。下一步操作稱之為L(zhǎng)ayout,顧名思義就是計(jì)算出每個(gè)節(jié)點(diǎn)在屏幕中的位置。
Painting:Layout后,瀏覽器已經(jīng)知道了哪些節(jié)點(diǎn)要顯示(which nodes are visible)、每個(gè)節(jié)點(diǎn)的CSS屬性是什么(their computed styles)、每個(gè)節(jié)點(diǎn)在屏幕中的位置是哪里(geometry)。就進(jìn)入了最后一步:Painting,按照算出來的規(guī)則,通過顯卡,把內(nèi)容畫到屏幕上。
以上五個(gè)步驟前3個(gè)步驟之所有使用 “Create/Update” 是因?yàn)镈OM、CSSOM、Render Tree都可能在第一次Painting后又被更新多次,比如JS修改了DOM或者CSS屬性。
Layout 和 Painting 也會(huì)被重復(fù)執(zhí)行,除了DOM、CSSOM更新的原因外,圖片下載完成后也需要調(diào)用Layout 和 Painting來更新網(wǎng)頁(yè)。
看 Timeline,一目了然
我扒了一段有贊PC首頁(yè)的代碼到本地,通過Node跑起來。Node作為Server端,對(duì)/js/jquery.js 做了延時(shí)2s返回的處理,并且把<script src="http://127.0.0.1:8080/js/jquery.js"></script> 放到導(dǎo)航欄的下面,結(jié)果是這樣的:
從上面的Timeline我們可以看出:
首屏?xí)r間和DomContentLoad事件沒有必然的先后關(guān)系
所有CSS盡早加載是減少首屏?xí)r間的最關(guān)鍵
js的下載和執(zhí)行會(huì)阻塞Dom樹的構(gòu)建(嚴(yán)謹(jǐn)?shù)卣f是中斷了Dom樹的更新),所以script標(biāo)簽放在首屏范圍內(nèi)的HTML代碼段里會(huì)截?cái)嗍灼恋膬?nèi)容。
script標(biāo)簽放在body底部,做與不做async或者defer處理,都不會(huì)影響首屏?xí)r間,但影響DomContentLoad和load的時(shí)間,進(jìn)而影響依賴他們的代碼的執(zhí)行的開始時(shí)間。
三、問題的答案
回到前面的問題:
script標(biāo)簽的位置會(huì)影響首屏?xí)r間么?
答案是:不影響(如果這里里的首屏指的是頁(yè)面從白板變成網(wǎng)頁(yè)畫面——也就是第一次Painting),但有可能截?cái)嗍灼恋膬?nèi)容,使其只顯示上面一部分。
為什么說是“有可能”呢?,如果該js下載地比css還快,或者script標(biāo)簽不在第一屏的html里,實(shí)際上是不影響的。明白這一影響邊界非常重要,這樣我們?cè)诳疾祉?yè)面性能瓶頸的時(shí)候就有的放矢了。舉個(gè)例子:在網(wǎng)頁(yè)的第二屏有一個(gè)通用模塊,實(shí)際上我們是可以把它的js邏輯獨(dú)立成一個(gè)文件,將模塊的html和js標(biāo)簽放在一起做成獨(dú)立的模板引進(jìn)來的(如果它的js比較小或者說因?yàn)槎嗔艘粋€(gè)文件會(huì)多占用一個(gè)TCP連接和帶寬,這實(shí)際上是另外一個(gè)話題了,請(qǐng)參考我文章開頭的聲明)。
四、總結(jié)、再進(jìn)一步
所以,總算弄清楚這個(gè)眾所周知的常識(shí)了。我們來總結(jié)一下:
如果script標(biāo)簽的位置不在首屏范圍內(nèi),不影響首屏?xí)r間
所有的script標(biāo)簽應(yīng)該放在body底部是很有道理的
但從性能最優(yōu)的角度考慮,即使在body底部的script標(biāo)簽也會(huì)拖慢首屏出來的速度,因?yàn)闉g覽器在最一開始就會(huì)請(qǐng)求它對(duì)應(yīng)的js文件,而這,占用了有限的TCP鏈接數(shù)、帶寬甚至運(yùn)行它所需要的CPU。這也是為什么script標(biāo)簽會(huì)有async或defer屬性的原因之一。
可是,在復(fù)雜的實(shí)際應(yīng)用場(chǎng)景中,要貫徹這幾條結(jié)論可能會(huì)遇到問題,比如:
你的頁(yè)面是分模塊來寫的,每一個(gè)模塊都有自己的html、js甚至css,當(dāng)把這些模塊湊到一個(gè)頁(yè)面中的時(shí)候就會(huì)出現(xiàn)js自然而然地出現(xiàn)在HTML中間部分。你很難把script標(biāo)簽都放到底部
即使你把script標(biāo)簽都放到底部,但script標(biāo)簽的存在終究是拖慢了首屏?xí)r間、DomContendLoad和loaded的時(shí)間。如果只有一個(gè)script標(biāo)簽,我們可以加一個(gè)async,但多個(gè)async的script標(biāo)簽的結(jié)果會(huì)是js文件被亂序執(zhí)行的,這顯然不是我們想要的。
我們也遇到了這樣的問題,所以就做了一個(gè)開源項(xiàng)目:Tiny-Loader —— A small loader that load CSS/JS in best way for page performance 簡(jiǎn)單好用。
瀏覽器頁(yè)面加載解析渲染機(jī)制(一)
JS 一定要放在 Body 的最底部么?聊聊瀏覽器的渲染機(jī)制
HankZhuo的技術(shù)博客