瀏覽器輸入URL到 請求全過程以及相應的效能優化
前言
對http 的理解,一直都出於看完資料,沒過幾天又忘記了下面這篇文章只是自己對http
的整個過程一個梳理,並且從http
的請求過來,來簡單的進行效能優化進行一個梳理。 其中主要設計如下幾個環節:
- http請求全過程
- DNS解析全過程(dns-prefetch, preconnect, preload, prefetch, def, async)
- TCP 連線(三次握手,四次揮手(為什麼需要三次握手,四次揮手))
- 快取處理(expires, cache-control(max-age, public/private, no-cache, no-store,Pragma, must-revalidation), last-modify(If-modify-since), etag(if-none-match)),瀏覽器快取(記憶體快取from memory cache), 強快取,協商快取(from disk cache)
- PageSpend 和LightHouse 來進行效能分析
對上面的幾個環節的梳理,都是借鑑前輩們的分析成果,後面會列出所有的文章連線。
http 請求全過程
參考文章 當你輸入一個網址的時候,實際會發生什麼?
- 在瀏覽器位址列輸入地址,比如說:
fecebook.com
- 瀏覽器通過域名查詢IP地址(DNS解析)
- 瀏覽器給Web伺服器傳送一個HTTP請求
- facebook 伺服器進行永久重定向
因為我們輸入的
fecebook.com
,而不是http://www.facebook.com/
所以伺服器自動進行了永久重定向,返回的是301 狀態碼
為什麼伺服器一定要重定向而不是直接發會使用者想看的網頁內容呢?這個問題有好多有意思的答案。
其中一個原因跟搜尋引擎排名有 關。你看,如果一個頁面有兩個地址,就像http://www.igoro.com/ 和http://igoro.com/,搜尋引擎會認為它們是兩個網站,結果造成每一個的搜尋連結都減少從而降低排名。而搜尋引擎知道301永久重定向是 什麼意思,這樣就會把訪問帶www的和不帶www的地址歸到同一個網站排名下。
還有一個是用不同的地址會造成快取友好性變差。當一個頁面有好幾個名字時,它可能會在快取裡出現好幾次。
- 瀏覽器跟蹤重定向地址
- 伺服器處理"請求
- 伺服器返回一個HTML響應
- 瀏覽器解析HTML並繪製頁面
- 瀏覽器傳送潛入在HTML中的物件,比如說圖片,CSS樣式,JS檔案,字型等
- 瀏覽器傳送Ajax 請求
從上面我們已經知道從輸入URL到展示頁面的整個大致過程,下面我們會針對其中幾個關鍵步驟再次深入分析
DNS 解析過程
在上面我們已經知道了,我們輸入一個URL,發起請求,其實接收請求的最終一個伺服器,而每個伺服器都一個IP地址,所以一般一個域名對一個一個IP地址(也有對應多個IP地址的,我們暫時值分析對一個IP地址的情況), 但是瀏覽器怎麼知道域名到底對應的是那個IP地址呢,這個就是涉及到怎麼去域名解析了。 域名解析主要是如下過程:
- 通過瀏覽器快取來查詢,如果快取中存在,則就不會繼續查詢,直接用查詢到的IP地址,我們可以在 Chrome 中檢視我們瀏覽器中快取的所有的DNS.
- 如果瀏覽器沒有查詢到,我們就會在我們電腦中去查詢是否儲存了對應的域名資訊
- 如果本地沒有儲存,就會從路由器去查詢
- 如果路由器都沒有,就會去ISP 中查詢
從上面分析可知,我們輸入一個域名需要去做DNS解析找到IP,但是在我們的程式碼中,經常將一些靜態資源放在CDN中,每個CDN地址我們都要去做下DNS解析,這個會浪費時間,我們可以通過預先進行DNS解析,然後在請求的時候,DNS已經解析完成就不用等待了
<!--在head標籤中,越早越好-->
<link rel="dns-prefetch" href="//example.com">
複製程式碼
Tcp 連線
參考文章探網路系列(1)-TCP三次握手&Render Tree頁面渲染=>從輸入URL到頁面顯示的過程?
第一次握手:建立連線
客戶端傳送連線請求報文段,將SYN值設為1,Sequence Number為x。客戶端進入SYN_SEND狀態,等待伺服器的確認。
第二次握手:伺服器收到SYN報文段
伺服器收到客戶端SYN報文段,需要對這個SYN報文段進行確認,設定Acknowledgment Number為x+1(Sequence Number+1)。同時,自己自己還要傳送SYN請求資訊,將SYN值設為1,Sequence Number設為y。伺服器端將上述所有資訊放到一個報文段(即SYN+ACK報文段)中,一併傳送給客戶端,伺服器進入SYN_RECV狀態。
第三次握手:客戶端收到SYN+ACK報文段
客戶端收到伺服器的SYN+ACK報文段後將Acknowledgment Number設定為y+1,向伺服器傳送ACK報文段,這個報文段傳送完畢以後,客戶端和伺服器端都進入ESTABLISHED狀態,完成TCP三次握手。
完成三次握手,客戶端與伺服器開始傳送資料,在上述過程中,還有一些重要的概念:
未連線佇列:在三次握手協議中,伺服器維護一個未連線佇列,該佇列為每個客戶端的SYN包(syn=j)開設一個條目,該條目表明伺服器已收到SYN包,並向客戶發出確認,正在等待客戶的確認包。這些條目所標識的連線在伺服器處於Syn_RECV狀態,當伺服器收到客戶的確認包時,刪除該條目,伺服器進入ESTABLISHED狀態。 Backlog引數:表示未連線佇列的最大容納數目。
SYN-ACK 重傳次數:伺服器傳送完SYN-ACK包,如果未收到客戶確認包,伺服器進行首次重傳,等待一段時間仍未收到客戶確認包,進行第二次重傳,如果重傳次數超過系統規定的最大重傳次數,系統將該連線資訊從未連線佇列中刪除。注意,每次重傳等待的時間不一定相同。
未連線存活時間:是指未連線佇列的條目存活的最長時間,也即服務從收到SYN包到確認這個報文無效的最長時間,該時間值是所有重傳請求包的最長等待時間總和。有時我們也稱未連線存活時間為Timeout時間、SYN_RECV存活時間。
為什麼是三次握手
在謝希仁著《計算機網路》第四版中講“三次握手”的目的是為了防止已失效的連線請求報文段突然又傳送到了服務端,因而產生錯誤
“已失效的連線請求報文段”的產生在這樣一種情況下:client發出的第一個連線請求報文段並沒有丟失,而是在某個網路結點長時間的滯留了,以致延誤到連線釋放以後的某個時間才到達server。本來這是一個早已失效的報文段。但server收到此失效的連線請求報文段後,就誤認為是client再次發出的一個新的連線請求。於是就向client發出確認報文段,同意建立連線。假設不採用“三次握手”,那麼只要server發出確認,新的連線就建立了。由於現在client並沒有發出建立連線的請求,因此不會理睬server的確認,也不會向server傳送資料。但server卻以為新的運輸連線已經建立,並一直等待client發來資料。這樣,server的很多資源就白白浪費掉了。採用“三次握手”的辦法可以防止上述現象發生。例如剛才那種情況,client不會向server的確認發出確認。server由於收不到確認,就知道client並沒有要求建立連線。”
作者:wuxinliulei
來源:知乎
為什麼是四次揮手
參考文章 TCP四次揮手(圖解)-為何要四次揮手
TCP協議是一種面向連線的、可靠的、基於位元組流的運輸層通訊協議。TCP是全雙工模式,這就意味著,當主機1發出FIN報文段時,只是表示主機1已經沒有資料要傳送了,主機1告訴主機2,它的資料已經全部發送完畢了;但是,這個時候主機1還是可以接受來自主機2的資料;當主機2返回ACK報文段時,表示它已經知道主機1沒有資料傳送了,但是主機2還是可以傳送資料到主機1的;當主機2也傳送了FIN報文段時,這個時候就表示主機2也沒有資料要傳送了,就會告訴主機1,我也沒有資料要傳送了,之後彼此就會愉快的中斷這次TCP連線。如果要正確的理解四次分手的原理,就需要了解四次分手過程中的狀態變化。作者:李太白不白
來源:CSDN
- 當主機1 發出FIN報文時,只是告訴主機2,我已經沒有資料需要傳送了, 但是還是可以接收主機2的資料(第一次)
- 當主機2發出報文時,只是告訴主機1,我已經接收到訊號,知道你沒有資料再要傳送了, 但是主機2還是可以繼續傳送資料給主機1(第二次)
- 當主機2也真的沒有資料要傳送給主機1時,就會發送報文給主機1, 告訴主機1我也沒有資料需要傳送了(第三次)
- 主機1收到報文後,再次傳送報文給主機2,說明可以關閉連線了(第四次)
總結
從上面分析可知,每次請求資源都需要進行TCP連線,會有三次握手操作,才表示連線成功,連線成功後,伺服器才會向客戶端傳送資料,如果每次請求資源時都才進行連線,是很浪費時間的,我們可以在請求資源之前,先預先連線,在真正請求的時候,就已經連線上,之前傳送資源就可以,我們可以利用如下方式:
參考文章Head標籤裡面的dns-prefetch,preconnect,prefetch和prerender
<link rel="preconnect" href="//example.com">
<link rel="preconnect" href="//cdn.example.com" crossorigin>
複製程式碼
瀏覽器會進行以下步驟:
- 解釋href的屬性值,如果是合法的URL,然後繼續判斷URL的協議是否是http或者https否則就結束處理
- 如果當前頁面host不同於href屬性中的host,crossorigin其實被設定為anonymous(就是不帶cookie了),如果希望帶上cookie等資訊可以加上crossorign屬性,corssorign就等同於設定為use-credentials
快取處理
我們已經建立了TCP連線,服務端已經可以往客戶端(瀏覽器)傳送資源了,但是如果如果已經請求過一次資源了,但是我們重新整理頁面,我們還需要重新請求資源,這樣也太浪費請求了,瀏覽器解決再次請求有快取 策略,快取就是再次請求資源能儘量從已經請求的資源中獲取最好從而減少了請求次數,也就是不需要再次進行TCP連線,但是如果瀏覽器每次都檢視快取中否已經有了資源就不再次請求,這樣也會造成可能我們獲取到的資源不是最新的,所以針對著這兩種情況,瀏覽器快取有如下兩種策略:
- 強快取
- 協商快取(弱快取)
下面我們來針對著兩種策略來進行簡單的分析。
強快取
強快取主要是瀏覽器根據請求頭部的兩個欄位來判斷的:
- expires
- cache-control
強快取命中 from memory cache & from disk cache
在測試的時候,看到命中強快取時,有兩種狀態,200 (from memory cache) cache & 200 (from disk cache),於是去找了一下這兩者的區別:
- memory cache: 將資源存到記憶體中,從記憶體中獲取。
- disk cache:將資源快取到磁碟中,從磁碟中獲取。 二者最大的區別在於:當退出程序時,記憶體中的資料會被清空,而磁碟的資料不會。
其實如果我們一個頁面中存在請求多個一樣的圖片資源,瀏覽器會自動處理,從記憶體快取中自動獲取(from memory cache), 但是我們關閉了頁面或者重新整理了頁面,這個記憶體快取就失效了, 不過這個快取是瀏覽器自動幫我們處理的,我們做不了什麼處理.
expires
expires 是http 1.0 裡面的特性,通過指定資源指定快取到期GMT的絕對時間 來判斷資源是否過期,如果沒有過期就用快取,否則重新請求資源.
缺點: 由於使用具體時間,如果時間表示出錯或者沒有轉換到正確的時區都可能造成快取生命週期出錯。
cache-control
Cache-Control 是http1.1
中為了彌補Expires的缺陷而加入的,當Expires和Cache-Control同時存在時,Cache-Control優先順序高於Expires。
下面我們梳理下cache-control
的配置:
屬性 | 描敘 |
---|---|
max-age | 設定快取儲存的最大週期,超過這個時間快取被認為過期(單位秒)。cache: max-age=60 這裡是60秒 |
public/private | public 表示伺服器端和瀏覽器端都能快取, cache: max-age=60, public , private 表示只能使用者的瀏覽器才能快取,路由器已經CDN不能快取 |
no-cache | no-cache 不是說不快取,而是必須需要從伺服器去請求一次,如果快取還生效,則就伺服器只會返回304,不會返回請求相應體,請求不會減少,但是請求的資源可能減小( Express 快取策略中,如果請求頭部攜帶了cache-control 而且設定了no-cache 則只會重新返回新的資源,不會返回304 ) |
no-store | 不快取,使用協商快取 |
must-revalidate | 快取必須在使用之前驗證舊資源的狀態,並且不可使用過期資源。 |
如果cache-control 表示資源過期,或者設定了no-store, 並不是說明快取的資源不能再使用,瀏覽器還可以配合來使用協商快取, 下面我們就來分析協商快取
協商快取
如果強快取(cache-control)資源失效,瀏覽器就會呼叫協商快取策略,協商快取策略主要是通過如下的兩個請求頭部來處理:
- last-modified (if-modified-since) -> http 1.0
- Etag(if-none-match) -> http 1.1
last-modified
瀏覽器在請求伺服器資源時,伺服器會將檔案的最後修改時間,賦值給相應求頭last-modified
,如: last-moified: Fri,08 Jun 2018 10:2:30: GMT
再次請求這個資源時(重新整理頁面(不是強制重新整理F5 + Ctrl),或者重新開啟這個頁面), 請求頭部會新增一個if-modified-since
的頭部資訊,其值就是last-modified
的值, 如:if-modified-since:Fri,08 Jun 2018 10:2:30: GMT, 傳送給伺服器,伺服器會根據這個值來判斷快取是否生效,如果快取依舊生效,則返回一個304,和一個空的響應體 , 瀏覽器機會從快取讀取,否則返回200 並且返回請求結果
Etag
Etag 其實和last-modified 的效果一樣,都是後端針對相應的資源,返回的一個標識,只是last-modified 是資源最後的修改時間,etag 是資源相應的標識,不同的伺服器生成etag的策略是不一樣的。比如說,express 框架生成etag 的規則是 檔案最後一次修改時間-檔案的大小
function stattag (stat) {
// mtime 檔案最後一次的修改時間
// size 檔案的大小
var mtime = stat.mtime.getTime().toString(16)
var size = stat.size.toString(16)
return '"' + size + '-' + mtime + '"'
}
複製程式碼
再次請求資源時(重新整理頁面(不是強制重新整理F5 + Ctrl),或者重新開啟這個頁面),請求頭部會新增一個if-none-match
的請求頭髮送給伺服器,伺服器會根據這個值來判斷快取是否生效, 如果快取依舊生效,則返回一個304,和一個空的響應體 , 瀏覽器機會從快取讀取,否則返回200 並且返回請求結果。
從上面的分析感覺last-modified
和etag
的功能,應該一樣,為什麼在HTTP 1.1 會出現etag 的概念呢,etag 主要是解決了如下問題:
- 一些檔案也許內容並不改變(僅僅改變的修改時間),這個時候我們不希望檔案重新載入。(Etag值會觸發快取,Last-Modified不會觸發)( 從express 生成的etag 的規則來看,這個問題並不存在 )
- If-Modified-Since能檢查到的粒度是秒級的,當修改非常頻繁時,Last-Modified會觸發快取,而Etag的值不會觸發,重新載入。
- 某些伺服器不能精確的得到檔案的最後修改時間。
如果同時設定了last-modified
和etag
標籤,那誰的優先順序更高呢
如果同時設定了last-modified
和etag
標籤,那誰的優先順序更高呢? 規定是etag優先生效, 那為什麼etag 為什麼會優先於last-modified 呢?是由瀏覽器決定的?
經過分析,不是由瀏覽器決定的,而是有伺服器 決定的。瀏覽器只是在請求資源的時候攜帶last-modified 和etag 的請求頭到伺服器,接下來就由伺服器來決定快取是否可以用, 我們可以檢視下express 的處理邏輯的原始碼來分析:
if (this.isCachable() && this.isFresh()) {
this.notModified()
return
}
複製程式碼
其中this.notModified()
就是直接返回一個304
:
SendStream.prototype.notModified = function notModified () {
var res = this.res
debug('not modified')
this.removeContentHeaderFields()
res.statusCode = 304
res.end()
}
複製程式碼
express 判斷快取是否生效最主要的邏輯是在this.isFresh()
方法中實現:
function fresh (reqHeaders, resHeaders) {
// fields
var modifiedSince = reqHeaders['if-modified-since']
var noneMatch = reqHeaders['if-none-match']
// unconditional request
if (!modifiedSince && !noneMatch) {
return false
}
// Always return stale when Cache-Control: no-cache
// to support end-to-end reload requests
// https://tools.ietf.org/html/rfc2616#section-14.9.4
var cacheControl = reqHeaders['cache-control']
if (cacheControl && CACHE_CONTROL_NO_CACHE_REGEXP.test(cacheControl)) {
return false
}
// if-none-match
if (noneMatch && noneMatch !== '*') {
var etag = resHeaders['etag']
if (!etag) {
return false
}
var etagStale = true
var matches = parseTokenList(noneMatch)
for (var i = 0; i < matches.length; i++) {
var match = matches[i]
if (match === etag || match === 'W/' + etag || 'W/' + match === etag) {
etagStale = false
break
}
}
if (etagStale) {
return false
}
}
// if-modified-since
if (modifiedSince) {
var lastModified = resHeaders['last-modified']
var modifiedStale = !lastModified || !(parseHttpDate(lastModified) <= parseHttpDate(modifiedSince))
if (modifiedStale) {
return false
}
}
return true
}
複製程式碼
我們可以根據上面的程式碼來具體分析express
具體是怎樣來判斷快取是否生效
- 如果請求頭部沒有攜帶
if-modified-since
和if-none-match
頭部,就直接判斷快取失效
var modifiedSince = reqHeaders['if-modified-since']
var noneMatch = reqHeaders['if-none-match']
if (!modifiedSince && !noneMatch) {
return false
}
複製程式碼
- 如果請求頭部有
cache-control
, 並且有設定no-cache , 則直接判斷快取失效(var CACHE_CONTROL_NO_CACHE_REGEXP = /(?:^|,)\s*?no-cache\s*?(?:,|$)/
)
var cacheControl = reqHeaders['cache-control']
//
if (cacheControl && CACHE_CONTROL_NO_CACHE_REGEXP.test(cacheControl)) {
return false
}
複製程式碼
- 然後來判斷
if-none-match
, 判斷的方法就是重新獲取一個etag
, 然後判斷if-none-match
是否與etag
相等, 如果不相等, 就直接判斷快取失效
// if-none-match
if (noneMatch && noneMatch !== '*') {
var etag = resHeaders['etag']
if (!etag) {
return false
}
var etagStale = true
var matches = parseTokenList(noneMatch)
for (var i = 0; i < matches.length; i++) {
var match = matches[i]
if (match === etag || match === 'W/' + etag || 'W/' + match === etag) {
etagStale = false
break
}
}
if (etagStale) {
return false
}
}
複製程式碼
其中var etag = resHeaders['etag']
是在請求時,重新獲取的etag
- 最後來判斷
if-modified-since
,其判斷的邏輯是,如果last-modified
的值小於等於if-modified-since
的值, 則直接判斷快取失效
// if-modified-since
if (modifiedSince) {
var lastModified = resHeaders['last-modified']
var modifiedStale = !lastModified || !(parseHttpDate(lastModified) <= parseHttpDate(modifiedSince))
if (modifiedStale) {
return false
}
}
複製程式碼
從上面的分析可知,其實Express
的快取生效機制並沒有遵循etag
的優先順序高於last-modified
,而是在判斷失效 的機制遵循了etag
的優先順序高於last-modified
.
繼續...