1. 程式人生 > >前端階段性總結(二):頁面渲染機制與效能優化

前端階段性總結(二):頁面渲染機制與效能優化

引言: 轉前端一年了,期間工作較忙,也沒時間整理一些知識體系,此係列文章是對前端基礎的一些回顧與總結。本文主要介紹瀏覽器工作的原理以及一些優化手段。

一、瀏覽器渲染過程

1. 瀏覽器的主要結構:

clipboard.png

2. 瀏覽器的多程序模型:

以chorme為例:

clipboard.png

  • Browser程序:瀏覽器的主程序,負責瀏覽器介面的顯示,各個頁面的管理,其他各種程序的管理;
  • Renderer程序:頁面的渲染程序,負責頁面的渲染工作,Blink的工作主要在這個程序中完成(主要分成render主執行緒和合成器執行緒);
  • NPAPI外掛程序:每種型別的外掛只會有一個程序,每個外掛程序可以被多個Render程序共享;
  • GPU程序:最多隻有一個,當且僅當GPU硬體加速開啟的時候才會被建立,主要用於對3D加速呼叫的實現;
  • Pepper外掛程序:同NPAPI外掛程序,不同的是為Pepper外掛而建立的程序

需要注意的是,NPAPI是指瀏覽器對系統或外部的一些程式的呼叫介面,比如播放視訊的 flash 外掛,而Pepper其實是基於NPAPI改進的外掛架構。

3. 網頁請求過程:

簡單的url請求過程

4. 瀏覽器渲染過程

a. 主要流程:

主流的瀏覽器核心主要有2種,Webkit 和 Geoko ,雖然 chorme 現在的核心更換為 blink ,但其實 blink是基於webkit的,差異不大。其渲染過程分別如下:

  • webkit

clipboard.png

  • Geoko

clipboard.png

這兩個核心的渲染流程大同小異,主要的過程可以總結為下列5個:

  • DomTree: 解析html構建DOM樹。
  • CssomTree : 解析CSS生成CSSOM規則樹。
  • RenderObjectTree: 將DOM樹與CSSOM規則樹合併在一起生成渲染物件樹。
  • Layout: 遍歷渲染樹開始佈局(layout),計算每個節點的位置大小資訊。
  • Painting: 將渲染樹每個節點繪製到螢幕。

使用chorme瀏覽器的開發者工具,我們很容易看到這5個過程的時間線,下面是segmentfault主頁的渲染截圖:

clipboard.png

可以看到上述流程的耗時,甚至可以統計到每一幀的耗時分佈,從而對影響渲染效能的程式碼精確定位。其中黃色為JS,紫色為Style和Layout,綠色為Paint和Composite部分,選中每個部分會顯示各自的花費時間等資訊,可以看出這個圖片中JS執行的時間太長。目前的顯示裝置一般重新整理率是60FPS,所以理想中每幀的時間最好為16毫秒。

需要注意的一點是,這裡的步驟執行並沒有特定的順序,為保證渲染的速度,瀏覽器一開始接收到html時就開始執行解析的過程,並且遇到需要重繪和重排的時候會重複執行這些步驟,下面我們詳細介紹一下這5個過程。

b. 具體流程

DOM樹的構建:

瀏覽器在接收到html檔案後即開始解析和構建DOM樹,在碰到js程式碼段時,由於js程式碼可能會改變dom的結構,所以為避免重複操作,瀏覽器會停止dom樹構建,先載入並解析js程式碼。而對於css,圖片,視訊等資源,則交由資源載入器去載入,這個過程是非同步的,並不會阻礙dom樹的生成。這個過程需要注意的點是:

  • display:none的元素、註釋存在於dom樹中
  • js會阻塞dom樹的構建從而阻塞其他資源的併發載入,因此好的做法是將js放在最後載入
  • 對於可非同步載入的js片段加上async

CSSOM樹的構建:

瀏覽器在碰到link 和 style 標籤時,會解析css生成cssom , 當然,link標籤需要先將css檔案載入完成才能解析。需要注意的是:

  • js 程式碼會阻塞cssom的構建,在webkit核心中有所優化,只有js訪問css才會阻塞
  • cssom的構建與dom樹的構建是並行的
  • 減少css的巢狀層級和合理的定義css選擇器可以加快解析速度,可參考如何提升 CSS 選擇器效能

RenderObject樹的構建:

在cssom 和dom 樹都構建完成後,瀏覽器會將他們結合,生成渲染物件樹,渲染樹的每一個節點,包含了可見的dom節點和節點的樣式 。需要注意的是:

  • renderObject樹 與 dom樹不是完全對應的,不可見的元素如display:none 是不會放入渲染樹的。
  • visibility: hidden的元素在Render Tree中

佈局:

這一步是瀏覽器遍歷渲染物件樹,並根據裝置螢幕的資訊,計算出節點的佈局、位置,構建出渲染布局樹(render layout)。渲染布局樹輸出的就是我們常說的盒子模型,需要注意的是:

  • float, absolute , fixed 的元素的位置會發生偏移
  • 我們常說的脫離文件流,其實就是脫離佈局樹

繪製:

瀏覽器對生成的佈局樹進行繪製,由使用者介面後端層將每個節點繪製出來。此時,Webkit核心還需要將渲染結果從Renderer程序傳遞到Browser程序。

4. 重繪和迴流

前面講到,js程式碼可以訪問和修改dom節點和css,所以在解析js的過程中會導致頁面重新佈局和渲染,這就是重繪(repaint)和迴流(reflow)。

a. 重繪:

概念:

重繪是指css樣式的改變,但元素的大小和尺寸不變,而導致節點的重新繪製。

重繪的觸發:

任何對元素樣式,如background-color、border-color、visibility 等屬性的改變。css 和 js 都可能引起重繪。

b. 迴流

概念

迴流(reflow)是指元素的大小、位置發生了改變,而導致了佈局的變化,從而導致了佈局樹的重新構建和渲染。

迴流的觸發

  • dom元素的位置和尺寸大小的變化
  • dom元素的增加和刪除
  • 偽類的啟用
  • 視窗大小的變化
  • 增加和刪除class樣式
  • 動態計算修改css樣式

當然,我們的瀏覽器不會每一次reflow都立刻執行,而是會積攢一批,這個過程也被成為非同步reflow,或者增量非同步reflow。但是有些情況瀏覽器是不會這麼做的,比如:resize視窗,改變了頁面預設的字型,等。對於這些操作,瀏覽器會馬上進行reflow。

二、頁面效能分析與測速

優化並不是無目的的,而是通過分析頁面各個維度,找到亟待優化的方向或者具體到某段程式碼。下面就討論一下如何對頁面做效能分析和測速監控。

1.效能分析

Chorme Devtools

chorme得devtools相信所有的前端開發者都用過,它不僅提供了日常開發中極強的除錯能力,同時也具備著極強的頁面分析能力。

第三方分析網站

2.測速上報

測速的關鍵指標

一般來說,我們開啟一個頁面,期望的是頁面的響應和呈現速度和流暢的互動體驗。所以,頁面的測速指標可以大致概括為: 白屏時間,首屏時間,可互動時間。

clipboard.png

如何計算

window.performance是w3c提供的用來測量網頁和Web應用程式的效能api。其中performance timing提供了延時相關的效能資訊,可以高精度測量網站效能。timing的整體結構如下圖所示:

clipboard.png

  • 白屏時間=頁面開始展示的時間點(PerformanceTiming.domLoading)-開始請求時間點(PerformanceTiming.navigationStart)
  • 首屏時間=首屏內容渲染結束時間點(視業務具體情況而定)-開始請求時間點(PerformanceTiming.navigationStart)
  • 可互動時間=使用者可以正常進行事件輸入時間點(PerformanceTIming.domInteractive)-開始請求時間點(PerformanceTiming.navigationStart)

三、效能優化

關於效能優化,涉及的方向太廣了,從網路請求到資料庫,整條鏈路都有其可優化的地方。這裡我只總結一下前端比較需要關注的一些優化點。這裡從兩個個維度進行討論:

(一). 網路請求的優化

從上文可知,瀏覽器渲染網頁的前提是下載相關的資源,html文件、css文件、圖片資源等。這些資源是客戶端基於HTTP協議,通過網路請求從伺服器端請求下載的,大家都知道,有網路,必定有延遲,而資源載入的網路延遲,是頁面緩慢的一個重要因素。所以,如何使資源更快、更合理的載入,是效能優化的必修課。

1. 靜態資源

1)拼接、合併、壓縮、製作雪碧圖:

由於HTTP的限制,在建立一個tcp請求時需要一些耗時,所以,我們對資源進行合併、壓縮,其目的是減少http請求數和減小包體積,加快傳輸速度。

  • 拼接、合併、壓縮: 在現代的前端工程化開發流程中,相信大家都有使用webpack或者gulp等打包工具對資源(js、css、圖片等)進行打包、合併、去重、壓縮。在這基礎上,我們需要根據自身的業務,合理的對公共程式碼,公共庫,和首屏程式碼進行單獨的打包壓縮,按需載入;
  • 雪碧圖:對於圖片資源,我們可以製作雪碧圖,即對一些頁面上的icon和小圖示,整合到一張圖片上,css使用背景圖定位來使用不同的icon,這樣做可以有效的減少圖片的請求數,降低網路延遲。而它的缺點也很明顯,由於整合在同一張圖片上,使用其中的一個圖示,就需要將整張圖片下載下來,所以,雪碧圖不能盲目的使用。

clipboard.png

segmentfault.com 的雪碧圖圖示

2)CDN資源分發:

將一些靜態資原始檔託管在第三方CDN服務中,一方面可以減少伺服器的壓力,另一方面,CDN的優勢在於,CDN系統能夠實時地根據網路流量和各節點的連線、負載狀況以及到使用者的距離和響應時間等綜合資訊將使用者的請求重新導向離使用者最近的服務節點上,保證資源的載入速度和穩定性。

3)快取:

快取的範圍很廣,比如協議層的DNS解析快取、代理伺服器快取,到客戶端的瀏覽器本地快取,再到服務端的快取。一個網路鏈路的每個環節都有被快取的空間。快取的目的是簡化資源的請求路徑,比如某些靜態資源在客戶端已經快取了,再次請求這個資源,只需要使用本地的快取,而無需走網路請求去服務端獲取。

clipboard.png

segmentfault 的主頁的一些靜態資源使用了快取,上面是一些控制快取的header首部欄位

4)分片:

分片指得是將資源分佈到不同的主機,這是為了突破瀏覽器對同一臺主機建立tcp連線的數量限制,一般為6~8個。現代網站的資源數量有50~100個很常見,所以將資源分佈到不同的主機上,可以建立更多的tcp請求,降低請求耗時,從而提升網頁速度。

clipboard.png

從segmentfault 的主頁請求可以看出,網站將靜態js檔案和圖片都放在了不同的子域名下。

5)升級協議:

可以升級我們的網路協議,比如使用HTTP2,quic 之類的,代替之前的http1.1,從協議層優化資源的載入。可以參考我之前的文章。

2. 業務資料

雖然做好了靜態資料的載入優化,但是還是會出現一種情景,即靜態資料已經載入完畢,但頁面還是在轉菊花,頁面還沒有進入可互動狀態,這是因為現如今的網站開發模式,前後端分離已經成為主流,不再由php或jsp服務端渲染前端頁面,而是前端先載入靜態資料,再通過ajax非同步獲取伺服器的資料,進而重新渲染頁面。這就導致了非同步從介面獲取資料也是網頁的一個性能瓶頸。響應緩慢,不穩定的介面,會導致使用者互動體驗極差,頁面渲染速度也不理想。比如點選一個提交資料的按鈕,介面速度慢,頁面上菊花需要轉好久才能交換完資料。

1)首屏直出

為了提升使用者體驗,我們認為首屏的渲染速度是極為重要的,使用者進來頁面,首頁可見區域的載入可以由服務端渲染,保證了首屏載入速度,而不可見的部分則可以非同步載入,甚至做到子路由頁面的預載入。業界已經有很多同構直出的方案,比如vue的nuxt , react的beidou等。

2)介面合併

前端經常有這樣的場景,完成一個功能需要先請求第一個介面獲得資料,然後再根據資料請求第二個介面獲取第二個資料,然後第三、第四...前端通常需要通過promise或者回調,一層一層的then下去,這樣顯然是很消耗效能的clipboard.png

通常後臺介面都按一定的粒度存在的,不可能一個介面滿足所有的場景。這是不可避免的,那麼如何做到只發送一個請求就能實現功能呢?有一種不錯的方案是,代理伺服器實現請求合併,即後臺的介面只需要保證健壯和分散式,而由nodejs(當然也可以使用其他語言)建設一層代理中間層,流程如下圖所示:

clipboard.png

前端只需要按找約定的規則,向代理伺服器發起一次請求,由代理伺服器向介面伺服器發起三次請求,再將目標資料返回給客戶端。這樣做的好處是:一方面是代理伺服器代替前端做了介面合併,減少了前端的請求數量;另一方面代理伺服器可以脫離HTTP的限制,使用更高效的通訊協議與伺服器通訊;

(二). 頁面渲染效能的優化

1. 防止阻塞渲染

頁面中的css 和 js 會阻塞html的解析,因為他們會影響dom樹和render樹。為了避免阻塞,我們可以做這些優化:

  • css 放在首部,提前載入,這樣做的原因是: 通常情況下 CSS 被認為是阻塞渲染的資源,在CSSOM 構建完成之前,頁面不會被渲染,放在頂部讓樣式表能夠儘早開始載入。但如果把引入樣式表的 link 放在文件底部,頁面雖然能立刻呈現出來,但是頁面加載出來的時候會是沒有樣式的,是混亂的。當後來樣式表載入進來後,頁面會立即進行重繪,這也就是通常所說的閃爍了。
  • js檔案放在底部,防止阻塞解析
  • 一些不改變dom和css的js 使用 defer 和 async 屬性告訴瀏覽器可以非同步載入,不阻塞解析

2. 減少重繪和迴流

重繪和迴流在實際開發中是很難避免的,我們能做的就是儘量減少這種行為的發生。

  • js儘量少訪問dom節點和css 屬性
  • 儘可能的為產生動畫的 HTML 元素使用 fixed 或 absolute 的 position ,那麼修改他們的 CSS 是不會 Reflow 的。
  • img標籤要設定高寬,以減少重繪重排
  • 把DOM離線後修改,如將一個dom脫離文件流,比如display:none ,再修改屬性,這裡只發生一次迴流。
  • 儘量用 transform 來做形變和位移,不會造成迴流

3. 提高程式碼質量

這最能體現一個前端工程師的水平了,高效能的程式碼能在實現功能的同時,還兼顧效能。下面是一些好的實踐:

1)html:

  • dom的層級儘量不要太深,否則會增加dom樹構建的時間,js訪問深層的dom也會造成更大的負擔。
  • meta標籤裡需要定義文件的編碼,便於瀏覽器解析

2)css:

3)js:

  • 減少通過JavaScript程式碼修改元素樣式,儘量使用修改class名方式操作樣式或動畫
  • 訪問dom節點時需要對dom節點轉存,防止迴圈中重複訪問dom節點造成效能損耗。
  • 慎用 定時器 和 計時器, 使用完後需要銷燬。
  • 用於複雜計算的js程式碼可以放在worker程序中執行
  • 對於一些高頻的回撥需要對其節流和消抖,就是 debounce 和 throttle 這兩個函式。比如sroll和touch事件

...

優化沒有正確答案,優化的手段也層出不窮,這裡也無法概括全面,只列舉了一些我瞭解過的。其實除了前端,後端也有許多可優化的地方,比如介面快取啊,資料庫快取啊等等。這個本騷年就瞭解的不深了。

四、思考與總結

效能一直是前端開發很重要的一個課題。效能優化也是一條不見盡頭的路,任重而道遠啊~