前端渲染過程的二三事

kejiacheng發表於2018-11-12
# 前端渲染過程的二三事
本文不會介紹整個前端渲染過程的步驟,只是記錄最近閱讀的文章的些許思考和感悟。([文章地址一(系列)](https://developers.google.cn/web/fundamentals/performance/critical-rendering-path/),[文章地址二](https://calendar.perfplanet.com/2012/deciphering-the-critical-rendering-path/))

希望大家在閱讀這篇文章之前能將上述文章仔細瀏覽一篇,因為本文所述基本是基於其內容。

## Navigation Timing API和瀏覽器載入事件
Navigation Timing API:可通過列印(performance)檢視;

瀏覽器載入事件:domContentLoaded,onload

前端渲染過程的二三事


上圖整理了Navigation Timing API中的一些事件和瀏覽器載入事件的發生順序。

這些事件的含義是什麼呢?我們直接引用[文章地址一(系列)](https://developers.google.cn/web/fundamentals/performance/critical-rendering-path/measure-crp)的介紹:
- domLoading:這是整個過程的起始時間戳,瀏覽器即將開始解析第一批收到的 HTML 文件位元組。
- domInteractive:表示瀏覽器完成對所有 HTML 的解析並且 DOM 構建完成的時間點。
- domContentLoaded:表示 DOM 準備就緒並且沒有樣式表阻止 JavaScript 執行的時間點,這意味著現在我們可以構建渲染樹了。
- 許多 JavaScript 框架都會等待此事件發生後,才開始執行它們自己的邏輯。因此,瀏覽器會捕獲 EventStart 和 EventEnd 時間戳,讓我們能夠追蹤執行所花費的時間。
- domComplete:顧名思義,所有處理完成,並且網頁上的所有資源(影像等)都已下載完畢,也就是說,載入轉環已停止旋轉。
- loadEvent:作為每個網頁載入的最後一步,瀏覽器會觸發 onload 事件,以便觸發額外的應用邏輯。

看了上述事件的介紹,大致瞭解了每個事件所代表的含義,但也帶著少許疑惑:

1. HTML解析是分批進行的嗎?為什麼要分批進行?
2. domContentLoaded事件結束後可以構建渲染樹了,是否意味著CSSOM樹構建也在該事件之前就已完成?
3. domContentLoaded事件表示DOM準備就緒並且沒有樣式表阻止 JavaScript執行的時間點,可是JavaScript執行是在構建DOM樹之前,那這是不是意味著該事件在DOM樹構建完成後就執行了?若是這與domInteractive事件有什麼區別呢?

首先看第一個問題,我們直接用chrome控制檯的performance來看效果。

前端渲染過程的二三事

上圖中,上方的Network的藍條是HTML的下載時間,下方箭頭所指是HTML的解析時間。

從中我們可以得出HTML解析的確是分批進行,並且解析並不需要等HTML完全下載完。那為什麼要分批進行呢?這個問題我找了許多資料也沒有得出結論,於是自己從中思考。假設不是分批進行,那實現會有兩種情況:1.等HTML全部下載完,再一起解析;2.每下載一定量的HTML就將其放入解析器等待排隊解析。前者首先肯定被排除,若HTML很大,會影響首屏載入速度,後者按理速度更快,或許其實現的難度以及可能會帶來的一些問題而沒有采用?這個問題思考了良久,感覺從理論上是可行的,但從技術角度,由於自己這方面的知識實在薄弱,所以也無法得知其實現的過程是否存在技術瓶頸。

第二個問題:CSSOM樹構建是否在domContentLoaded事件之前?

話不多說我們自己實踐。

首先我先建立一個HTML檔案並寫入少許標籤,再建立一個CSS並於HTML引入,在CSS檔案中寫入大量內容(自己實踐時寫了5W多行)。然後用performance檢視。

前端渲染過程的二三事


上圖每個箭頭代表的含義:
1. HTML解析結束時間
2. domContentLoaded載入時間
3. CSS檔案下載完的時間
4. CSS檔案解析的時間

從這個步驟以及其所處時間,我們可以清晰的得出,該結論不準確。那麼該作者為什麼會得出該結論呢,是他犯錯了嗎?我隨後發現他在這篇文章下面還寫了一句“domContentLoaded一般表示DOM和CSSOM均準備就緒的時間點”。那麼這句話意味著大部分的時候CSSOM樹的構建是在domContentLoaded事件之前。可是這個大部分又指的是什麼情況,這又涉及到另一個知識點“DOM樹,CSSOM樹,JS的三角關係”,構造DOM樹時遇見JS會先解析執行JS,而在解析執行JS時遇到CSSOM,又會先構造CSSOM樹,這個過程稍後會具體說明。那麼現在我們可以明白這個問題的關鍵所在了,因為在大部分頁面中是擁有JS的,而由於其解析順序,那麼在domContentLoaded事件之前必定已經成功構造CSSOM樹。

第三個問題:domInteractive與domContentLoaded的區別是什麼呢?這兩個事件中間是否還會進行其他操作?

我們都知道script標籤有defer和async兩個屬性。有了這兩個屬性,瀏覽器就會加一個程式下載JS。那麼下載完的執行時間點是在什麼時候?其中async會在JS下載完後立馬執行,也正是這個原因,會導致JS的執行順序不一定按標籤的從上至下,而是按照下載完的時間。那麼defer屬性的執行時間呢,我想大家應該都能猜到了,它的執行時間點的確就是在上述的兩個事件之間,那麼我們也就得知這兩個事件的區別所在。

## 三大樹

我們都知道前端渲染有三大樹:DOM樹,CSSOM樹,RENDER樹。那麼這三大樹的構造時間和上述的事件執行時間的順序又是怎樣的呢。

其中DOM樹和RENDER樹所在位置其實是顯而易見的,並且在之前內容也已經指出。DOM樹在domInteractive事件之前,RENDER樹在domContentLoaded事件之後。但是CSSOM樹就難以捉摸,其與DOM樹的關係,完全受到是否擁有JS影響。

在確定CSSOM樹所處位置前,我們先確定一個上面提到的概念:構造DOM樹時遇見JS會先解析執行JS,而在解析執行JS時遇到CSSOM,又會先構造CSSOM樹。官方給出的原因是,JS會使用document.write而改變DOM樹,所以構造DOM樹時碰到JS會先執行JS;而JS在執行時,需要查詢CSS,所以執行JS時,碰到構造CSSOM樹,會先構造CSSOM樹。但是這裡有一點奇怪的時,JS也可以通過創造Link標籤的方式改變CSSOM樹,所以個人感覺官方的這種解釋有點牽強。不過官方的解釋雖然不能讓人完全信服,但這執行順序是不會有錯的。

接下來我們分別通過有無JS兩大類確認CSSOM樹所處位置:
1. 無JS的情況
由於DOM樹和CSSOM樹是並行解析的情況,所以這兩個樹構建完成的順序完全無法固定,只由它們自己本身大小有關。因此CSSOM樹構建完成的時間既可能在domInteractive之前,可能在domContentLoaded之後,也可能在這兩事件之間。
2. 有JS的情況
這裡我們先假設CSS檔案很小,在沒還解析到JS時就已完成解析,那麼這種情況其實必定發生在domInteractive之前,因為都還沒解析到JS,說明DOM樹必定沒有構建完成。那麼再假設CSS檔案很大,然後中途遇到JS檔案,這時候JS檔案發現在構造CSSOM樹,其就會等待CSSOM樹構建完成後再解析執行JS,所以這種情況CSSOM也必定在domInteractive之前。

但是有2個有意思的情況:
(1) 如果我們動態建立link標籤並新增到html中,那麼又會發生什麼呢?若JS還沒解析執行完,那麼會停止JS而去解析CSS,若JS已執行完那麼CSSOM樹其實也不是必定在domInteractive之前。(當然這可能已經不算初次渲染構建)
(2) link標籤放到JS後面又會發生什麼呢?這種情況我發現不管我怎麼嘗試,CSSOM樹必定在DOM樹構建之前構建,但其又在JS執行完成後。如果有興趣的同學可以查查是為什麼。

## 總結
其實前端渲染是一個很龐大的知識點,並且其涉及的周邊知識也及其龐大,本文只是對其中一個小知識點做了思考和實踐。最後要說的一點是,以上內容,純屬個人見解,如有不當,請多指教。


相關文章