前言
瀏覽器是我們日常開發的重要的工具,那麼你瞭解瀏覽器嗎?即使在前端面試中,我們也經常會遇到:在瀏覽器地址中從輸入url地址到出現頁面,這個過程發生了什麼?介紹一下重繪和迴流?這一類關於瀏覽器的問題。我們可能會知道大概的輪廓但對於具體的細節卻是不那麼清楚,那麼今天我們就從瀏覽器組成開始來了解一下瀏覽器的渲染機制
瀏覽器組成
瀏覽器主要由7個部分組成:
- 使用者介面(User Interface):定義了一些常用的瀏覽器元件,比如位址列,返回、書籤等等
- 資料持久化(Data Persistence):指瀏覽器的cookie、local storage等元件
- 瀏覽器引擎(Browser engine):平臺應用的相關介面,在使用者介面和呈現引擎之間傳送指令。
- 渲染引擎(Rendering engine):處理HTML、CSS的解析與渲染
- JavaScript直譯器(JavaScript Interpreter):解析和執行JavaScript程式碼
- 使用者介面後端(UI Backend):指瀏覽器的的圖形庫等
- 網路(Networking):用於網路呼叫,比如HTTP請求
瀏覽器核心
瀏覽器核心分為兩部分:渲染引擎(layout engineer或Rendering Engine)和JS引擎
- 渲染引擎:負責取得網頁的內容(HTML、XML、影像等等)、整理訊息(例如加入CSS等),以及計算網頁的顯示方式,然後會輸出至顯示器或印表機
- JS引擎:負責解析和執行javascript來實現網頁的動態效果 瀏覽器的核心的不同對於網頁的語法解釋會有不同,所以渲染的效果也不相同。所有網頁瀏覽器、電子郵件客戶端以及其它需要編輯、顯示網路內容的應用程式都需要核心,最開始渲染引擎和JS引擎並沒有區分的很明確,後來JS引擎越來越獨立,核心就傾向於只指渲染引擎
常見的瀏覽器核心:Trident(IE)、Gecko(火狐)、Blink(Chrome、Opera)、Webkit(Safari)
頁面載入流程
在瞭解瀏覽器渲染過程之前,先來了解一下頁面的載入流程。有助於更好理解後續渲染過程。從瀏覽器地址中從輸入url地址到渲染出一個頁面,會經過以下過程。 1.瀏覽器輸入的url地址經過DNS解析獲得對應的IP 2.向伺服器發起TCP的3次握手 3.建立連結後,瀏覽器向該IP地址傳送http請求 4.伺服器接收到請求,返回一堆 HMTL 格式的字串程式碼 5.瀏覽器獲得html程式碼,解析成DOM樹 6.獲取CSS並構建CSSOM 7.將DOM與CSSOM結合,建立渲染樹 8.找到所有內容都處於網頁的哪個位置,佈局渲染樹 9.最終繪製出頁面
瀏覽器渲染機制
我們將要介紹的瀏覽器渲染過程主要步驟是5-9步,可以用下面的圖來形象的展示
解析HTML成DOM樹
這個解析過程大概可以分為幾個步驟:
第一步:瀏覽器從磁碟或網路讀取HTML的原始位元組,也就是傳輸的0和1這樣的位元組資料,並根據檔案的指定編碼(例如 UTF-8)將它們轉換成字串。 第二步:將字串轉換成Token,例如:“”、“”等。Token中會標識出當前Token是“開始標籤”或是“結束標籤”亦或是“文字”等資訊 第三步:在每個Token被生成後,會立刻消耗這個Token建立出節點物件,因此在構建DOM的過程中,不是等待所有的Token都生成後才去構建DOM,而是一邊生成Token一邊消耗來生成節點物件。注意:帶有結束標籤標識的Token不會建立節點物件 第四步:通過“開始標籤”與“結束標籤”來識別並關聯節點之間的關係。當所有Token都生成並消耗完畢後,我們就得到了一顆完整的DOM樹。
但是現在有一個疑問,節點之間的關聯關係是如何維護的呢? 上面我們提到Token會標識是“開始標籤”還是“結束標籤”,以下圖為例:“Hello”Token位於“title”開始標籤與“title”結束標籤之間,表明“Hello”Token是“title”Token的子節點。同理“title”Token是“head”Token的子節點。
構建CSSOM
既然有了html解析,那css解析也是必不可少的,解析css構建CSSOM 的過程和構建DOM的過程非常的相似。當瀏覽器接收到一段CSS,瀏覽器首先要做的是識別出Token,然後構建節點並生成CSSOM
節點中樣式可以通過繼承得到,也可以自己設定,因此在構建的過程中瀏覽器得遞迴 CSSOM 樹,然後確定具體的元素到底是什麼樣式。為了CSSOM的完整性,也只有等構建完畢才能進入到下一個階段,哪怕DOM已經構建完,它也得等CSSOM,然後才能進入下一個階段。CSS匹配HTML元素是一個相當複雜和有效能問題的事情。所以,DOM樹要小,CSS儘量用id和class,千萬不要過渡層疊下去 所以,CSS的載入速度與構建CSSOM的速度將直接影響首屏渲染速度,因此在預設情況下CSS被視為阻塞渲染的資源
構建渲染樹
當我們生成DOM樹和CSSOM樹後,我們需要將這兩顆樹合併成渲染樹,在構建渲染樹的過程中瀏覽器需要做如下工作:
- 從 DOM 樹的根節點開始遍歷每個可見節點。
- 有些節點不可見(例如指令碼Token、元Token等),因為它們不會體現在渲染輸出中,所以會被忽略。
- 某些節點被CSS隱藏,因此在渲染樹中也會被忽略。例如某些節點設定了display: none屬性。
- 對於每個可見節點,為其找到適配的 CSSOM 規則並應用它們
渲染阻塞
在渲染的過程中,遇到一個script標記時,就會停止渲染,去請求指令碼檔案並執行指令碼檔案,因為瀏覽器渲染和 JS 執行共用一個執行緒,而且這裡必須是單執行緒操作,多執行緒會產生渲染 DOM 衝突。JavaScript的載入、解析與執行會嚴重阻塞DOM的構建。只有等到指令碼檔案執行完畢,才會去繼續構建DOM。
js不單會阻塞DOM構建,還會導致CSSOM也阻塞DOM的構建,如果JavaScript指令碼還操作了CSSOM,而正好這個CSSOM還沒有下載和構建,瀏覽器甚至會延遲指令碼執行和構建DOM,直至完成其CSSOM的下載和構建,然後再執行JavaScript,最後在繼續構建DOM
因此script的位置很重要,在實際使用過程中遵循以下兩個原則:
- CSS 優先:引入順序上,CSS 資源先於 JavaScript 資源。
- JS置後:我們通常把JS程式碼放到頁面底部,且JavaScript 應儘量少影響 DOM 的構建
佈局與繪製
瀏覽器拿到渲染樹後,就會從渲染樹的根節點開始遍歷,然後確定每個節點物件在頁面上的確切大小與位置,通常這一行為也被稱為“自動重排”。佈局階段的輸出是一個盒子模型,它會精確地捕獲每個元素在螢幕內的確切位置與大小,所有相對測量值都將轉換為螢幕上的絕對畫素。這一過程也可稱為迴流
佈局完成後,瀏覽器會立即發出“Paint Setup”和“Paint”事件,將渲染樹轉換成螢幕上的畫素。
效能優化策略
在我們瞭解瀏覽器的渲染機制後,DOM 和 CSSOM 結構構建順序,我們可以針對效能優化問題給出一些方案,提升頁面效能。
1.迴流(reflow)與重繪(repaint)
當元素的樣式發生變化時,瀏覽器需要觸發更新,重新繪製元素。這個過程中,有兩種型別的操作,即重繪與迴流。
- 重繪(repaint): 當元素樣式的改變不影響佈局時,瀏覽器將使用重繪對元素進行更新,此時由於只需要UI層面的重新畫素繪製,因此損耗較少
- 迴流(reflow): 當元素的尺寸、結構或觸發某些屬性時,瀏覽器會重新渲染頁面,稱為迴流。此時,瀏覽器需要重新經過計算,計算後還需要重新頁面佈局,因此是較重的操作。會觸發迴流的操作:
- 新增或刪除可見的DOM元素
- 元素的位置發生變化
- 元素的尺寸發生變化(包括外邊距、內邊框、邊框大小、高度和寬度等)
- 內容發生變化,比如文字變化或圖片被另一個不同尺寸的圖片所替代。
- 頁面一開始渲染的時候(這肯定避免不了)
- 瀏覽器的視窗尺寸變化(因為迴流是根據視口的大小來計算元素的位置和大小的
注意:迴流一定會觸發重繪,而重繪不一定會迴流,重繪的開銷較小,迴流的代價較高
因此為了減少效能優化,我們可以儘量避免迴流或者重繪操作 css
- 避免使用table佈局
- 將動畫效果應用到position屬性為absolute或fixed的元素上
javascript
- 避免頻繁操作樣式,可彙總後統一 一次修改
- 儘量使用class進行樣式修改
- 減少dom的增刪次數,可使用 字串 或者 documentFragment 一次性插入
- 極限優化時,修改樣式可將其display: none後修改
- 避免多次觸發上面提到的那些會觸發迴流的方法,可以的話儘量用 變數存住
async和defer的作用是什麼?有什麼區別?
defer 和 async 屬性的區別:
其中藍色線代表JavaScript載入;紅色線代表JavaScript執行;綠色線代表 HTML 解析 1)情況1 <scriptsrc="script.js"> 沒有 defer 或 async,瀏覽器會立即載入並執行指定的指令碼,也就是說不等待後續載入的文件元素,讀到就載入並執行。2)情況2 (非同步下載) async 屬性表示非同步執行引入的 JavaScript,與 defer 的區別在於,如果已經載入好,就會開始執行——無論此刻是 HTML 解析階段還是 DOMContentLoaded 觸發之後。需要注意的是,這種方式載入的 JavaScript 依然會阻塞 load 事件。換句話說,async-script 可能在 DOMContentLoaded 觸發之前或之後執行,但一定在 load 觸發之前執行。
3)情況3 <scriptdefersrc="script.js">(延遲執行) defer 屬性表示延遲執行引入的 JavaScript,即這段 JavaScript 載入時 HTML 並未停止解析,這兩個過程是並行的。整個 document 解析完畢且 defer-script 也載入完成之後(這兩件事情的順序無關),會執行所有由 defer-script 載入的 JavaScript 程式碼,然後觸發 DOMContentLoaded 事件。
defer 與相比普通 script,有兩點區別:
- 載入 JavaScript 檔案時不阻塞 HTML 的解析,執行階段被放到 HTML 標籤解析完成之後;
- 在載入多個JS指令碼的時候,async是無順序的載入,而defer是有順序的載入
js優化可以在script標籤加上 defer屬性 和 async屬性用於在不阻塞頁面文件解析的前提下,控制指令碼的下載和執行
其他: CSS 標籤的 rel屬性 中的屬性值設定為 preload 能夠讓你在你的HTML頁面中可以指明哪些資源是在頁面載入完成後即刻需要的,最優的配置載入順序,提高渲染效能
首屏優化載入
- 減少首屏CGI的計算量:比如在微信8.8無現金日H5開發中,前端希望拿到使用者的個人資訊、消費記錄、排名三類資料,如果只通過一個CGI來處理,那麼後臺響應時間肯定會變長;由於在H5的首屏中,只包含了使用者資訊,消費記錄、排名都在第2屏和第3屏,此時其實可以利用非同步的方式來拿消費記錄、排名的資料。
- 頁面瘦身:壓縮HTML、CSS、JavaScript。
- 減少請求:CSS、JavaScript檔案數儘量少,甚至當CSS、JS的程式碼不多時,可以考慮直接將程式碼內嵌到頁面中。
- 多用快取:快取能大幅度降低頁面非首次載入的時間。
- 少用table佈局,瀏覽器在渲染table時會消耗較多資源,而且只有table裡有一點變化,整個table都會重新渲染。
- 做預載入:部分H5頁面首屏可能要下載較多的靜態資源,比如圖片,這時為了避免載入時出現“難看”的頁面,用預載入(loading的方式)做一個過渡
總結
我們已經將瀏覽器的渲染機制瞭解了一遍,不僅瞭解到一些效能優化方案,也可以得出結論: 瀏覽器渲染的關鍵路徑共分五個步驟:
構建DOM -> 構建CSSOM -> 構建渲染樹 -> 佈局 -> 繪製
參考連結