從渲染原理談前端性能優化

做者:李佳曉 原文:學而思網校技術團隊css

前言

合格的開發者知道怎麼作,而優秀的開發者知道爲何這麼作。html

這句話來自《web性能權威指南》,我一直很喜歡,而本文嘗試從瀏覽器渲染原理探討如何進行性能提高。
全文將從網絡通訊以及頁面渲染兩個過程去探討瀏覽器的行爲及在此過程當中咱們能夠針對那些點進行優化,有些的不足之處還請各位不吝雅正。前端

1、關於瀏覽器渲染的容易誤解點總結

關於瀏覽器渲染機制已是老生常談,並且網上現有資料中有很是多的優秀資料對此進行闡述。遺憾的是網上的資料參差不齊,常常在不一樣的文檔中對同一件事的描述出現了極大的差別。懷着嚴謹求學的態度通過大量資料的查閱和請教,將會在後文總結出一個完整的流程。linux

一、DOM樹的構建是文檔加載完成開始的?

DOM樹的構建是從接受到文檔開始的,先將字節轉化爲字符,而後字符轉化爲標記,接着標記構建dom樹。這個過程被分爲標記化和樹構建
而這是一個漸進的過程。爲達到更好的用戶體驗,呈現引擎會力求儘快將內容顯示在屏幕上。它沒必要等到整個 HTML 文檔解析完畢以後,就會開始構建呈現樹和設置佈局。在不斷接收和處理來自網絡的其他內容的同時,呈現引擎會將部份內容解析並顯示出來。
參考文檔:
http://taligarsiel.com/Projec...nginx

二、渲染樹是在DOM樹和CSS樣式樹構建完畢纔開始構建的嗎?

這三個過程在實際進行的時候又不是徹底獨立,而是會有交叉。會形成一邊加載,一邊解析,一邊渲染的工做現象。
參考文檔:web

http://www.jianshu.com/p/2d52...算法

三、css的標籤嵌套越多,越容易定位到元素

css的解析是自右至左逆向解析的,嵌套越多越增長瀏覽器的工做量,而不會越快。
由於若是正向解析,例如「div div p em」,咱們首先就要檢查當前元素到 html 的整條路徑,找到最上層的 div,再往下找,若是遇到不匹配就必須回到最上層那個 div,往下再去匹配選擇器中的第一個 div,回溯若干次才能肯定匹配與否,效率很低。
逆向匹配則不一樣,若是當前的 DOM 元素是 div,而不是 selector 最後的 em,那隻要一步就能排除。只有在匹配時,纔會不斷向上找父節點進行驗證。
打個好比 p span.showing
你認爲從一個p元素下面找到全部的span元素並判斷是否有class showing快,仍是找到全部的span元素判斷是否有class showing而且包括一個p父元素快
參考文檔:
http://www.imooc.com/code/4570chrome

2、頁面渲染的完整流程

當瀏覽器拿到HTTP報文時呈現引擎將開始解析 HTML 文檔,並將各標記逐個轉化成「內容樹」上的 DOM 節點。同時也會解析外部 CSS 文件以及樣式元素中的樣式數據。HTML 中這些帶有視覺指令的樣式信息將用於建立另外一個樹結構:呈現樹。瀏覽器將根據呈現樹進行佈局繪製。數據庫

以上就是頁面渲染的大體流程。那麼瀏覽器從用戶輸入網址以後到底作了什麼呢?如下將會進行一個完整的梳理。鑑於本文是前端向的因此梳理內容會有所偏重。而從輸入到呈現能夠分爲兩個部分:網絡通訊頁面渲染跨域

咱們首先來看網絡通訊部分:

一、用戶輸入url並敲擊回車。

二、進行DNS解析。

若是用戶輸入的是ip地址則直接進入第三條。但去記錄毫無規律且冗長的ip地址顯然不是易事,因此一般都是輸入的域名,此時就會進行dns解析。所謂DNS(Domain Name System)指域名系統。因特網上做爲域名和IP地址相互映射的一個分佈式數據庫,可以使用戶更方便的訪問互聯網,而不用去記住可以被機器直接讀取的IP數串。經過主機名,最終獲得該主機名對應的IP地址的過程叫作域名解析(或主機名解析)。這個過程以下所示:

瀏覽器會首先搜索瀏覽器自身的DNS緩存(緩存時間比較短,大概只有2分鐘左右,且只能容納1000條緩存)。

  • 若是瀏覽器自身緩存找不到則會查看系統的DNS緩存,若是找到且沒有過時則中止搜索解析到此結束.
  • 而若是本機沒有找到DNS緩存,則瀏覽器會發起一個DNS的系統調用,就會向本地配置的首選DNS服務器發起域名解析請求(經過的是UDP協議向DNS的53端口發起請求,這個請求是遞歸的請求,也就是運營商的DNS服務器必須得提供給咱們該域名的IP地址),運營商的DNS服務器首先查找自身的緩存,找到對應的條目,且沒有過時,則解析成功。
  • 若是沒有找到對應的條目,則有運營商的DNS代咱們的瀏覽器發起迭代DNS解析請求,它首先是會找根域的DNS的IP地址(這個DNS服務器都內置13臺根域的DNS的IP地址),找打根域的DNS地址,就會向其發起請求(請問www.xxxx.com這個域名的IP地址是多少啊?)
  • 根域發現這是一個頂級域com域的一個域名,因而就告訴運營商的DNS我不知道這個域名的IP地址,可是我知道com域的IP地址,你去找它去,因而運營商的DNS就獲得了com域的IP地址,又向com域的IP地址發起了請求(請問www.xxxx.com這個域名的IP地址是多少?),com域這臺服務器告訴運營商的DNS我不知道www.xxxx.com這個域名的IP地址,可是我知道xxxx.com這個域的DNS地址,你去找它去,因而運營商的DNS又向linux178.com這個域名的DNS地址(這個通常就是由域名註冊商提供的,像萬網,新網等)發起請求(請問www.xxxx.com這個域名的IP地址是多少?),這個時候xxxx.com域的DNS服務器一查,誒,果然在我這裏,因而就把找到的結果發送給運營商的DNS服務器,這個時候運營商的DNS服務器就拿到了www.xxxx.com這個域名對應的IP地址,並返回給Windows系統內核,內核又把結果返回給瀏覽器,終於瀏覽器拿到了www.xxxx.com對應的IP地址,此次dns解析圓滿成功。

三、創建tcp鏈接

拿到域名對應的IP地址以後,User-Agent(通常是指瀏覽器)會以一個隨機端口(1024< 端口 < 65535)向服務器的WEB程序(經常使用的有httpd,nginx等)80端口發起TCP的鏈接請求。這個鏈接請求(原始的http請求通過TCP/IP4層模型的層層封包)到達服務器端後(這中間經過各類路由設備,局域網內除外),進入到網卡,而後是進入到內核的TCP/IP協議棧(用於識別該鏈接請求,解封包,一層一層的剝開),還有可能要通過Netfilter防火牆(屬於內核的模塊)的過濾,最終到達WEB程序,最終創建了TCP/IP的鏈接。

tcp創建鏈接和關閉鏈接均須要一個完善的確認機制,咱們通常將鏈接稱爲三次握手,而鏈接關閉稱爲四次揮手。而不管是三次握手仍是四次揮手都須要數據從客戶端到服務器的一次完整傳輸。將數據從客戶端到服務端經歷的一個完整時延包括:

  • 發送時延:把消息中的全部比特轉移到鏈路中須要的時間,是消息長度和鏈路速度的函數
  • 傳播時延:消息從發送端到接受端須要的時間,是信號傳播距離和速度的函數
  • 處理時延:處理分組首部,檢查位錯誤及肯定分組目標所需的時間
  • 排隊時延:到來的分組排隊等待處理的時間以上的延遲總和就是客戶端到服務器的總延遲時間

以上的延遲總和就是客戶端到服務器的總延遲時間。所以每一次的鏈接創建和斷開都是有巨大代價的。所以去掉沒必要要的資源和資源合併(包括js及css資源合併、雪碧圖等)纔會成爲性能優化繞不開的方案。可是好消息是隨着協議的發展咱們將對性能優化這個主題有着新的見解和思考。雖然還未到來,但也不遠了。若是你感到好奇那就接着往下看。

如下簡述下tcp創建鏈接的過程:

clipboard.png

  • 第一次握手:客戶端發送syn包(syn=x,x爲客戶端隨機序列號)的數據包到服務器,並進入SYN_SEND狀態,等待服務器確認;
  • 第二次握手:服務器收到syn包,必須確認客戶的SYN(ack=x+1),同時本身也發送一個SYN包(syn=y,y爲服務端生成的隨機序列號),即SYN+ACK包,此時服務器進入SYN_RECV狀態;
  • 第三次握手:客戶端收到服務器的SYN+ACK包,向服務器發送確認包ACK(ack=y+1)

此包發送完畢,客戶端和服務器進入ESTABLISHED狀態,完成三次握手。握手過程當中傳送的包裏不包含數據,三次握手完畢後,客戶端與服務器才正式開始傳送數據。理想狀態下,TCP鏈接一旦創建,在通訊雙方中的任何一方主動關閉鏈接以前,TCP鏈接都將被一直保持下去

這裏注意, 三次握手是不攜帶數據的,而是在握手完畢纔開始數據傳輸。所以若是每次數據請求都須要從新進行完整的tcp鏈接創建,通訊時延的耗時是難以估量的!這也就是爲何咱們老是能聽到資源合併減小請求次數的緣由。

下面來看看HTTP如何在協議層面幫咱們進行優化的:

HTTP1.0

在http1.0時代,每一個TCP鏈接只能發送一個請求。發送數據完畢,鏈接就關閉,若是還要請求其餘資源,就必須再新建一個鏈接。 TCP鏈接的新建成本很高,由於須要客戶端和服務器三次握手,而且開始時發送速率較慢(TCP的擁塞控制開始時會啓動慢啓動算法)。在數據傳輸的開始只能發送少許包,並隨着網絡狀態良好(無擁塞)指數增加。但遇到擁塞又要從新從1個包開始進行傳輸。

如下圖爲例,慢啓動時第一次數據傳輸只能傳輸一組數據,獲得確認後傳輸2組,每次翻倍,直到達到閾值16時開始啓用擁塞避免算法,既每次獲得確認後數據包只增長一個。當發生網絡擁塞後,閾值減半從新開始慢啓動算法。

clipboard.png

所以爲避免tcp鏈接的三次握手耗時及慢啓動引發的發送速度慢的狀況,應儘可能減小tcp鏈接的次數。

而HTTP1.0每一個數據請求都須要從新創建鏈接的特色使得HTTP 1.0版本的性能比較差。隨着網頁加載的外部資源愈來愈多,這個問題就愈發突出了。 爲了解決這個問題,有些瀏覽器在請求時,用了一個非標準的Connection字段。 Kepp-alive 一個能夠複用的TCP鏈接就創建了,直到客戶端或服務器主動關閉鏈接。可是,這不是標準字段,不一樣實現的行爲可能不一致,所以不是根本的解決辦法。

HTTP1.1

http1.1(如下簡稱h1.1) 版的最大變化,就是引入了持久鏈接(persistent connection),即TCP鏈接默認不關閉,能夠被多個請求複用,不用聲明Connection: keep-alive。 客戶端和服務器發現對方一段時間沒有活動,就能夠主動關閉鏈接。不過,規範的作法是,客戶端在最後一個請求時,發送Connection: close,明確要求服務器關閉TCP鏈接。 目前,對於同一個域名,大多數瀏覽器容許同時創建6個持久鏈接。相比與http1.0,1.1的頁面性能有了巨大提高,由於省去了不少tcp的握手揮手時間。下圖第一種是tcp創建後只能發一個請求的http1.0的通訊狀態,而擁有了持久鏈接的h1.1則避免了tcp握手及慢啓動帶來的漫長時延。

clipboard.png

從圖中能夠看到相比h1.0,h1.1的性能有所提高。然而雖然1.1版容許複用TCP鏈接,可是同一個TCP鏈接裏面,全部的數據通訊是按次序進行的。服務器只有處理完一個迴應,纔會進行下一個迴應。要是前面的迴應特別慢,後面就會有許多請求排隊等着。這稱爲"隊頭堵塞"(Head-of-line blocking)。 爲了不這個問題,只有三種方法:一是減小請求數,二是同時多開持久鏈接。這致使了不少的網頁優化技巧,好比合並腳本和樣式表、將圖片嵌入CSS代碼、域名分片(domain sharding)等等。若是HTTP協議能繼續優化,這些額外的工做是能夠避免的。三是開啓pipelining,不過pipelining並非救世主,它也存在很多缺陷:

    • pipelining只能適用於http1.1,通常來講,支持http1.1的server都要求支持pipelining
    • 只有冪等的請求(GET,HEAD)能使用pipelining,非冪等請求好比POST不能使用,由於請求之間可能會存在前後依賴關係。
    • head of line blocking並無徹底獲得解決,server的response仍是要求依次返回,遵循FIFO(first in first out)原則。也就是說若是請求1的response沒有回來,2,3,4,5的response也不會被送回來。
    • 絕大部分的http代理服務器不支持pipelining。 和不支持pipelining的老服務器協商有問題。 可能會致使新的隊首阻塞問題。

    鑑於以上種種緣由,pipelining的支持度並不友好。能夠看看chrome對pipelining的描述:

    https://www.chromium.org/deve...

    clipboard.png

    HTTP2

    2015年,HTTP/2 發佈。它不叫 HTTP/2.0,是由於標準委員會不打算再發布子版本了,下一個新版本將是 HTTP/3。HTTP2將具備如下幾個主要特色:

    • 二進制協議 :HTTP/1.1 版的頭信息確定是文本(ASCII編碼),數據體能夠是文本,也能夠是二進制。HTTP/2 則是一個完全的二進制協議,頭信息和數據體都是二進制,而且統稱爲"幀"(frame):頭信息幀和數據幀。
    • 多工 :HTTP/2 複用TCP鏈接,在一個鏈接裏,客戶端和瀏覽器均可以同時發送多個請求或迴應,並且不用按照順序一一對應,這樣就避免了"隊頭堵塞"。
    • 數據流:由於 HTTP/2 的數據包是不按順序發送的,同一個鏈接裏面連續的數據包,可能屬於不一樣的迴應。所以,必需要對數據包作標記,指出它屬於哪一個迴應。 HTTP/2 將每一個請求或迴應的全部數據包,稱爲一個數據流(stream)。每一個數據流都有一個獨一無二的編號。數據包發送的時候,都必須標記數據流ID,用來區分它屬於哪一個數據流。另外還規定,客戶端發出的數據流,ID一概爲奇數,服務器發出的,ID爲偶數。 數據流發送到一半的時候,客戶端和服務器均可以發送信號(RST_STREAM幀),取消這個數據流。1.1版取消數據流的惟一方法,就是關閉TCP鏈接。這就是說,HTTP/2 能夠取消某一次請求,同時保證TCP鏈接還打開着,能夠被其餘請求使用。 客戶端還能夠指定數據流的優先級。優先級越高,服務器就會越早迴應。
    • 頭信息壓縮: HTTP 協議不帶有狀態,每次請求都必須附上全部信息。因此,請求的不少字段都是重複的,好比Cookie和User Agent,如出一轍的內容,每次請求都必須附帶,這會浪費不少帶寬,也影響速度。 HTTP2對這一點作了優化,引入了頭信息壓縮機制(header compression)。一方面,頭信息使用gzip或compress壓縮後再發送;另外一方面,客戶端和服務器同時維護一張頭信息表,全部字段都會存入這個表,生成一個索引號,之後就不發送一樣字段了,只發送索引號,這樣就提升速度了。
    • 服務器推送: HTTP/2 容許服務器未經請求,主動向客戶端發送資源,這叫作服務器推送(server push)。 常見場景是客戶端請求一個網頁,這個網頁裏面包含不少靜態資源。正常狀況下,客戶端必須收到網頁後,解析HTML源碼,發現有靜態資源,再發出靜態資源請求。其實,服務器能夠預期到客戶端請求網頁後,極可能會再請求靜態資源,因此就主動把這些靜態資源隨着網頁一塊兒發給客戶端了。

    就這幾個點咱們分別討論一下:
    就多工來看:雖然http1.1支持了pipelining,可是仍然會有隊首阻塞問題,若是瀏覽器同時發出http請求請求和css,服務器端處理css請求耗時20ms,可是由於先請求資源是html,此時的css儘管已經處理好了但仍不能返回,而須要等待html處理好一塊兒返回,此時的客戶端就處於盲等狀態,而事實上若是服務器先處理好css就先返回css的話,瀏覽器就能夠開始解析css了。而多工的出現就解決了http以前版本協議的問題,極大的提高了頁面性能。縮短了通訊時間。咱們來看看有了多工以後有那些影響:

    • 無需進行資源分片:爲了不請求tcp鏈接耗時長的和初始發送速率低的問題,瀏覽器容許同時打開多個tcp鏈接讓資源同時請求。可是爲了不服務器壓力,通常針對一個域名會有最大併發數的限制,通常來講是6個。容許一個頁面同時對相同域名打開6個tcp鏈接。爲了繞過最大併發數的限制,會將資源分佈在不一樣的域名下,避免資源在超過併發數後須要等待才能開始請求。而有了http2,能夠同步請求資源,資源分片這種方式就能夠再也不使用。
    • 資源合併:資源合併會不利於緩存機制,由於單文件修改會影響整個資源包。並且單文件過大對於 HTTP/2 的傳輸很差,儘可能作到細粒化更有利於 HTTP/2 傳輸。並且內置資源也是同理,將資源以base64的形式放進代碼中不利於緩存。且編碼後的圖片資源大小是要超過圖片大小的。這二者都是以減小tcp請求次數增大單個文件大小來進行優化的。

    就頭部壓縮來看:HTTP/1.1 版的頭信息是ASCII編碼,也就是不通過壓縮的,當咱們請求只攜帶少許數據時,http頭部可能要比載荷要大許多,尤爲是有了很長的cookie以後這一點尤其顯著,頭部壓縮毫無疑問能夠對性能有很大提高。

    就服務器推送來看:少去了資源請求的時間,服務端能夠將可能用到的資源推送給服務端以待使用。這項能力幾乎是革新了以前應答模式的認知,對性能提高也有巨大幫助。

    所以不少優化都是在基於tcp及http的一些問題來避免和繞過的。事實上多數的優化都是針對網絡通訊這個部分在作。

    四、創建TCP鏈接後發起http請求

    五、服務器端響應http請求,瀏覽器獲得html代碼

    以上是網絡通訊部分,接下來將會對頁面渲染部分進行敘述。

    • 當瀏覽器拿到HTML文檔時首先會進行HTML文檔解析,構建DOM樹。
    • 遇到css樣式如link標籤或者style標籤時開始解析css,構建樣式樹。HTML解析構建和CSS的解析是相互獨立的並不會形成衝突,所以咱們一般將css樣式放在head中,讓瀏覽器儘早解析css。
    • 當html的解析遇到script標籤會怎樣呢?答案是中止DOM樹的解析開始下載js。由於js是會阻塞html解析的,是阻塞資源。其緣由在於js可能會改變html現有結構。例若有的節點是用js動態構建的,在這種狀況下就會中止dom樹的構建開始下載解析js。腳本在文檔的何處插入,就在何處執行。當 HTML 解析器遇到一個 script 標記時,它會暫停構建 DOM,將控制權移交給 JavaScript 引擎;等 JavaScript 引擎運行完畢,瀏覽器會從中斷的地方恢復 DOM 構建。而所以就會推遲頁面首繪的時間。能夠在首繪不須要js的狀況下用async和defer實現異步加載。這樣js就不會阻塞html的解析了。當HTML解析完成後,瀏覽器會將文檔標註爲交互狀態,並開始解析那些處於「deferred」模式的腳本,也就是那些應在文檔解析完成後才執行的腳本。而後,文檔狀態將設置爲「完成」,一個「加載」事件將隨之觸發。

    注意,異步執行是指下載。執行js時仍然會阻塞。

    • 在獲得DOM樹和樣式樹後就能夠進行渲染樹的構建了。應注意的是渲染樹和 DOM 元素相對應的,但並不是一一對應。好比非可視化的 DOM 元素不會插入呈現樹中,例如「head」元素。若是元素的 display 屬性值爲「none」,那麼也不會顯示在呈現樹中(可是 visibility 屬性值爲「hidden」的元素仍會顯示)

    clipboard.png

    渲染樹構建完畢後將會進行佈局。佈局使用流模型的Layout算法。所謂流模型,便是指Layout的過程只需進行一遍便可完成,後出如今流中的元素不會影響前出如今流中的元素,Layout過程只需從左至右從上至下一遍完成便可。但實際實現中,流模型會有例外。Layout是一個遞歸的過程,每一個節點都負責本身及其子節點的Layout。Layout結果是相對父節點的座標和尺寸。其過程能夠簡述爲:

    clipboard.png

    • 此時renderTree已經構建完畢,不過瀏覽器渲染樹引擎並不直接使用渲染樹進行繪製,爲了方便處理定位(裁剪),溢出滾動(頁內滾動),CSS轉換/不透明/動畫/濾鏡,蒙版或反射,Z (Z排序)等,瀏覽器須要生成另一棵樹 - 層樹。所以繪製過程以下:一、獲取 DOM 並將其分割爲多個層(RenderLayer) 二、將每一個層柵格化,並獨立的繪製進位圖中 三、將這些位圖做爲紋理上傳至 GPU 四、複合多個層來生成最終的屏幕圖像(終極layer)。

    3、HTML及CSS樣式的解析

    HTML解析是一個將字節轉化爲字符,字符解析爲標記,標記生成節點,節點構建樹的過程。。CSS樣式的解析則因爲複雜的樣式層疊而變得複雜。對此不一樣的渲染引擎在處理上有所差別,後文將會就這點進行詳細講解

    一、HTML的解析分爲標記化和樹構建兩個階段

    標記化算法:

    是詞法分析過程,將輸入內容解析成多個標記。HTML標記包括起始標記、結束標記、屬性名稱和屬性值。標記生成器識別標記,傳遞給樹構造器,而後接受下一個字符以識別下一個標記;如此反覆直到輸入的結束。
    該算法的輸出結果是 HTML 標記。該算法使用狀態機來表示。每個狀態接收來自輸入信息流的一個或多個字符,並根據這些字符更新下一個狀態。當前的標記化狀態和樹結構狀態會影響進入下一狀態的決定。這意味着,即便接收的字符相同,對於下一個正確的狀態也會產生不一樣的結果,具體取決於當前的狀態。
    樹構建算法:

    在樹構建階段,以 Document 爲根節點的 DOM 樹也會不斷進行修改,向其中添加各類元素。
    標記生成器發送的每一個節點都會由樹構建器進行處理。規範中定義了每一個標記所對應的 DOM 元素,這些元素會在接收到相應的標記時建立。這些元素不只會添加到 DOM 樹中,還會添加到開放元素的堆棧中。此堆棧用於糾正嵌套錯誤和處理未關閉的標記。其算法也能夠用狀態機來描述。這些狀態稱爲「插入模式」。

    如下將會舉一個例子來分析這兩個階段:

    clipboard.png

    標記化:初始狀態是數據狀態。

    • 遇到字符 < 時,狀態更改成「標記打開狀態」。接收一個 a-z字符會建立「起始標記」,狀態更改成「標記名稱狀態」。這個狀態會一直保持到接收> 字符。在此期間接收的每一個字符都會附加到新的標記名稱上。在本例中,咱們建立的標記是 html 標記。
    • 遇到 > 標記時,會發送當前的標記,狀態改回「數據狀態」。 標記也會進行一樣的處理。目前 html 和 body 標記均已發出。如今咱們回到「數據狀態」。接收到 Hello world 中的 H 字符時,將建立併發送字符標記,直到接收</body> 中的<。咱們將爲 Hello world 中的每一個字符都發送一個字符標記。
    • 如今咱們回到「標記打開狀態」。接收下一個輸入字符 / 時,會建立 end tag token 並改成「標記名稱狀態」。咱們會再次保持這個狀態,直到接收 >。而後將發送新的標記,並回到「數據狀態」。 輸入也會進行一樣的處理。

    仍是以上的例子,咱們來看看樹構建

    樹構建:樹構建階段的輸入是一個來自標記化階段的標記序列。

    • 第一個模式是「initial mode」。接收 HTML 標記後轉爲「before html」模式,並在這個模式下從新處理此標記。這樣會建立一個 HTMLHtmlElement 元素,並將其附加到 Document 根對象上。
    • 而後狀態將改成「before head」。此時咱們接收「body」標記。即便咱們的示例中沒有「head」標記,系統也會隱式建立一個 HTMLHeadElement,並將其添加到樹中。
    • 如今咱們進入了「in head」模式,而後轉入「after head」模式。系統對 body 標記進行從新處理,建立並插入 HTMLBodyElement,同時模式轉變爲「body」。
    • 如今,接收由「Hello world」字符串生成的一系列字符標記。接收第一個字符時會建立並插入「Text」節點,而其餘字符也將附加到該節點
    • 接收 body 結束標記會觸發「after body」模式。如今咱們將接收 HTML 結束標記,而後進入「after after body」模式。接收到文件結束標記後,解析過程就此結束。解析結束後的操做

    在此階段,瀏覽器會將文檔標註爲交互狀態,並開始解析那些處於「deferred」模式的腳本,也就是那些應在文檔解析完成後才執行的腳本。而後,文檔狀態將設置爲「完成」,一個「加載」事件將隨之觸發。

    完整解析過程以下圖:

    clipboard.png

    二、CSS的解析與層疊規則

    每個呈現器都表明了一個矩形的區域,一般對應於相關節點的 CSS 框,這一點在 CSS2 規範中有所描述。它包含諸如寬度、高度和位置等幾何信息。就是咱們 CSS 裏常提到的盒子模型。構建呈現樹時,須要計算每個呈現對象的可視化屬性。這是經過計算每一個元素的樣式屬性來完成的。因爲應用規則涉及到至關複雜的層疊規則,因此給樣式樹的構建形成了巨大的困難。爲何說它複雜?由於同一個元素可能涉及多條樣式,就須要判斷最終到底哪條樣式生效。首先咱們來了解一下css的樣式層疊規則

    ①層疊規則:

    根據不一樣的樣式來源優先級排列從小到大:

    • 1>、用戶端聲明:來自瀏覽器的樣式,被稱做 UA style,是瀏覽器默認的樣式。 好比,對於 DIV 元素,瀏覽器默認其 ‘display’ 的特性值是 「block」,而 SPAN 是 「inline」。
    • 2>、通常用戶聲明:這個樣式表是使用瀏覽器的用戶,根據本身的偏好設置的樣式表。好比,用戶但願全部 P 元素中的字體都默認顯示成藍色,能夠先定義一個樣式表,存成 css 文件。
    • 3>、通常做者聲明:即開發者在開發網頁時,所定義的樣式表。
    • 4>、加了’!important’ 的做者聲明
    • 5>、加了’!important’ 的用戶聲明

    !important 規則1:根據 CSS2.1 規範中的描述,’!important’ 能夠提升樣式的優先級,它對樣式優先級的影響是巨大的。
    注意,’!important’ 規則在 IE7 之前的版本中是被支持不完善。所以,常常被用做 CSS hack2。

    若是來源和重要性相同則根據CSS specificity來進行斷定。

    特殊性的值能夠看做是一個由四個數組成的一個組合,用 a,b,c,d 來表示它的四個位置。 依次比較 a,b,c,d 這個四個數比較其特殊性的大小。好比,a 值相同,那麼 b 值大的組合特殊性會較大,以此類推。 注意,W3C 中並非把它做爲一個 4 位數來看待的。
    a,b,c,d 值的肯定規則:

    • 若是 HTML 標籤的 ‘style’ 屬性中該樣式存在,則記 a 爲 1;
    • 數一下選擇器中 ID 選擇器的個數做爲 b 的值。好比,樣式中包含 ‘#c1’ 和 ‘#c2’ 的選擇器;
    • 其餘屬性以及僞類(pseudo-classes)的總數量是 c 的值。好比’.con’,’:hover’ 等;
    • 元素名和僞元素的數量是 d 的值

    在這裏咱們來看一個W3C給出的例子:

    clipboard.png

    那麼在以下例子中字體的顯示應當爲綠色:

    clipboard.png

    總結爲表格的話計算規則以下:

    clipboard.png

    ②CSS解析

    爲了簡化樣式計算,Firefox 還採用了另外兩種樹:規則樹和樣式上下文樹。Webkit 也有樣式對象,但它們不是保存在相似樣式上下文樹這樣的樹結構中,只是由 DOM 節點指向此類對象的相關樣式。

    1>、Firefox的規則樹和樣式上下文樹:

    樣式上下文包含端值。要計算出這些值,應按照正確順序應用全部的匹配規則,並將其從邏輯值轉化爲具體的值。例如,若是邏輯值是屏幕大小的百分比,則須要換算成絕對的單位。規則樹的點子真的很巧妙,它使得節點之間能夠共享這些值,以免重複計算,還能夠節約空間。
    全部匹配的規則都存儲在樹中。路徑中的底層節點擁有較高的優先級。規則樹包含了全部已知規則匹配的路徑。規則的存儲是延遲進行的。規則樹不會在開始的時候就爲全部的節點進行計算,而是隻有當某個節點樣式須要進行計算時,纔會向規則樹添加計算的路徑。
    這個想法至關於將規則樹路徑視爲詞典中的單詞。若是咱們已經計算出以下的規則樹:

    clipboard.png

    假設咱們須要爲內容樹中的另外一個元素匹配規則,而且找到匹配路徑是 B - E - I(按照此順序)。因爲咱們在樹中已經計算出了路徑 A - B - E - I - L,所以就已經有了此路徑,這就減小了如今所需的工做量。

    那麼Firefox是如何解決樣式計算難題的呢?接下來看一個樣例,假設咱們有以下HTML代碼:

    clipboard.png

    而且咱們有以下規則:

    clipboard.png

    爲了簡便起見,咱們只須要填充兩個結構:color 結構和 margin 結構。color 結構只包含一個成員(即「color」),而 margin 結構包含四條邊。
    造成的規則樹以下圖所示(節點的標記方式爲「節點名 : 指向的規則序號」):

    clipboard.png

    上下文樹以下圖所示(節點名 : 指向的規則節點):

    clipboard.png

    假設咱們解析 HTML 時遇到了第二個 <div> 標記,咱們須要爲此節點建立樣式上下文,並填充其樣式結構。
    通過規則匹配,咱們發現該 <div> 的匹配規則是第 一、2 和 6 條。這意味着規則樹中已有一條路徑可供咱們的元素使用,咱們只須要再爲其添加一個節點以匹配第 6 條規則(規則樹中的 F 節點)。
    咱們將建立樣式上下文並將其放入上下文樹中。新的樣式上下文將指向規則樹中的 F 節點。
    如今咱們須要填充樣式結構。首先要填充的是 margin 結構。因爲最後的規則節點 (F) 並無添加到 margin 結構,咱們須要上溯規則樹,直至找到在先前節點插入中計算過的緩存結構,而後使用該結構。咱們會在指定 margin 規則的最上層節點(即 B 節點)上找到該結構。
    咱們已經有了 color 結構的定義,所以不能使用緩存的結構。因爲 color 有一個屬性,咱們無需上溯規則樹以填充其餘屬性。咱們將計算端值(將字符串轉化爲 RGB 等)並在此節點上緩存通過計算的結構。
    第二個 元素處理起來更加簡單。咱們將匹配規則,最終發現它和以前的 span 同樣指向規則 G。因爲咱們找到了指向同一節點的同級,就能夠共享整個樣式上下文了,只需指向以前 span 的上下文便可。
    對於包含了繼承自父代的規則的結構,緩存是在上下文樹中進行的(事實上 color 屬性是繼承的,可是 Firefox 將其視爲 reset 屬性,並緩存到規則樹上)。
    例如,若是咱們在某個段落中添加 font 規則:

    clipboard.png

    那麼,該段落元素做爲上下文樹中的 div 的子代,就會共享與其父代相同的 font 結構(前提是該段落沒有指定 font 規則)。

    2>、Webkit的樣式解析

    在 Webkit 中沒有規則樹,所以會對匹配的聲明遍歷 4 次。首先應用非重要高優先級的屬性(因爲做爲其餘屬性的依據而應首先應用的屬性,例如 display),接着是高優先級重要規則,而後是普通優先級非重要規則,最後是普通優先級重要規則。這意味着屢次出現的屬性會根據正確的層疊順序進行解析。最後出現的最終生效。

    4、渲染樹的構建

    樣式樹和DOM樹鏈接在一塊兒造成一個渲染樹,渲染樹用來計算可見元素的佈局而且做爲將像素渲染到屏幕上的過程的輸入。值得一提的是,Gecko 將視覺格式化元素組成的樹稱爲「框架樹」。每一個元素都是一個框架。Webkit 使用的術語是「渲染樹」,它由「呈現對象」組成。 Webkit 和 Gecko 使用的術語略有不一樣,但總體流程是基本相同的。

    接下來未來看一下兩種渲染引擎的工做流程:
    Webkit 主流程:

    ![clipboard.png

    Mozilla 的 Gecko 呈現引擎主流程

    clipboard.png

    雖然 Webkit 和 Gecko 使用的術語略有不一樣,但總體流程是基本相同的。

    Gecko 將視覺格式化元素組成的樹稱爲「框架樹」。每一個元素都是一個框架。
    Webkit 使用的術語是「呈現樹」,它由「呈現對象」組成。
    對於元素的放置,Webkit 使用的術語是「佈局」,而 Gecko 稱之爲「重排」。
    對於鏈接 DOM 節點和可視化信息從而建立呈現樹的過程,Webkit 使用的術語是「附加」。有一個細微的非語義差異,就是 Gecko 在 HTML 與 DOM 樹之間還有一個稱爲「內容槽」的層,用於生成 DOM 元素。咱們會逐一論述流程中的每一部分。

    5、關於瀏覽器渲染過程當中須要瞭解的概念

    Repaint(重繪)——屏幕的一部分要重畫,好比某個CSS的背景色變了。可是元素的幾何尺寸沒有變。
    Reflow(重排)——意味着元件的幾何尺寸變了,咱們須要從新驗證並計算Render Tree。是Render Tree的一部分或所有發生了變化。這就是Reflow,或是Layout。reflow 會從這個root frame開始遞歸往下,依次計算全部的結點幾何尺寸和位置,在reflow過程當中,可能會增長一些frame,好比一個文本字符串必需被包裝起來。
    onload事件——當 onload 事件觸發時,頁面上全部的DOM,樣式表,腳本,圖片,flash都已經加載完成了。
    DOMContentLoaded事件——當 DOMContentLoaded 事件觸發時,僅當DOM加載完成,不包括樣式表,圖片,flash。
    首屏時間——當瀏覽器顯示第一屏頁面所消耗的時間,在國內的網絡條件下,一般一個網站,若是「首屏時間」在2秒之內是比較優秀的,5秒之內用戶能夠接受,10秒以上就不可容忍了。
    白屏時間——指瀏覽器開始顯示內容的時間。可是在傳統的採集方式裏,是在HTML的頭部標籤結尾裏記錄時間戳,來計算白屏時間。在這個時刻,瀏覽器開始解析身體標籤內的內容。而現代瀏覽器不會等待CSS樹(全部CSS文件下載和解析完成)和DOM樹(整個身體標籤解析完成)構建完成纔開始繪製,而是立刻開始顯示中間結果。因此常常在低網速的環境中,觀察到頁面由上至下緩慢顯示完,或者先顯示文本內容後再重繪成帶有格式的頁面內容。

    6、頁面優化方案

    本文的主題在於從瀏覽器的渲染過程談頁面優化。瞭解瀏覽器如何通訊並將拿到的數據如何進行解析渲染,本節將從網絡通訊、頁面渲染、資源預取及如何除了以上方案外,如何藉助chrome來針對一個頁面進行實戰優化四個方面來談。

    從網絡通訊過程入手能夠作的優化

    減小DNS查找

    每一次主機名解析都須要一次網絡往返,從而增長請求的延遲時間,同時還會阻塞後續請求。

    重用TCP鏈接

    儘量使用持久鏈接,以消除 TCP 握手和慢啓動延遲;

    減小HTTP重定向

    HTTP 重定向極費時間,特別是不一樣域名之間的重定向,更加費時;這裏面既有額外的 DNS 查詢、TCP 握手,還有其餘延遲。最佳的重定向次數爲零。

    使用 CDN(內容分發網絡)

    把數據放到離用戶地理位置更近的地方,能夠顯著減小每次 TCP 鏈接的網絡延遲,增大吞吐量。

    去掉沒必要要的資源

    任何請求都不如沒有請求快。說到這,全部建議都無需解釋。延遲是瓶頸,最快的速度莫過於什麼也不傳輸。然而,HTTP 也提供了不少額外的機制,好比緩存和壓縮,還有與其版本對應的一些性能技巧。

    在客戶端緩存資源

    應該緩存應用資源,從而避免每次請求都發送相同的內容。(瀏覽器緩存)

    傳輸壓縮過的內容

    傳輸前應該壓縮應用資源,把要傳輸的字節減至最少:確保每種要傳輸的資源採用最好的壓縮手段。(Gzip,減小60%~80%的文件大小)

    消除沒必要要的請求開銷

    減小請求的 HTTP 首部數據(好比HTTPcookie),節省的時間至關於幾回往返的延遲時間。

    並行處理請求和響應

    請求和響應的排隊都會致使延遲,不管是客戶端仍是服務器端。這一點常常被忽視,但卻會無謂地致使很長延遲。

    針對協議版本採起優化措施

    HTTP 1.x 支持有限的並行機制,要求打包資源、跨域分散資源,等等。相對而言,
    HTTP 2.0 只要創建一個鏈接就能實現最優性能,同時無需針對 HTTP 1.x 的那些優化方法。
    可是壓縮、使用緩存、減小dns等的優化方案不管在哪一個版本都一樣適用

    你須要瞭解的資源預取

    preload :能夠對當前頁面所需的腳本、樣式等資源進行預加載,而無需等到解析到 script 和 link 標籤時才進行加載。這一機制使得資源能夠更早的獲得加載並可用,且更不易阻塞頁面的初步渲染,進而提高性能。
    用法文檔:

    https://developer.mozilla.org...

    prefetch:prefetch 和 preload 同樣,都是對資源進行預加載,可是 prefetch 通常預加載的是其餘頁面會用到的資源。 固然,prefetch 不會像 preload 同樣,在頁面渲染的時候加載資源,而是利用瀏覽器空閒時間來下載。當進入下一頁面,就可直接從 disk cache 裏面取,既不影響當前頁面的渲染,又提升了其餘頁面加載渲染的速度。
    用法文檔:

    https://developer.mozilla.org...

    subresource: 被Chrome支持了有一段時間,而且已經有些搔到預加載當前導航/頁面(所含有的資源)的癢處了。但它有一個問題——沒有辦法處理所獲取內容的優先級(as也並不存在),因此最終,這些資源會以一個至關低的優先級被加載,這使得它能提供的幫助至關有限

    prerender:prerender 就像是在後臺打開了一個隱藏的 tab,會下載全部的資源、建立DOM、渲染頁面、執行js等等。若是用戶進入指定的連接,隱藏的這個頁面就會立馬進入用戶的視線。 可是要注意,必定要在十分肯定用戶會點擊某個連接時才使用該特性,不然客戶端會無故的下載不少資源和渲染這個頁面。 正如任何提早動做同樣,預判老是有必定風險出錯。若是提早的動做是昂貴的(好比高CPU、耗電、佔用帶寬),就要謹慎使用了。

    preconnect: preconnect 容許瀏覽器在一個 HTTP 請求正式發給服務器前預先執行一些操做,這包括

    dns-prefetch:經過 DNS 預解析來告訴瀏覽器將來咱們可能從某個特定的 URL 獲取資源,當瀏覽器真正使用到該域中的某個資源時就能夠儘快地完成 DNS 解析

    這些屬性雖然並不是全部瀏覽器都支持,可是不支持的瀏覽器也只是不處理而已,而是別的話則會省去不少時間。所以,合理的使用資源預取能夠顯著提升頁面性能。

    高效合理的css選擇符能夠減輕瀏覽器的解析負擔。

    由於css是逆向解析的因此應當避免多層嵌套。

    避免使用通配規則。如 *{} 計算次數驚人!只對須要用到的元素進行選擇

    儘可能少的去對標籤進行選擇,而是用class。如:#nav li{},能夠爲li加上nav_item的類名,以下選擇.nav_item{}

    不要去用標籤限定ID或者類選擇符。如:ul#nav,應該簡化爲#nav

    儘可能少的去使用後代選擇器,下降選擇器的權重值。後代選擇器的開銷是最高的,儘可能將選擇器的深度降到最低,最高不要超過三層,更多的使用類來關聯每個標籤元素。

    考慮繼承。瞭解哪些屬性是能夠經過繼承而來的,而後避免對這些屬性重複指定規則

    從js層面談頁面優化

    ①解決渲染阻塞
    若是在解析HTML標記時,瀏覽器遇到了JavaScript,解析會中止。只有在該腳本執行完畢後,HTML渲染纔會繼續進行。因此這阻塞了頁面的渲染。
    解決方法:在標籤中使用 async或defer特性
    ②減小對DOM的操做
    對DOM操做的代價是高昂的,這在網頁應用中的一般是一個性能瓶頸。
    解決辦法:修改和訪問DOM元素會形成頁面的Repaint和Reflow,循環對DOM操做更是罪惡的行爲。因此請合理的使用JavaScript變量儲存內容,考慮大量DOM元素中循環的性能開銷,在循環結束時一次性寫入。
    減小對DOM元素的查詢和修改,查詢時可將其賦值給局部變量。
    ③使用JSON格式來進行數據交換
    JSON是一種輕量級的數據交換格式,採用徹底獨立於語言的文本格式,是理想的數據交換格式。同時,JSON是 JavaScript原生格式,這意味着在 JavaScript 中處理 JSON數據不須要任何特殊的 API 或工具包。
    ④讓須要常常改動的節點脫離文檔流
    由於重繪有時確實不可避免,因此只能儘量限制重繪的影響範圍。

    如何藉助chrome針對性優化頁面

    首先打開控制檯,點擊Audits一欄,會看到以下表單。在選取本身須要模擬測試的狀況後點擊run audits,便可開始頁面性能分析。

    clipboard.png

    而後將會獲得分析結果及優化建議:

    clipboard.png

    咱們能夠逐項根據現有問題進行優化,如性能類目(performance)中的第一項優化建議延遲加載屏幕外圖像(defer offscreen images),點擊後就能看到詳情如下詳情:

    clipboard.png

    而具體頁面的指標優化能夠根據給出的建議進行逐條優化。目前提供的性能分析及建議的列表包括性能分析、漸進式web應用、最佳實踐、無障礙訪問及搜索引擎優化五個部分。基本上涵蓋了常見優化方案及性能點的方方面面,開發時合理使用也能更好的提高頁面性能

    相信以上優化方案之因此行之有效的緣由大均可以在本文中找出緣由。理論是用來指導實踐的,即不能閉門造車式的埋頭苦幹,也不能絕不實踐的誇誇其談。這樣纔會造成完整的知識體系,讓知識體系樹更加龐大。知道該如何優化是一回事,真正合理應用是另外一回事,要有好的性能,要着手於能作的每一件「小事」。

    7、附錄

    性能優化是一門藝術,更是一門綜合藝術。這其中涉及不少知識點。而這些知識點都有不少不錯的文章進行了總結。若是你想深刻探究或許這裏推薦的文章會給你啓發。

    HTTP2詳解:

    https://www.jianshu.com/p/e57...
    TCP擁塞控制:

    https://www.cnblogs.com/losby...
    頁面性能分析網站:

    https://gtmetrix.com/analyze....
    Timing官方文檔:

    https://www.w3.org/TR/navigat...
    chrome中的高性能網絡:

    https://www.cnblogs.com/xuan5...

    相關文章
    相關標籤/搜索