前端效能優化:細說瀏覽器渲染的重排與重繪

bs123發表於2018-07-22

前端效能優化因為涉及到計算機網路、資料演算法、圖形影像處理、瀏覽器渲染等多方面計算機知識,常作為前端工程師樂此不疲的技術討論話題,也正因如此,它也是面試時容易被問及的面試題之一。

緣起

本篇文章緣起一次偶然的面試問答所引申出的思考整理,著筆於瀏覽器渲染的角度,探討前端效能優化的思路和實踐建議,當然,瀏覽器渲染是一個複雜的過程,本文筆者將圍繞重排和重繪兩個關鍵詞開始行文。

目錄結構

文章大致行文思路如下:

  • URL從輸入到頁面展示的過程

  • DOM和JavaScript的關係

  • 為什麼操作DOM會很“慢”

  • 瀏覽器解析HTML的過程

  • 重排

  • 重繪

  • 優化方案

URL從輸入到頁面展示的過程

在探討瀏覽器解析html之前,先了解url從輸入到最後頁面渲染的過程是一個很有必要的步驟,它可以幫助我們把握整體流程,讓我們在瞭解HTML解析細節之前知道它處於整個請求週期中的哪一階段,這對我們構建完善知識圖譜很有幫助。

首先,我們假設輸入的url的請求為最簡單的Http請求,以GET請求為例,大致分以下幾個步驟:

  1. 使用者在瀏覽器的位址列輸入訪問的URL地址。瀏覽器會先根據這個URL檢視瀏覽器快取-系統快取-路由器快取,若快取中有,直接跳到第6步操作,若沒有,則按照下面的步驟進行操作。

  2. 瀏覽器根據輸入的URL地址解析出主機名。

  3. 瀏覽器將主機名轉換成伺服器ip地址。瀏覽器先查詢本地DNS快取列表,看快取裡面是否存在這個ip,如果有則進入第4步,如果快取中不存在這個ip地址,就再向瀏覽器預設的DNS伺服器傳送查詢請求,同時快取當前這個ip到DNS快取列表中。更詳細步驟參考DNS查詢域名的過程

  4. 拿到ip地址後,瀏覽器再從URL中解析出埠號。

  5. 拿到ip和埠後,瀏覽器會建立一條與目標Web伺服器的TCP連線,也就是傳說中的三次握手。傳送門:完整的tcp連結

  6. 瀏覽器向伺服器傳送一條HTTP請求報文。

  7. 伺服器向瀏覽器返回一條HTTP響應報文。

  8. 關閉連線 瀏覽器解析文件。

  9. 如果文件中有資源則重複6、7、8動作,直至資源全部載入完畢。

以上步驟簡述了瀏覽器從輸入url到最後頁面呈現的大致過程,但這並不很具體,比如瀏覽器請求報文型別是什麼,會遇到哪些錯誤場景、瀏覽器又是如何解析響應報文等等都沒具體描述。

實際上在http請求方式不同、有無代理、有無負載均衡等不同場景下訪問伺服器的細節流程也會有一些差別,但這並不影響我們對整個訪問環節的理解,有興趣的同學可網上自行詳細瞭解,在此不做詳述。

DOM和JavaScript的關係

文件物件模型(DOM)是一個獨立於語言,用於操作XML和HTML文件的API,在web端,我們常用來操作HTML,但其實DOM也是可以操作XML文件的。

我們現在知道,DOM是一個獨立於語言的API,換句話說,DOM是一個與語言無關的API,別的語言也可以實現操作DOM的具體api,但是它在瀏覽器中是用JavaScript來實現的,也因此,DOM是現在JavaScript編碼中很重要的一部分,因為JavaScript很多時候都在操作底層文件。

為什麼操作DOM會很慢

雖然DOM是由JavaScript實現的,但是在瀏覽器中都是把DOM和JavaScript分開來實現的,比如IE中,JavaScript的實現名為JScript,放在jscript.dll檔案中,而DOM則放在另一個叫做mshtml.dll的庫中。在Safari中,DOM和渲染是使用Webkit中的WebCore實現,而JavaScript是由獨立的JavaScriptCore引擎實現,同樣在Chrome中,同樣是使用WebCore來實現渲染,而JavaScript引擎則是他們自己研發的V8引擎。

由於DOM和JavaScript是被分開獨立實現的,因此,每一次在通過js操作DOM的時候,就需要先去連線js和DOM,我們可以這樣理解:把DOM和JavaScript比作兩個島,他們之間通過一個收費的橋連線著,每一次訪問DOM的時候,就需要經過這座橋,並且給“過路費”,訪問的次數越多,路費就會越高,並且訪問到DOM後,操作具體的DOM還需要給“操作費”,由於瀏覽器訪問DOM的操作很多,因此,“路費”和“操作費”自然會增加,這就是為什麼操作DOM會很慢的原因

瀏覽器渲染HTML的步驟

HTML渲染大致分為如下幾步:

  1. HTML被HTML解析器解析成DOM Tree, css則被css解析器解析成CSSOM Tree。

  2. DOM Tree和CSSOM Tree解析完成後,被附加到一起,形成渲染樹(Render Tree)。

  3. 節點資訊計算(重排),這個過程被叫做Layout(Webkit)或者Reflow(Mozilla)。即根據渲染樹計算每個節點的幾何資訊。

  4. 渲染繪製(重繪),這個過程被叫做(Painting 或者 Repaint)。即根據計算好的資訊繪製整個頁面。

以上4步簡述瀏覽器的一次渲染過程,理論上,每一次的dom更改或者css幾何屬性更改,都會引起一次瀏覽器的重排/重繪過程,而如果是css的非幾何屬性更改,則只會引起重繪過程。所以說重排一定會引起重繪,而重繪不一定會引起重排。

重排(Relayout/Reflow)

在弄明白什麼是重排之前,我們要知道:瀏覽器渲染頁面預設採用的是流式佈局模型(Flow Based Layout),這一點很重要。

所謂重排,實際上是根據渲染樹中每個渲染物件的資訊,計算出各自渲染物件的幾何資訊(DOM物件的位置和尺寸大小),並將其安置在介面中的正確位置。

由於瀏覽器渲染介面是基於流式佈局模型的,也就是某一個DOM節點資訊更改了,就需要對DOM結構進行重新計算,重新佈局介面,再次引發迴流,只是這個結構更改程度會決定周邊DOM更改範圍,即全域性範圍和區域性範圍,全域性範圍就是從根節點html開始對整個渲染樹進行重新佈局,例如當我們改變了視窗尺寸或方向或者是修改了根元素的尺寸或者字型大小等;而區域性佈局可以是對渲染樹的某部分或某一個渲染物件進行重新佈局。

在此,總結會引起重排的操作有:

  1. 頁面首次渲染。

  2. 瀏覽器視窗大小發生改變。

  3. 元素尺寸或位置發生改變。

  4. 元素內容變化(文字數量或圖片大小等等)。

  5. 元素字型大小變化。

  6. 新增或者刪除可見的DOM元素。

  7. 啟用CSS偽類(例如::hover)。

  8. 設定style屬性

  9. 查詢某些屬性或呼叫某些方法。

常見引起重排屬性和方法
width height margin padding
display border position overflow
clientWidth clientHeight clientTop clientLeft
offsetWidth offsetHeight offsetTop offsetLeft
scrollWidth scrollHeight scrollTop scrollLeft
scrollIntoView() scrollTo() getComputedStyle()
getBoundingClientRect() scrollIntoViewIfNeeded()

重排也叫回流,實際上,reflow的字面意思也是迴流,之所以有的叫做重排,也許是因為重排更好理解,更符合中國人的思維。標準文件之所以叫做迴流(Reflow),是因為瀏覽器渲染是基於“流式佈局”的模型,流實際就使我們常說的文件流,當dom或者css幾何屬性發生改變的時候,文件流會受到波動聯動的去更改,流就好比一條河裡的水,迴流就好比向河裡扔了一塊石頭,激起漣漪,然後引起周邊水流受到波及,所以叫做迴流,這樣理解似乎更標準更規範,不過叫什麼並不重要,重要的是我們真正理解了這個過程便好。

重繪(Repainting)

相比重排,重繪就簡單多了,所謂重繪,就是當頁面中元素樣式的改變並不影響它在文件流中的位置時,例如更改了字型顏色,瀏覽器會將新樣式賦予給元素並重新繪製的過程稱。

常見引起瀏覽器繪製過程的屬性包含:

color border-style visibility background
text-decoration background-image background-position background-repeat
outline-color outline outline-style border-radius
outline-width box-shadow background-size

效能優化

我們知道操作DOM是一個高成本的操作,不僅是因為本身js與DOM的連結訪問,還包括操作DOM後悔引起一連串的連鎖反應(重排),因此,從效能優化角度,我們可以從以下幾個方面著手:

  • 減少DOM操作

    • 最小化DOM訪問次數,儘量快取訪問DOM的樣式資訊,避免過度觸發迴流。

    • 如果在一個區域性方法中需要多次訪問同一個dom,則先暫存它的引用。

  • 採用更優的API替代消費高的api,轉換優化消費高的集合

    • 用querySelectorAll()替代getElementByXX()。

    • 開啟動畫的GPU加速,把渲染計算交給GPU。

    • 少用HTML集合(類陣列)來遍歷,因為集合遍歷比真陣列遍歷耗費更高。

    • 用事件委託來減少事件處理器的數量。

  • 減少重排

    • 避免設定大量的style屬性,因為通過設定style屬性改變結點樣式的話,每一次設定都會觸發一次reflow,所以最好是使用class屬性

    • 實現元素的動畫,它的position屬性,最好是設為absoulte或fixed,這樣不會影響其他元素的佈局

    • 動畫實現的速度的選擇。比如實現一個動畫,以1個畫素為單位移動這樣最平滑,但是reflow就會過於頻繁,大量消耗CPU資源,如果以3個畫素為單位移動則會好很多。

    • 不要使用table佈局,因為table中某個元素旦觸發了reflow,那麼整個table的元素都會觸發reflow。那麼在不得已使用table的場合,可以設定table-layout:auto;或者是table-layout:fixed這樣可以讓table一行一行的渲染,這種做法也是為了限制reflow的影響範圍

  • css及動畫處理

    • 少用css表示式

    • 減少通過JavaScript程式碼修改元素樣式,儘量使用修改class名方式操作樣式或動畫;

    • 動畫儘量使用在絕對定位或固定定位的元素上;

    • 隱藏在螢幕外,或在頁面滾動時,儘量停止動畫;

最後總結

本篇文章主要抓取url從輸入到最後渲染成介面這一流程中的瀏覽器解析渲染HTML這一步驟來探討前端優化的思路和原因,核心思想基於重排和重繪的關係來展開討論,主題大致有如下幾點:

  • url從輸入到最後渲染的大致環節。

  • 重排一定會重繪,重繪不一定有重排。

  • Js操作DOM是一個高消費過程。

  • 會引起重排/重繪的屬性和方法列舉

  • 優化思路(減少dom操作、替換高效能api、暫存引用、減少重排、開啟硬體加速等)。

最後,由於個人水平原因,若有行文不全或疏漏錯誤之處,懇請各位讀者批評指正,一路有你,不勝感激!

感謝這個時代,讓我們可以站在巨人的肩膀上,窺探程式世界的巨集偉壯觀,我願以一顆赤子心,踏遍程式世界的千山萬水!願每一個行走在程式世界的同仁,都活成心中想要的樣子,加油

相關文章