2016年5月3日 星期二

JS 一定要放在Body 的最底部麼?聊聊瀏覽器的渲染機制


我們需要統一一下什麼叫我們經常掛在嘴邊的“頁面渲染出來了” ——指的是是“首屏顯示出來了”還是“頁面完整地加載好了”(後面統稱StepC) ? 
如果指的是首屏顯示出來了,那麼問題又來了:假設網頁首屏有圖片,這裡的“首屏”指的是“顯示了全部圖片的首屏”(後面統稱StepB)還是“沒有圖片的首屏”(後面統稱StepA)。

確定清楚“頁面渲染出來了” 指的是StepA、StepB、StepC 中的哪一個是非常關鍵的(雖然至今還沒有一個應聘者嘗試這麼做過),如果“頁面渲染出來了” 指的是StepC,那麼我的最後一問的答案是肯定的——script標籤不放在body底部不會拖慢頁面完整地加載好的時間。

瀏覽器的渲染過程

  1. Create/Update DOM And request css/image/js:瀏覽器請求到HTML代碼後,在生成DOM的最開始階段(應該是Bytes → characters後),並行發起css、圖片、js的請求,無論他們是否在HEAD裡。注意:發起js文件的下載request並不需要DOM處理到那個script節點,比如:簡單的正則匹配就能做到這一點,雖然實際上並不一定是通過正則:)。這是很多人在理解渲染機制的時候存在的誤區。
  2. Create/Update Render CSSOM:CSS文件下載完成,開始構建CSSOM
  3. Create/Update Render Tree:所有CSS文件下載完成,CSSOM構建結束後,和DOM一起生成Render Tree。
  4. Layout:有了Render Tree,瀏覽器已經能知道網頁中有哪些節點、各個節點的CSS定義以及他們的從屬關係。下一步操作稱之為Layout,顧名思義就是計算出每個節點在屏幕中的位置。
  5. 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來更新網頁。


  • 首屏時間和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屬性的原因之一。

沒有留言:

張貼留言