摘要:通過主機名,最終得到該主機名對應的地址的過程叫做域名解析或主機名解析。因此去掉不必要的資源和資源合并包括及資源合并雪碧圖等才會成為性能優化繞不開的方案。
作者:李佳曉 原文:學而思網校技術團隊
前言合格的開發者知道怎么做,而優秀的開發者知道為什么這么做。
這句話來自《web性能權威指南》,我一直很喜歡,而本文嘗試從瀏覽器渲染原理探討如何進行性能提升。
全文將從網絡通信以及頁面渲染兩個過程去探討瀏覽器的行為及在此過程中我們可以針對那些點進行優化,有些的不足之處還請各位不吝雅正。
關于瀏覽器渲染機制已經是老生常談,而且網上現有資料中有非常多的優秀資料對此進行闡述。遺憾的是網上的資料良莠不齊,經常在不同的文檔中對同一件事的描述出現了極大的差異。懷著嚴謹求學的態度經過大量資料的查閱和請教,將會在后文總結出一個完整的流程。
1、DOM樹的構建是文檔加載完成開始的?DOM樹的構建是從接受到文檔開始的,先將字節轉化為字符,然后字符轉化為標記,接著標記構建dom樹。這個過程被分為標記化和樹構建
而這是一個漸進的過程。為達到更好的用戶體驗,呈現引擎會力求盡快將內容顯示在屏幕上。它不必等到整個 HTML 文檔解析完畢之后,就會開始構建呈現樹和設置布局。在不斷接收和處理來自網絡的其余內容的同時,呈現引擎會將部分內容解析并顯示出來。
參考文檔:
http://taligarsiel.com/Projec...
這三個過程在實際進行的時候又不是完全獨立,而是會有交叉。會造成一邊加載,一邊解析,一邊渲染的工作現象。
參考文檔:
http://www.jianshu.com/p/2d52...
3、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/4570
當瀏覽器拿到HTTP報文時呈現引擎將開始解析 HTML 文檔,并將各標記逐個轉化成“內容樹”上的 DOM 節點。同時也會解析外部 CSS 文件以及樣式元素中的樣式數據。HTML 中這些帶有視覺指令的樣式信息將用于創建另一個樹結構:呈現樹。瀏覽器將根據呈現樹進行布局繪制。
以上就是頁面渲染的大致流程。那么瀏覽器從用戶輸入網址之后到底做了什么呢?以下將會進行一個完整的梳理。鑒于本文是前端向的所以梳理內容會有所偏重。而從輸入到呈現可以分為兩個部分:網絡通信和頁面渲染
我們首先來看網絡通信部分:
1、用戶輸入url并敲擊回車。 2、進行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解析圓滿成功。
3、建立tcp連接拿到域名對應的IP地址之后,User-Agent(一般是指瀏覽器)會以一個隨機端口(1024< 端口 < 65535)向服務器的WEB程序(常用的有httpd,nginx等)80端口發起TCP的連接請求。這個連接請求(原始的http請求經過TCP/IP4層模型的層層封包)到達服務器端后(這中間通過各種路由設備,局域網內除外),進入到網卡,然后是進入到內核的TCP/IP協議棧(用于識別該連接請求,解封包,一層一層的剝開),還有可能要經過Netfilter防火墻(屬于內核的模塊)的過濾,最終到達WEB程序,最終建立了TCP/IP的連接。
tcp建立連接和關閉連接均需要一個完善的確認機制,我們一般將連接稱為三次握手,而連接關閉稱為四次揮手。而不論是三次握手還是四次揮手都需要數據從客戶端到服務器的一次完整傳輸。將數據從客戶端到服務端經歷的一個完整時延包括:
發送時延:把消息中的所有比特轉移到鏈路中需要的時間,是消息長度和鏈路速度的函數
傳播時延:消息從發送端到接受端需要的時間,是信號傳播距離和速度的函數
處理時延:處理分組首部,檢查位錯誤及確定分組目標所需的時間
排隊時延:到來的分組排隊等待處理的時間以上的延遲總和就是客戶端到服務器的總延遲時間
以上的延遲總和就是客戶端到服務器的總延遲時間。因此每一次的連接建立和斷開都是有巨大代價的。因此去掉不必要的資源和資源合并(包括js及css資源合并、雪碧圖等)才會成為性能優化繞不開的方案。但是好消息是隨著協議的發展我們將對性能優化這個主題有著新的看法和思考。雖然還未到來,但也不遠了。如果你感到好奇那就接著往下看。
以下簡述下tcp建立連接的過程:
第一次握手:客戶端發送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時開始啟用擁塞避免算法,既每次得到確認后數據包只增加一個。當發生網絡擁塞后,閾值減半重新開始慢啟動算法。
因此為避免tcp連接的三次握手耗時及慢啟動引起的發送速度慢的情況,應盡量減少tcp連接的次數。
而HTTP1.0每個數據請求都需要重新建立連接的特點使得HTTP 1.0版本的性能比較差。隨著網頁加載的外部資源越來越多,這個問題就愈發突出了。 為了解決這個問題,有些瀏覽器在請求時,用了一個非標準的Connection字段。 Kepp-alive 一個可以復用的TCP連接就建立了,直到客戶端或服務器主動關閉連接。但是,這不是標準字段,不同實現的行為可能不一致,因此不是根本的解決辦法。
HTTP1.1http1.1(以下簡稱h1.1) 版的最大變化,就是引入了持久連接(persistent connection),即TCP連接默認不關閉,可以被多個請求復用,不用聲明Connection: keep-alive。 客戶端和服務器發現對方一段時間沒有活動,就可以主動關閉連接。不過,規范的做法是,客戶端在最后一個請求時,發送Connection: close,明確要求服務器關閉TCP連接。 目前,對于同一個域名,大多數瀏覽器允許同時建立6個持久連接。相比與http1.0,1.1的頁面性能有了巨大提升,因為省去了很多tcp的握手揮手時間。下圖第一種是tcp建立后只能發一個請求的http1.0的通信狀態,而擁有了持久連接的h1.1則避免了tcp握手及慢啟動帶來的漫長時延。
從圖中可以看到相比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...
HTTP22015年,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的一些問題來避免和繞過的。事實上多數的優化都是針對網絡通信這個部分在做。
4、建立TCP連接后發起http請求 5、服務器端響應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”的元素仍會顯示)
渲染樹構建完畢后將會進行布局。布局使用流模型的Layout算法。所謂流模型,即是指Layout的過程只需進行一遍即可完成,后出現在流中的元素不會影響前出現在流中的元素,Layout過程只需從左至右從上至下一遍完成即可。但實際實現中,流模型會有例外。Layout是一個遞歸的過程,每個節點都負責自己及其子節點的Layout。Layout結果是相對父節點的坐標和尺寸。其過程可以簡述為:
此時renderTree已經構建完畢,不過瀏覽器渲染樹引擎并不直接使用渲染樹進行繪制,為了方便處理定位(裁剪),溢出滾動(頁內滾動),CSS轉換/不透明/動畫/濾鏡,蒙版或反射,Z (Z排序)等,瀏覽器需要生成另外一棵樹 - 層樹。因此繪制過程如下:1、獲取 DOM 并將其分割為多個層(RenderLayer) 2、將每個層柵格化,并獨立的繪制進位圖中 3、將這些位圖作為紋理上傳至 GPU 4、復合多個層來生成最終的屏幕圖像(終極layer)。
三、HTML及CSS樣式的解析HTML解析是一個將字節轉化為字符,字符解析為標記,標記生成節點,節點構建樹的過程。。CSS樣式的解析則由于復雜的樣式層疊而變得復雜。對此不同的渲染引擎在處理上有所差異,后文將會就這點進行詳細講解
1、HTML的解析分為標記化和樹構建兩個階段標記化算法:
是詞法分析過程,將輸入內容解析成多個標記。HTML標記包括起始標記、結束標記、屬性名稱和屬性值。標記生成器識別標記,傳遞給樹構造器,然后接受下一個字符以識別下一個標記;如此反復直到輸入的結束。
該算法的輸出結果是 HTML 標記。該算法使用狀態機來表示。每一個狀態接收來自輸入信息流的一個或多個字符,并根據這些字符更新下一個狀態。當前的標記化狀態和樹結構狀態會影響進入下一狀態的決定。這意味著,即使接收的字符相同,對于下一個正確的狀態也會產生不同的結果,具體取決于當前的狀態。
樹構建算法:
在樹構建階段,以 Document 為根節點的 DOM 樹也會不斷進行修改,向其中添加各種元素。
標記生成器發送的每個節點都會由樹構建器進行處理。規范中定義了每個標記所對應的 DOM 元素,這些元素會在接收到相應的標記時創建。這些元素不僅會添加到 DOM 樹中,還會添加到開放元素的堆棧中。此堆棧用于糾正嵌套錯誤和處理未關閉的標記。其算法也可以用狀態機來描述。這些狀態稱為“插入模式”。
以下將會舉一個例子來分析這兩個階段:
標記化:初始狀態是數據狀態。
遇到字符 < 時,狀態更改為“標記打開狀態”。接收一個 a-z字符會創建“起始標記”,狀態更改為“標記名稱狀態”。這個狀態會一直保持到接收> 字符。在此期間接收的每個字符都會附加到新的標記名稱上。在本例中,我們創建的標記是 html 標記。
遇到 > 標記時,會發送當前的標記,狀態改回“數據狀態”。 標記也會進行同樣的處理。目前 html 和 body 標記均已發出。現在我們回到“數據狀態”。接收到 Hello world 中的 H 字符時,將創建并發送字符標記,直到接收
中的<。我們將為 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”模式的腳本,也就是那些應在文檔解析完成后才執行的腳本。然后,文檔狀態將設置為“完成”,一個“加載”事件將隨之觸發。
完整解析過程如下圖:
2、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給出的例子:
那么在如下例子中字體的顯示應當為綠色:
總結為表格的話計算規則如下:
②CSS解析為了簡化樣式計算,Firefox 還采用了另外兩種樹:規則樹和樣式上下文樹。Webkit 也有樣式對象,但它們不是保存在類似樣式上下文樹這樣的樹結構中,只是由 DOM 節點指向此類對象的相關樣式。
1>、Firefox的規則樹和樣式上下文樹:樣式上下文包含端值。要計算出這些值,應按照正確順序應用所有的匹配規則,并將其從邏輯值轉化為具體的值。例如,如果邏輯值是屏幕大小的百分比,則需要換算成絕對的單位。規則樹的點子真的很巧妙,它使得節點之間可以共享這些值,以避免重復計算,還可以節約空間。
所有匹配的規則都存儲在樹中。路徑中的底層節點擁有較高的優先級。規則樹包含了所有已知規則匹配的路徑。規則的存儲是延遲進行的。規則樹不會在開始的時候就為所有的節點進行計算,而是只有當某個節點樣式需要進行計算時,才會向規則樹添加計算的路徑。
這個想法相當于將規則樹路徑視為詞典中的單詞。如果我們已經計算出如下的規則樹:
假設我們需要為內容樹中的另一個元素匹配規則,并且找到匹配路徑是 B - E - I(按照此順序)。由于我們在樹中已經計算出了路徑 A - B - E - I - L,因此就已經有了此路徑,這就減少了現在所需的工作量。
那么Firefox是如何解決樣式計算難題的呢?接下來看一個樣例,假設我們有如下HTML代碼:
并且我們有如下規則:
為了簡便起見,我們只需要填充兩個結構:color 結構和 margin 結構。color 結構只包含一個成員(即“color”),而 margin 結構包含四條邊。
形成的規則樹如下圖所示(節點的標記方式為“節點名 : 指向的規則序號”):
上下文樹如下圖所示(節點名 : 指向的規則節點):
假設我們解析 HTML 時遇到了第二個
那么,該段落元素作為上下文樹中的 div 的子代,就會共享與其父代相同的 font 結構(前提是該段落沒有指定 font 規則)。
2>、Webkit的樣式解析在 Webkit 中沒有規則樹,因此會對匹配的聲明遍歷 4 次。首先應用非重要高優先級的屬性(由于作為其他屬性的依據而應首先應用的屬性,例如 display),接著是高優先級重要規則,然后是普通優先級非重要規則,最后是普通優先級重要規則。這意味著多次出現的屬性會根據正確的層疊順序進行解析。最后出現的最終生效。
四、渲染樹的構建樣式樹和DOM樹連接在一起形成一個渲染樹,渲染樹用來計算可見元素的布局并且作為將像素渲染到屏幕上的過程的輸入。值得一提的是,Gecko 將視覺格式化元素組成的樹稱為“框架樹”。每個元素都是一個框架。Webkit 使用的術語是“渲染樹”,它由“呈現對象”組成。 Webkit 和 Gecko 使用的術語略有不同,但整體流程是基本相同的。
接下來將來看一下兩種渲染引擎的工作流程:
Webkit 主流程:
Mozilla 的 Gecko 呈現引擎主流程
雖然 Webkit 和 Gecko 使用的術語略有不同,但整體流程是基本相同的。
Gecko 將視覺格式化元素組成的樹稱為“框架樹”。每個元素都是一個框架。
Webkit 使用的術語是“呈現樹”,它由“呈現對象”組成。
對于元素的放置,Webkit 使用的術語是“布局”,而 Gecko 稱之為“重排”。
對于連接 DOM 節點和可視化信息從而創建呈現樹的過程,Webkit 使用的術語是“附加”。有一個細微的非語義差別,就是 Gecko 在 HTML 與 DOM 樹之間還有一個稱為“內容槽”的層,用于生成 DOM 元素。我們會逐一論述流程中的每一部分。
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樹(整個身體標簽解析完成)構建完成才開始繪制,而是馬上開始顯示中間結果。所以經常在低網速的環境中,觀察到頁面由上至下緩慢顯示完,或者先顯示文本內容后再重繪成帶有格式的頁面內容。
本文的主題在于從瀏覽器的渲染過程談頁面優化。了解瀏覽器如何通信并將拿到的數據如何進行解析渲染,本節將從網絡通信、頁面渲染、資源預取及如何除了以上方案外,如何借助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,即可開始頁面性能分析。
然后將會得到分析結果及優化建議:
我們可以逐項根據現有問題進行優化,如性能類目(performance)中的第一項優化建議延遲加載屏幕外圖像(defer offscreen images),點擊后就能看到詳情以下詳情:
而具體頁面的指標優化可以根據給出的建議進行逐條優化。目前提供的性能分析及建議的列表包括性能分析、漸進式web應用、最佳實踐、無障礙訪問及搜索引擎優化五個部分。基本上涵蓋了常見優化方案及性能點的方方面面,開發時合理使用也能更好的提升頁面性能
相信以上優化方案之所以行之有效的原因大都可以在本文中找出原因。理論是用來指導實踐的,即不能閉門造車式的埋頭苦干,也不能毫不實踐的夸夸其談。這樣才會形成完整的知識體系,讓知識體系樹更加龐大。知道該如何優化是一回事,真正合理應用是另一回事,要有好的性能,要著手于能做的每一件“小事”。
七、附錄性能優化是一門藝術,更是一門綜合藝術。這其中涉及很多知識點。而這些知識點都有很多不錯的文章進行了總結。如果你想深入探究或許這里推薦的文章會給你啟發。
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...
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/104794.html
摘要:淺談網站性能之前端性能優化性能優化的目的無非是減少用戶流量消耗,提升用戶首屏體驗,提升用戶訪問速度,讓用戶專注內容本身。前端性能優化減少請求數量基本原理在瀏覽器與服務器進行通信時,主要是通過進行通信。 最近項目慢慢走上正軌,需求趨于平穩,這才想起需要對整站進行性能優化。經過一段時間的學習,結合現在項目的實際性能情況,發現確實有許多地方可以進行優化。于是就開始了我的前端性能優化之旅。以下...
摘要:淺談網站性能之前端性能優化性能優化的目的無非是減少用戶流量消耗,提升用戶首屏體驗,提升用戶訪問速度,讓用戶專注內容本身。前端性能優化減少請求數量基本原理在瀏覽器與服務器進行通信時,主要是通過進行通信。 最近項目慢慢走上正軌,需求趨于平穩,這才想起需要對整站進行性能優化。經過一段時間的學習,結合現在項目的實際性能情況,發現確實有許多地方可以進行優化。于是就開始了我的前端性能優化之旅。以下...
摘要:聲明的變量不得改變值,這意味著,一旦聲明變量,就必須立即初始化,不能留到以后賦值。 雖然今年沒有換工作的打算 但為了跟上時代的腳步 還是忍不住整理了一份最新前端知識點 知識點匯總 1.HTML HTML5新特性,語義化瀏覽器的標準模式和怪異模式xhtml和html的區別使用data-的好處meta標簽canvasHTML廢棄的標簽IE6 bug,和一些定位寫法css js放置位置和原因...
摘要:聲明的變量不得改變值,這意味著,一旦聲明變量,就必須立即初始化,不能留到以后賦值。 雖然今年沒有換工作的打算 但為了跟上時代的腳步 還是忍不住整理了一份最新前端知識點 知識點匯總 1.HTML HTML5新特性,語義化瀏覽器的標準模式和怪異模式xhtml和html的區別使用data-的好處meta標簽canvasHTML廢棄的標簽IE6 bug,和一些定位寫法css js放置位置和原因...
摘要:聲明的變量不得改變值,這意味著,一旦聲明變量,就必須立即初始化,不能留到以后賦值。 雖然今年沒有換工作的打算 但為了跟上時代的腳步 還是忍不住整理了一份最新前端知識點 知識點匯總 1.HTML HTML5新特性,語義化瀏覽器的標準模式和怪異模式xhtml和html的區別使用data-的好處meta標簽canvasHTML廢棄的標簽IE6 bug,和一些定位寫法css js放置位置和原因...
閱讀 1552·2021-09-22 15:52
閱讀 3459·2021-09-22 14:59
閱讀 2843·2021-09-02 15:12
閱讀 971·2021-08-20 09:35
閱讀 1578·2019-08-30 14:09
閱讀 2709·2019-08-30 13:56
閱讀 1646·2019-08-26 18:27
閱讀 3363·2019-08-26 13:37