說明:javascript
本文提到的瀏覽器均是指Chrome。css
「script標籤「指的都是普通的不帶其餘屬性的外聯javascript。html
web性能優化的手段並非非黑即白的,有些手段過頭了反而下降性能,因此在討論條件和結論的時候,雖然不少條件自己會帶來其餘細微的負面或正面影響,爲了避免使論述失去重點,不會擴展太開。前端
面試前端的時候我喜歡問一些看上去是常識的問題。好比:爲何你們廣泛把
<script src=""></script>
這樣的代碼放在body最底部?(爲了溝通效率,我會提早和對方約定全部的討論都以chrome爲例)java應聘者通常會回答:由於瀏覽器生成Dom樹的時候是一行一行讀HTML代碼的,script標籤放在最後面就不會影響前面的頁面的渲染。node
我很雞賊地接着問:既然Dom樹徹底生成好後頁面才能渲染出來,瀏覽器又必須讀徹底部HTML才能生成完整的Dom樹,script標籤不放在body底部是否是也同樣?jquery
留
一
段
空
白
讓
你
先
想
一
想 web
這實際上是個開放性的問題,裏面涉及的概念的界定自己就很重要。面試
嚴格來講,個人最後一問是有歧義的:咱們須要統一一下什麼叫咱們常常掛在嘴邊的「頁面渲染出來了」 —— 指的是是 「首屏顯示出來了」 仍是 「頁面完整地加載好了」(後面統稱StepC) ?
若是指的是首屏顯示出來了,那麼問題又來了:假設網頁首屏有圖片,這裏的「首屏」 指的是 「顯示了所有圖片的首屏」(後面統稱StepB) 仍是 「沒有圖片的首屏」(後面統稱StepA)。chrome
肯定清楚 「頁面渲染出來了」 指的是 StepA、StepB、StepC 中的哪個是很是關鍵的(雖然至今尚未一個應聘者嘗試這麼作過),若是 「頁面渲染出來了」 指的是 StepC,那麼個人最後一問的答案是確定的——script標籤不放在body底部不會拖慢頁面完整地加載好的時間。
顯然,咱們每每更關心首屏時間,因此,若是 「頁面渲染出來了」 特指「沒有圖片的首屏」,那個人最後一問變成了下面這樣,又該如何回答呢?
既然Dom樹徹底生成好後才能顯示「沒有圖片的首屏」,瀏覽器又必須讀徹底部HTML才能生成完整的Dom樹,script標籤不放在body底部是否是也同樣?
然而上面的問題仍是存在一個陷阱——既然Dom樹徹底生成好後才能顯示「沒有圖片的首屏」
這句話是帶欺騙性的,「沒有圖片的首屏」並不以「完整的Dom樹」爲必要條件。也就是說:在生成Dom樹的過程當中只要某些條件具有了,「沒有圖片的首屏」就能顯示出來。
因此,拋開這些歧義和陷阱,個人問題變成了:
script標籤的位置會影響首屏時間麼?
然而答案並非那麼顯而易見,這得從瀏覽器的渲染機制提及。(再一次說明:本文所說的瀏覽器都是指chrome)
Google Web Fundamentals 是一個很是優秀的文檔,裏面講到了跟web、瀏覽器、前端的方方面面。我總結一下其中的 Ilya Grigorik 寫的 Critical rendering path 瀏覽器渲染機制部分的內容以下:
一、DOM:Document Object Model,瀏覽器將HTML解析成樹形的數據結構,簡稱DOM。
二、CSSOM:CSS Object Model,瀏覽器將CSS代碼解析成樹形的數據結構。
三、DOM 和 CSSOM 都是以 Bytes → characters → tokens → nodes → object model.
這樣的方式生成最終的數據。以下圖所示:
DOM 樹的構建過程是一個深度遍歷過程:當前節點的全部子節點都構建好後纔會去構建當前節點的下一個兄弟節點。
四、Render Tree:DOM 和 CSSOM 合併後生成 Render Tree,以下圖:
Render Tree 和DOM同樣,以多叉樹的形式保存了每一個節點的css屬性、節點自己屬性、以及節點的孩子節點。
注意:display:none
的節點不會被加入 Render Tree,而 visibility: hidden
則會,因此,若是某個節點最開始是不顯示的,設爲 display:none
是更優的。(具體能夠看這裏)
Create/Update DOM And request css/image/js:瀏覽器請求到HTML代碼後,在生成DOM的最開始階段(應該是 Bytes → characters 後),並行發起css、圖片、js的請求,不管他們是否在HEAD裏。
注意:發起 js 文件的下載 request 並不須要 DOM 處理到那個 script 節點,好比:簡單的正則匹配就能作到這一點,雖然實際上並不必定是經過正則:)。這是不少人在理解渲染機制的時候存在的誤區。
Create/Update Render CSSOM:CSS文件下載完成,開始構建CSSOM
Create/Update Render Tree:全部CSS文件下載完成,CSSOM構建結束後,和 DOM 一塊兒生成 Render Tree。
Layout:有了Render Tree,瀏覽器已經能知道網頁中有哪些節點、各個節點的CSS定義以及他們的從屬關係。下一步操做稱之爲Layout,顧名思義就是計算出每一個節點在屏幕中的位置。
Painting:Layout後,瀏覽器已經知道了哪些節點要顯示(which nodes are visible)、每一個節點的CSS屬性是什麼(their computed styles)、每一個節點在屏幕中的位置是哪裏(geometry)。就進入了最後一步:Painting,按照算出來的規則,經過顯卡,把內容畫到屏幕上。
以上五個步驟前3個步驟之全部使用 「Create/Update」 是由於DOM、CSSOM、Render Tree均可能在第一次Painting後又被更新屢次,好比JS修改了DOM或者CSS屬性。
Layout 和 Painting 也會被重複執行,除了DOM、CSSOM更新的緣由外,圖片下載完成後也須要調用Layout 和 Painting來更新網頁。
我扒了一段有贊PC首頁的代碼到本地,經過Node跑起來。Node做爲Server端,對/js/jquery.js
作了延時2s返回的處理,而且把<script src="http://127.0.0.1:8080/js/jquery.js"></script>
放到導航欄的下面,結果是這樣的:
從上面的Timeline咱們能夠看出:
首屏時間和DomContentLoad事件沒有必然的前後關係
全部CSS儘早加載是減小首屏時間的最關鍵
js的下載和執行會阻塞Dom樹的構建(嚴謹地說是中斷了Dom樹的更新),因此script標籤放在首屏範圍內的HTML代碼段裏會截斷首屏的內容。
script標籤放在body底部,作與不作async或者defer處理,都不會影響首屏時間,但影響DomContentLoad和load的時間,進而影響依賴他們的代碼的執行的開始時間。
回到前面的問題:
script標籤的位置會影響首屏時間麼?
答案是:不影響(若是這裏裏的首屏指的是頁面從白板變成網頁畫面——也就是第一次Painting),但有可能截斷首屏的內容,使其只顯示上面一部分。
爲何說是「有可能」呢?,若是該js下載地比css還快,或者script標籤不在第一屏的html裏,其實是不影響的。明白這一影響邊界很是重要,這樣咱們在考察頁面性能瓶頸的時候就有的放矢了。舉個例子:在網頁的第二屏有一個通用模塊,實際上咱們是能夠把它的js邏輯獨立成一個文件,將模塊的html和js標籤放在一塊兒作成獨立的模板引進來的(若是它的js比較小或者說由於多了一個文件會多佔用一個TCP鏈接和帶寬,這其實是另一個話題了,請參考我文章開頭的聲明)。
因此,總算弄清楚這個衆所周知的常識了。咱們來總結一下:
若是script標籤的位置不在首屏範圍內,不影響首屏時間
全部的script標籤應該放在body底部是頗有道理的
但從性能最優的角度考慮,即便在body底部的script標籤也會拖慢首屏出來的速度,由於瀏覽器在最一開始就會請求它對應的js文件,而這,佔用了有限的TCP連接數、帶寬甚至運行它所須要的CPU。這也是爲何script標籤會有async或defer屬性的緣由之一。
但是,在複雜的實際應用場景中,要貫徹這幾條結論可能會遇到問題,好比:
你的頁面是分模塊來寫的,每個模塊都有本身的html、js甚至css,當把這些模塊湊到一個頁面中的時候就會出現js天然而然地出如今HTML中間部分。你很難把script標籤都放到底部
即便你把script標籤都放到底部,但script標籤的存在終究是拖慢了首屏時間、DomContendLoad和loaded的時間。若是隻有一個script標籤,咱們能夠加一個async,但多個async的script標籤的結果會是js文件被亂序執行的,這顯然不是咱們想要的。
咱們也遇到了這樣的問題,因此就作了一個開源項目:Tiny-Loader —— A small loader that load CSS/JS in best way for page performance 簡單好用。
本文首發於個人
SegmentFault專欄:http://segmentfault.com/a/1190000004292479
我的技術博客:http://delai.me/code/js-and-performance/轉載請註明出處