1. 程式人生 > >http系列--從輸入 URL 到頁面載入完成的過程

http系列--從輸入 URL 到頁面載入完成的過程

一、前言

這道題的覆蓋面可以非常廣,很適合作為一道承載知識體系的題目。每一個前端人員,如果要往更高階發展,必然會將自己的知識體系梳理一遍,沒有牢固的知識體系,無法往更高處走!

 

二、主幹流程

在將瀏覽器渲染原理、JS執行機制、JS引擎解析流程梳理一遍後,感覺就跟打通了任督二脈一樣,有了一個整體的架構,以前的知識點都連貫起來了。

1、從瀏覽器接收url到開啟網路請求執行緒(涉及到:瀏覽器機制,執行緒和程序之間的關係等)

2、開啟網路執行緒到發出一個完整的http請求(涉及到:dns查詢,tcp/ip請求,5層網路協議棧等)

3、從伺服器接收到請求到對應後臺接收到請求(涉及到:均衡負載,安全攔截,後臺內部的處理等)

4、後臺和前臺的http互動(涉及到:http頭,響應碼,報文結構,cookie等,可以提下靜態資源的cookie優化,以及編碼解碼如gzip壓縮等)

5、快取問題:http快取(涉及到:涉及到http快取頭部,etag,expired,cache-control等)

6、瀏覽器接收到http資料包後的解析流程(涉及到:html的詞法分析,然後解析成dom樹,同時解析css生成css規則樹,合併生成render樹。然後layout佈局、painting渲染、複合圖層的合成、GPU繪製、外連結處理、loaded和documentloaded等)

7、css視覺化格式模型(涉及到:元素渲染規則,如:包含塊,控制框,BFC,IFC等概念)

8、js引擎解析過程(涉及到:js解釋階段,預處理階段,執行階段生成執行上下文,VO(全域性物件),作用域鏈,回收機制等)

9、其他(擴充套件其他模組:跨域,web安全等)

 

三、從瀏覽器接收到url到開啟網路請求執行緒

涉及到:瀏覽器的程序和執行緒模型,js的執行機制。

1、瀏覽器是多程序的

(1)瀏覽器是多程序的;

(2)不同型別的標籤頁會開啟一個新的程序;

(3)相同型別的標籤頁會合併到一個程序中。

瀏覽器中各個程序以及作用:

1、瀏覽器程序:只有1個程序,(1)負責管理各個標籤的建立和銷燬;(2)負責瀏覽器頁面顯示;(3)負責資源的管理和下載;

2、第三方外掛程序:可以是多個程序,負責每一個第三方外掛的使用,每一個第三方外掛使用時候會建立一個對應的程序;

3、GPU程序:最多1個程序,負責3D繪製和硬體加速;

4、瀏覽器渲染程序:可以是多個程序,瀏覽器的核心,每個tab頁一個程序,主要負責HTML、,css,js等檔案的解析,執行和渲染,以及事件處理等。

 

2、瀏覽器渲染程序(核心程序)

每一個tab頁面是瀏覽器核心程序,然後這個每一個程序是多執行緒的,它有幾大類子執行緒:

(1)GUI執行緒;(2)JS引擎執行緒;(3)事件觸發執行緒;(4)定時器執行緒;(5)非同步的http網路請求執行緒

可以看出來JS引擎是核心程序中的一個執行緒,所以常說JS引擎時單執行緒的。

 

3、解析URL

輸入url後,會進行解析(URL是統一資源定位符)。

URL包括幾個部分:(1)protocol,協議頭,比如http,https,ftp等;(2)host,主機域名或者IP地址;(3)port,埠號;(4)path,目錄路徑;(5)query,查詢的引數;(6)fragment,#後邊的hash值,用來定位某一個位置。

 

4、網路請求時單獨的執行緒

每一次網路請求都是需要單獨開闢單獨的執行緒進行,比如URL解析到http協議,就會新建一個網路執行緒去處理資源下載。

因此瀏覽器會根據解析出得協議,開闢一個網路執行緒,前往請求資源。

 

四、開啟網路執行緒到發出一個完整的http請求

包括:DNS查詢,tcp/ip請求構建,五層網際網路協議等等。

1、DNS查詢得到IP

如果輸入的域名,需要DNS解析成IP,流程如下:

(1)瀏覽器有快取,直接用瀏覽器快取,沒有就去本機快取,沒有就看是不是host。

(2)如果還沒有,就向DNS域名伺服器查詢(這個過程經過路由,路由也有快取),查詢到對應的IP。

注意:1、域名查詢的時候有可能經過CDN排程器(如果CDN有儲存功能);

2、DNS解析是很耗時的,因此如果解析域名過多,首屏載入會變慢,可以考慮使用dns-prefetch優化。

 

2、tcp/ip請求構建

http的本質就是tcp/ip請求構建。需要3次握手規則簡歷連線,以及斷開連線時候的4次揮手。

tcp將http長報文劃分為短報文,通過3次握手與服務端建立連線,進行可靠的傳輸。

3次握手步驟:

客戶端:hello,你是server麼?
服務端:hello,我是server,你是client麼
客戶端:yes,我是client

建立成功之後,接下來就是正式傳輸資料。

然後,等到斷開連線時,需要進行4次揮手(因為是全雙工的,所以需要4次握手)。

4次揮手步驟:

主動方:我已經關閉了向你那邊的主動通道了,只能被動接收了
被動方:收到通道關閉的資訊
被動方:那我也告訴你,我這邊向你的主動通道也關閉了
主動方:最後收到資料,之後雙方無法通訊

 

tcp/ip的併發限制

瀏覽器對同一域名下併發的tcp連線是有限制的(2-10個不等)。而且在http1.0中往往一個資源下載就需要對應一個tcp/ip請求。所以針對這個瓶頸,又出現了很多的資源優化方案。

 

get和post區別

get和post本質都是tcp/ip,但是除了http外層外,在tcp/ip層面也有區別。get會產生1個tcp資料包,post產生2個tcp資料包。

具體就是:

(1)get請求時,瀏覽器會把header和data一起傳送出去,伺服器響應200(返回資料)。

(2)post請求時,瀏覽器首先發送headers,伺服器響應100 continue,瀏覽器再發送data,伺服器響應200(返回資料)。

 

3、五層網路協議棧

客戶端發出http請求到伺服器接收,中間會經過一系列的流程。

客戶端傳送請求具體:從應用層發動http請求,到傳輸層通過三次握手簡歷tcp/ip連線,再到網路層的ip定址,再到資料鏈路層的封裝成幀,最後在物理層通過物理介質傳輸。

服務端接收請求具體:反過來。

五層網路協議:

1、應用層(DNS,HTTP):DNS解析成IP併發送http請求;

2、傳輸層(TCP,UDP):建立TCP連線(3次握手);

3、網路層(IP,ARP):IP定址;

4、資料鏈路層(PPP):封裝成幀;

5、物理層(利用物理介質傳輸位元流):物理傳輸(通過雙絞線,電磁波等各種介質)。

其實也有一個完整的OSI七層框架,與之相比,多了會話層、表示層。

OSI七層框架:物理層資料鏈路層網路層傳輸層會話層表示層應用層

表示層:主要處理兩個通訊系統中互動資訊的表示方式,包括資料格式交換,資料加密和解密,資料壓縮和終端型別轉換等。

會話層:具體管理不同使用者和程序之間的對話,如控制登入和登出過程。

 

五、從伺服器接收請求到對應後臺接收到請求

服務端接收到請求時,內部會有很多處理。

包括:均衡負載,

1、負載均衡

對於大型專案,併發訪問很大,一臺伺服器吃不消,一般會有若干臺伺服器組成一個叢集,然後配合反向代理實現均衡負載。均衡負載不止一種實現方式。

概括的說:使用者傳送的請求指向排程伺服器(反向代理伺服器,比如nginx的均衡負載),然後排程伺服器根據實際的排程演算法,分配不同的請求給對應的叢集中的伺服器執行,然後排程伺服器等待實際伺服器的HTTP響應,並且反饋給使用者。

2、後臺處理

一般後臺都部署到容器中。過程如下:

(1)先是容器接收到請求(比如tomcat容器);

(2)然後對應容器中的後臺程式接收到請求(比如java程式);

(3)然後就是後臺自己的統一處理,處理完畢後響應結果。

具體概括一下:

(1)一般有的後端有統一的驗證,比如安全攔截,跨域驗證;

(2)如果不符合驗證規則,就直接返回相應的http報文(拒絕請求等);

(3)如果驗證通過了,才會進入到實際的後臺程式碼,此時程式接收到請求,然後執行查詢資料庫,大量計算等等;

(4)等程式執行完畢後,會返回一個http響應包(一般這一步會經過多層封裝);

(5)然後將這個資料包從後端返回到前端,完成互動。

 

六、後臺和前臺的http互動

前後端的互動,http報文作為資訊的載體。

1、http報文結構

報文一般包括:通用頭部,請求/響應頭部,請求/響應體

1.1 通用頭部

Request Url: 請求的web伺服器地址

Request Method: 請求方式
(Get、POST、OPTIONS、PUT、HEAD、DELETE、CONNECT、TRACE)

Status Code: 請求的返回狀態碼,如200代表成功

Remote Address: 請求的遠端伺服器地址(會轉為IP)

比如跨區拒絕時,methord為option,狀態碼404/405。

其中method分為兩批次:

HTTP1.0定義了三種請求方法: GET, POST 和 HEAD方法。
以及幾種Additional Request Methods:PUT、DELETE、LINK、UNLINK

HTTP1.1定義了八種請求方法:GET、POST、HEAD、OPTIONS, PUT, DELETE, TRACE 和 CONNECT 方法。

比如有些狀態碼來判斷:

200——表明該請求被成功地完成,所請求的資源傳送回客戶端
304——自從上次請求後,請求的網頁未修改過,請客戶端使用本地快取
400——客戶端請求有錯(譬如可以是安全模組攔截)
401——請求未經授權
403——禁止訪問(譬如可以是未登入時禁止)
404——資源未找到
500——伺服器內部錯誤
503——服務不可用

大致範圍

1xx——指示資訊,表示請求已接收,繼續處理
2xx——成功,表示請求已被成功接收、理解、接受
3xx——重定向,要完成請求必須進行更進一步的操作
4xx——客戶端錯誤,請求有語法錯誤或請求無法實現
5xx——伺服器端錯誤,伺服器未能實現合法的請求

 

1.2 請求頭/響應頭

常用的請求頭(部分)

Accept: 接收型別,表示瀏覽器支援的MIME型別
(對標服務端返回的Content-Type)
Accept-Encoding:瀏覽器支援的壓縮型別,如gzip等,超出型別不能接收
Content-Type:客戶端傳送出去實體內容的型別
Cache-Control: 指定請求和響應遵循的快取機制,如no-cache
If-Modified-Since:對應服務端的Last-Modified,用來匹配看檔案是否變動,只能精確到1s之內,http1.0中
Expires:快取控制,在這個時間內不會請求,直接使用快取,http1.0,而且是服務端時間
Max-age:代表資源在本地快取多少秒,有效時間內不會請求,而是使用快取,http1.1中
If-None-Match:對應服務端的ETag,用來匹配檔案內容是否改變(非常精確),http1.1中
Cookie: 有cookie並且同域訪問時會自動帶上
Connection: 當瀏覽器與伺服器通訊時對於長連線如何進行處理,如keep-alive
Host:請求的伺服器URL
Origin:最初的請求是從哪裡發起的(只會精確到埠),Origin比Referer更尊重隱私
Referer:該頁面的來源URL(適用於所有型別的請求,會精確到詳細頁面地址,csrf攔截常用到這個欄位)
User-Agent:使用者客戶端的一些必要資訊,如UA頭部等

常用的響應頭(部分)

Access-Control-Allow-Headers: 伺服器端允許的請求Headers
Access-Control-Allow-Methods: 伺服器端允許的請求方法
Access-Control-Allow-Origin: 伺服器端允許的請求Origin頭部(譬如為*)
Content-Type:服務端返回的實體內容的型別
Date:資料從伺服器傳送的時間
Cache-Control:告訴瀏覽器或其他客戶,什麼環境可以安全的快取文件
Last-Modified:請求資源的最後修改時間
Expires:應該在什麼時候認為文件已經過期,從而不再快取它
Max-age:客戶端的本地資源應該快取多少秒,開啟了Cache-Control後有效
ETag:請求變數的實體標籤的當前值
Set-Cookie:設定和頁面關聯的cookie,伺服器通過這個頭部把cookie傳給客戶端
Keep-Alive:如果客戶端有keep-alive,服務端也會有響應(如timeout=38)
Server:伺服器的一些相關資訊

一般來說,請求頭部和響應頭部是匹配分析的。

比如:

(1)請求頭部的Accept要和響應頭部的Content-Type匹配,否則會報錯;

(2)跨域請求中,請求頭部的Origin要匹配響應頭的Access-Control-Allow-Origin,否則會報跨域錯誤;

(3)使用快取,請求頭部的if-modified-since,if-none-match分別和響應頭的Last-modified,etag對應。

 

1.3 請求/響應實體

http請求時,除了頭部,還有訊息實體。

請求實體中會將一些需要的引數都放入進入(用於post請求)。

比如:(1)實體中可以放參數的序列化形式(a=1&b=2這種),或者直接放表單(Form Data物件,上傳時可以夾雜其他以及檔案)等等。

響應實體中,就是服務端需要傳給客戶端的內容。

一般現在的介面請求時,實體中就是對應資訊的json格式,而像頁面請求這種,裡面就是直接放一個html的字串,然後瀏覽器自己解析並渲染。

 

1.4 CRLF

CRLF(Carriage-Return Line-Feed),意思是回車換行,一般作為分隔符存在。

請求頭和實體訊息之間有一個CRLF分隔,響應頭部和響應實體之間用一個CRLF分隔。

下圖是對某請求的http報文結構的簡要分析:

 

2、 cookie以及優化

cookie是瀏覽器的一種本地儲存方式,一般用來幫助客戶端和服務端通訊的,常用來進行身份校驗,結合服務端的session使用。

在登陸頁面,使用者登陸了

此時,服務端會生成一個session,session中有對於使用者的資訊(如使用者名稱、密碼等)

然後會有一個sessionid(相當於是服務端的這個session對應的key)

然後服務端在登入頁面中寫入cookie,值就是:jsessionid=xxx

然後瀏覽器本地就有這個cookie了,以後訪問同域名下的頁面時,自動帶上cookie,自動檢驗,在有效時間內無需二次登陸。

一般來說,cookie是不允許存放敏感資訊的(千萬不要明文儲存使用者名稱、密碼),因為非常不安全,如果一定要強行儲存,首先,一定要在cookie中設定httponly(這樣就無法通過js操作了),另外可以考慮rsa等非對稱加密(因為實際上,瀏覽器本地也是容易被攻克的,並不安全)

比如這樣的場景:

客戶端在域名A下有cookie(這個可以是登陸時由服務端寫入的)

然後在域名A下有一個頁面,頁面中有很多依賴的靜態資源(都是域名A的,譬如有20個靜態資源)

此時就有一個問題,頁面載入,請求這些靜態資源時,瀏覽器會預設帶上cookie

也就是說,這20個靜態資源的http請求,每一個都得帶上cookie,而實際上靜態資源並不需要cookie驗證

此時就造成了較為嚴重的浪費,而且也降低了訪問速度(因為內容更多了)

當然了,針對這種場景,是有優化方案的(多域名拆分)。具體做法就是:

(1)將靜態資源分組,分別放到不同的域名下(如static.base.com

(2)而page.base.com(頁面所在域名)下請求時,是不會帶上static.base.com域名的cookie的,所以就避免了浪費

 

說到多域名拆分,還有一個問題?

(1)在移動端,如果請求的域名數過多,會降低請求速度(因為域名整套解析流程很浪費時間,而且移動端一般頻寬比不上PC)。

(2)這時候有個優化方案:dns-prefetch(這個是幹嘛的?就是讓瀏覽器空閒時提前解析dns域名,不過請合理使用)

 

關於cookie的互動,可以看下圖總結

 

3、gzip壓縮

首先,gzip是請求頭裡的Accept-Encoding:瀏覽器支援的壓縮型別之一。gzip是一種壓縮格式,需要瀏覽器支援才有效(一般瀏覽器都支援),而且gzip的壓縮率很好(高達70%);

然後gzip一般是apach,nginx,tomcat等web伺服器開啟。

除了gzip的壓縮格式,還有deflate,沒有gzip高效,不流行。

所以一般只需要在伺服器上開啟gzip壓縮,然後之後的請求都是基於gzip壓縮格式的,非常方便。

 

4、長連線和短連線

首先我們看一下tcp/ip的定義:

(1)長連線:一個tcp/ip連線上可以連續傳送多個數據包,tcp連線保持期間,如果乜有資料包傳送,需要雙方發檢測包以維持此連線,一般需要自己做線上維持(類似於心跳包)。

(2)短連線:通訊雙方有資料互動是,簡歷一個tcp連線,資料傳送完成後,則斷開此tcp連線。

我們再看一下http層面上:

(1)http1.0中,預設是使用的短連線,瀏覽器每進行一次http操作,就建立一次連線,任務結束就中斷連線,比如每一個靜態資源請求都是一個單獨的連線

(2)http1.1開始,預設是使用長連線,長連線會設定connection: keep-alive,在長連線的情況下,當一個網頁開啟後,客戶端和服務端之間用於傳輸http的tcp連線不會關閉,如果客戶端再次訪問伺服器這個頁面,會繼續使用這一條已經建立起來的連線。

注意:kee-alive不會永遠保持,他有一個持續時間,一般服務中進行配置,另外長連線是需要客戶端和伺服器端都支援才有效。

 

5、http2.0

http2.0不是https,它相當於http的下一代規範(https也可能是http2.0規範)

比較一下http1.1和http2.0顯著不同地方:

(1)http1.1中,每請求一個資源,都是需要開啟一個tcp/ip連線的,所以對應的結果是:每一個資源對應一個tcp/ip請求,由於tcp/ip本身有個併發數的限制,資源一旦多了,速度會下降慢下來。

(2)http2.0中,一個tcp/ip請求可以請求多個資源,也就說,只要一次tcp/ip請求,就可以請求多個資源,分隔成更小的幀請求,速度明顯提升。

所以,如果http2.0全面應用的,很多http1.1中的優化方案無需用到(比如:精靈圖,靜態組員多域名拆分等)。

現在介紹一下http2.0的一些特性:

(1)多路複用(一個tcp/ip可以請求多個資源);

(2)首部壓縮(http頭部壓縮,減少體積);

(3)二進位制分幀(在應用層跟傳輸層之間增加一個二進位制分幀層,改進傳輸效能,實現低延遲和高吞吐);

(4)伺服器端推送(服務端可以對客戶端的一個請求發出多個響應可以主動通知客戶端);

(5)請求優先順序(如果流被賦予了優先順序,就會基於這個優先順序來處理,有伺服器決定需要多少資源來處理該請求)

 

6、https

https就是安全版本的http,比如一些支付操作服務基本上都是基於https的,因為http請求的安全係數太低了。

簡單來看,https和http區別是:在請求前,會建立ssl連結,確保接下來的通訊都是加密的,無法輕易擷取分析。

一般來說,需要將網站升級到https,需要後端支援(後端需要申請證書等),然後https的開銷比http大(因為要額外的簡歷安全連結和加密等),所以一般來說http2.0配合https的體驗更佳(http2.0更快)。

主要關注的就是SSL/TLS的握手流程,如下(簡述):

(1)瀏覽器請求建立SSL連結,並向服務端傳送一個隨機數(client random)和客戶端支援的加密方法,比如是RSA加密,此時是明文傳輸。

(2)服務端從中選出一組加密演算法和hash演算法,回覆一個隨機數(server random),並將自己的身份資訊以證書的形式發回給瀏覽器(證書中包含了網站地址,非對稱加密的公鑰,以及證書頒發機構等資訊)。

(3)瀏覽器收到服務端證書後:

1、首先驗證證書的合法性(頒發機構是否合法,證書包含的網站是否和正在訪問的一樣),如果證書信任,瀏覽器會顯示一個小頭鎖,否則會有提示。

2、使用者接受到證書後(不管信任不信任),瀏覽器會產生一個新的隨機數(Premaster secret),然後證書中的公鑰以及制定的加密方法加密`Premaster secret`(預主密碼),傳送給伺服器。

3、利用client random,server random 和 premaster secret 通過一定的演算法生成HTTP連結資料傳輸的對稱加密key-‘sessionkey’

4、使用約定好的hash演算法計算握手訊息,並使用生成的session key 對訊息進行加密,最後將之前生成的所有資訊傳送給服務端。

(4)服務端收到瀏覽器的回覆

1、利用已知的加密方式與自己的私鑰進行解密,獲取Premaster secret,

2、和瀏覽器相同規則生成session key,

3、使用session key 解密瀏覽器發來的握手訊息,並驗證hash是否與瀏覽器發來的一致,

4、使用session key 加密一段握手訊息,傳送給瀏覽器

(5)瀏覽器解密並計算握手訊息的hash值,如果與服務端發來的hash一致,此時握手結束。

之後所有的https通訊資料將由之前瀏覽器生成的session key並利用對稱加密演算法進行加密

 

七、快取問題:http快取

http互動中,快取很大程度上提升效率。

1、強快取與弱快取

快取可以簡單劃分為兩種型別:強快取(200 from cache)與協商快取(304);

區別簡介一下:

(1)強快取(200 from cache)時,瀏覽器如果判斷本地快取未過期,就直接使用,無需發起http請求。

(2)協商快取(304)時,瀏覽器會向伺服器發起http請求,然後服務端告訴瀏覽器檔案未改變,讓瀏覽器使使用者本地快取。

對於協商快取,可以使用ctrl + F5強制重新整理,使得協商快取無效。

對於強制快取,在未過期,必須更新資源路徑才能傳送新的請求。

 

2、快取頭部簡述

怎麼在程式碼中區分強快取和協商快取?

通過不同的http的頭部控制。

 

屬於強制快取的:

(http1.1)Cache-Control/Max-Age
(http1.0)Pragma/Expires

注意:cache_control的值:public,private,no-store,no-cache,max-age

屬於協商快取的:

(http1.1)If-None-Match/E-tag
(http1.0)If-Modified-Since/Last-Modified

 

再提一點,其實HTML頁面中也有一個meta標籤可以控制快取方案-Pragma

<META HTTP-EQUIV="Pragma" CONTENT="no-cache">

不過,這種方案還是比較少用到,因為支援情況不佳,譬如快取代理伺服器肯定不支援,所以不推薦。

 

3、快取頭部區別

在http1.1中,出現了一些新內容,彌補http1.0不足。

http1.0中的快取控制:

(1)Pragma:嚴格來說不算快取控制的頭部,設定了no-cache會讓本地快取失效(屬於編譯控制,來實現特定的指令)。

(2)Expires:服務端配置,屬於強快取,用來控制在規定的時間之前,瀏覽器不會發送大量請求,而直接使用本地快取,注意:Expires一般對應伺服器端時間,比如:Expires:Fri, 30 Oct 1998 14:19:41

(3)If-Modified-Since/Last-modified:這兩個是成對出現的,屬於協商快取。其中瀏覽器頭部是If-Modified-Since,而服務端是Last-Modified,傳送請求時,如果這兩個匹配成功,代表伺服器資源並沒有改變,服務端不會返回資源實體,而是返回頭部,告知瀏覽器使用本地快取。Last-modifed指檔案最後的修改時間,只能精確到1S以內。

http1.1中快取的控制:

(1)cache-control :快取的控制頭部,有nocache,max-age等多個取值。

(2)Max-Age:服務端配置的,用來控制強快取的,在規定的時間內,瀏覽器不用發出請求,直接使用本地的快取。Max-Age是cache-control的值,比如:cache-control: max-age=60*1000,值是絕對時間,瀏覽器自己計算。

(3)If-None-Match/E-tag:這兩個是成對的出現的,屬於協商快取,其中瀏覽器頭部是If-None-Match,而服務端是E-tag,同樣,發出請求後,如果If-None-Match和E-tag匹配,代表內容沒有變化,告訴瀏覽器使用本地快取,和Last-modified不同,E-tag更精確,它類似於指紋一樣,基於FileEtag INode Mtime Size生成的,就是說檔案變,指紋就會變,沒有精確度的限制。

 

Cache-Control相比Expires?

1、都是強制快取。

2、Expires使用服務端時間,因為存在時區,和瀏覽器本地時間可以修改問題,在http1.1不推薦使用Expires;Cache-Control的Max-Age是瀏覽器端本地的絕對時間。

3、同時使用Cache-Control和Expires,Cache_control優先順序高。

 

E-tag相比Last-Modified?

1、都是協商快取。

2、Last-modified指的是服務端檔案最後改變時間,缺陷是精確只能到1s,檔案週期性的改變,導致快取失效;E-tag是一種指紋機制,檔案指紋,只要檔案改變,E-tag立刻變,沒有精度限制。

3、帶有E-tag和Last-modified時候,E-tag優先順序高。

 

各大快取頭部的整體關係如下圖

 

八、解析頁面流程

前面提到是http互動,接下來是瀏覽器獲取到html,然後解析,渲染。

1、流程簡述

瀏覽器核心拿到內容後,渲染大致分為以下幾步:

(1)解析html,構建DOM樹;同時解析CSS,生成CSS規則樹。

(2)合併DOM樹和CSS規則樹,生成Render樹。

(3)佈局Render樹(layout/reflow),負責各元素的尺寸,位置計算。

(4)繪製render樹(paint),繪製頁面畫素資訊。

(5)瀏覽器會將各層的資訊發給GPU。GPU會將各層合成(composite),顯示在螢幕上。

如下圖:

 

2、html解析,構建DOM

這一步的流程是這樣的:瀏覽器解析HTML,構建DOM樹。實際上,稍微展開一下。

解析html到構建dom過程簡述如下:

Bytes -> characters -> tokens -> nodes ->DOM

比如,有這樣一個html頁面:

<html>
  <head>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <link href="style.css" rel="stylesheet">
    <title>Critical Path</title>
  </head>
  <body>
    <p>Hello <span>web performance</span> students!</p>
    <div><img src="awesome-photo.jpg"></div>
  </body>
</html>

瀏覽器的處理如下:

列舉一下其中一些重點過程:

1. Conversion轉換:瀏覽器將獲得的HTML內容(Bytes)基於他的編碼轉換為單個字元

2. Tokenizing分詞:瀏覽器按照HTML規範標準將這些字元轉換為不同的標記token。每個token都有自己獨特的含義以及規則集

3. Lexing詞法分析:分詞的結果是得到一堆的token,此時把他們轉換為物件,這些物件分別定義他們的屬性和規則

4. DOM構建:因為HTML標記定義的就是不同標籤之間的關係,這個關係就像是一個樹形結構一樣
例如:body物件的父節點就是HTML物件,然後段略p物件的父節點就是body物件

最後的DOM樹:


3、css解析,構建css規則樹

CSS規則樹的生成也是類似

Bytes → characters → tokens → nodes → CSSOM

比如:style.css內容如下:

body { font-size: 16px }
p { font-weight: bold }
span { color: red }
p span { display: none }
img { float: right }

最終的CSSOM樹就是


 

4、構建渲染樹

當DOM樹和CSSOM都有了後,就要開始構建渲染樹了。一般來說,渲染樹和DOM樹相對應的,但不是嚴格意義上的一一對應。

因為有一些不可見的DOM元素不會插入到渲染樹中,如head這種不可見的標籤或者display: none

 

5、渲染

有了render樹,接下來就是開始渲染,基本流程如下:

圖中重要的四個步驟就是:

(1)計算CSS樣式 ;

(2)構建渲染樹 ;

(3)佈局,主要定位座標和大小,是否換行,各種position overflow z-index屬性 ;

(4)繪製,將影象繪製出來。

然後,圖中的線與箭頭代表通過js動態修改了DOM或CSS,導致了重新佈局(Layout)或渲染(Repaint)

 

這裡Layout和Repaint的概念是有區別的:

(1)Layout,也稱為Reflow,即迴流。一般意味著元素的內容、結構、位置或尺寸發生了變化,需要重新計算樣式和渲染樹。

(2)Repaint,即重繪。意味著元素髮生的改變只是影響了元素的一些外觀之類的時候(例如,背景色,邊框顏色,文字顏色等),此時只需要應用新樣式繪製這個元素就可以了。

迴流的成本開銷要高於重繪,而且一個節點的迴流往往回導致子節點以及同級節點的迴流, 所以優化方案中一般都包括,儘量避免迴流。

 

6、什麼引起迴流

1.頁面渲染初始化

2.DOM結構改變,比如刪除了某個節點

3.render樹變化,比如減少了padding

4.視窗resize

5.最複雜的一種:獲取某些屬性,引發迴流,
很多瀏覽器會對迴流做優化,會等到數量足夠時做一次批處理迴流,
但是除了render樹的直接變化,當獲取一些屬性時,瀏覽器為了獲得正確的值也會觸發迴流,這樣使得瀏覽器優化無效,包括
    (1)offset(Top/Left/Width/Height)
     (2) scroll(Top/Left/Width/Height)
     (3) cilent(Top/Left/Width/Height)
     (4) width,height
     (5) 呼叫了getComputedStyle()或者IE的currentStyle

迴流一定伴隨著重繪,重繪卻可以單獨出現。

優化方案:

(1)減少逐項更改樣式,做好一次性更改樣式。或者將樣式定義為class,並一次性更新。

(2)避免迴圈操作dom,建立一個documentFragment或div,在他上面進行所有的dom操作,最後新增到window.document中。

(3)避免多次讀取offset等屬性,無法避免就將他們快取到變數中。

(4)將複雜的元素絕對定位或者固定定位,使他們脫離文件流,否則迴流代價很高。

注意:改變字型大小會引起迴流。

再看一個例子:

var s = document.body.style;

s.padding = "2px"; // 迴流+重繪
s.border = "1px solid red"; // 再一次 迴流+重繪
s.color = "blue"; // 再一次重繪
s.backgroundColor = "#ccc"; // 再一次 重繪
s.fontSize = "14px"; // 再一次 迴流+重繪
// 新增node,再一次 迴流+重繪
document.body.appendChild(document.createTextNode('abc!'));

 

6、簡單層和複合層

上述中的渲染中止步於繪製,但實際上繪製這一步也沒有這麼簡單,它可以結合複合層和簡單層的概念來講。

簡單介紹下:

(1)可以預設只有一個複合層,所有的DOM節點都是在這個複合圖層下。

(2)如果開啟了硬體加速功能,可以將某一個節點變成複合圖層。

(3)複合圖層之間的繪製互不干擾,直接GPU直接控制。

(4)簡單圖層中,就算是absolute等佈局,變化時不影響整體迴流,但是由於在同一個圖層中,仍然會影響繪製的,因此做動畫時候效能仍然很低。而且複合層是獨立的,所以一般做動畫推薦使用硬體加速。

更多參考:https://segmentfault.com/a/1190000012925872#articleHeader16

 

7、Chrome的除錯

Chrome的開發者工具中,Performance中可以看到詳細的渲染過程:

 

8、資源外鏈的下載

上面介紹了html解析,渲染流程。但實際上,在解析html時,會遇到一些資源連線,此時就需要進行單獨處理了。

簡單起見,這裡將遇到的靜態資源分為一下幾大類(未列舉所有):

(1)css樣式資源

(2)js指令碼資源

(3)img圖片類資源

 

(1)遇到外鏈的處理

當遇到上述的外鏈時,會單獨開啟一個下載執行緒去下載資源(http1.1中是每一個資源的下載都要開啟一個http請求,對應一個tcp/ip連結)

 

(2)遇到css樣式資源

css資源處理特點:

(1)css下載時非同步的,不會阻塞瀏覽器構建DOM樹;

(2)但是會阻塞渲染,也就是在構建render樹時,等到css下載解析後才進行(與瀏覽器優化有關,防止css規則不斷變化,避免重複的構建)

(3)有例外,遇到media query 宣告的css是不會阻塞構建render樹

 

(3)遇到js指令碼資源

JS指令碼資源的處理有幾個特點:

(1)阻塞瀏覽器的解析,也就是說發現一個外鏈指令碼時,需等待指令碼下載完成並執行後才會繼續解析HTML。

(2)瀏覽器的優化,一般現代瀏覽器有優化,在指令碼阻塞時,也會繼續下載其它資源(當然有併發上限),但是雖然指令碼可以並行下載,解析過程仍然是阻塞的,也就是說必須這個指令碼執行完畢後才會接下來的解析,並行下載只是一種優化而已。

(3)defer與async,普通的指令碼是會阻塞瀏覽器解析的,但是可以加上defer或async屬性,這樣指令碼就變成非同步了,可以等到解析完畢後再執行。

注意,defer和async是有區別的:defer是延遲執行,而async是非同步執行。

簡單的說:

(1)async是非同步執行,非同步下載完畢後就會執行,不確保執行順序,一定在onload前,但不確定在DOMContentLoaded事件的前或後。

(2)defer是延遲執行,在瀏覽器看起來的效果像是將指令碼放在了body後面一樣(雖然按規範應該是在DOMContentLoaded事件前,但實際上不同瀏覽器的優化效果不一樣,也有可能在它後面)。

 

(4)遇到img圖片類資源

遇到圖片等資源時,直接就是非同步下載,不會阻塞解析,下載完畢後直接用圖片替換原有src的地方

 

9、loaded和domcontentloaded

對比:

(1)DOMContentLoaded 事件觸發時,僅當DOM載入完成,不包括樣式表,圖片(譬如如果有async載入的指令碼就不一定完成)。

(2)load 事件觸發時,頁面上所有的DOM,樣式表,指令碼,圖片都已經載入完成了。

 

【謝謝關注和閱讀,後續新的文章首發:sau交流學習社群:https://www.mwcxs.top