瀏覽器輸入URL到 請求全過程以及相應的性能優化

前言

對http 的理解,一直都出於看完資料,沒過幾天又忘記了下面這篇文章只是本身對http的整個過程一個梳理,而且從http的請求過來,來簡單的進行性能優化進行一個梳理。 其中主要設計以下幾個環節:html

  1. http請求全過程
  2. DNS解析全過程(dns-prefetch, preconnect, preload, prefetch, def, async)
  3. TCP 鏈接(三次握手,四次揮手(爲何須要三次握手,四次揮手))
  4. 緩存處理(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)
  5. PageSpend 和LightHouse 來進行性能分析

對上面的幾個環節的梳理,都是借鑑前輩們的分析成果,後面會列出全部的文章鏈接。express

http 請求全過程

參考文章 當你輸入一個網址的時候,實際會發生什麼?segmentfault

  1. 在瀏覽器地址欄輸入地址,好比說: fecebook.com
  2. 瀏覽器經過域名查找IP地址(DNS解析)
  3. 瀏覽器給Web服務器發送一個HTTP請求
  4. facebook 服務器進行永久重定向

由於咱們輸入的fecebook.com,而不是http://www.facebook.com/因此服務器自動進行了永久重定向,返回的是301 狀態碼後端

爲何服務器必定要重定向而不是直接發會用戶想看的網頁內容呢?這個問題有好多有意思的答案。瀏覽器

其中一個緣由跟搜索引擎排名有 關。你看,若是一個頁面有兩個地址,就像http://www.igoro.com/ 和http://igoro.com/,搜索引擎會認爲它們是兩個網站,結果形成每個的搜索連接都減小從而下降排名。而搜索引擎知道301永久重定向是 什麼意思,這樣就會把訪問帶www的和不帶www的地址歸到同一個網站排名下。緩存

還有一個是用不一樣的地址會形成緩存友好性變差。當一個頁面有好幾個名字時,它可能會在緩存裏出現好幾回。性能優化

  1. 瀏覽器跟蹤重定向地址
  2. 服務器處理"請求
  3. 服務器返回一個HTML響應
  4. 瀏覽器解析HTML並繪製頁面
  5. 瀏覽器發送潛入在HTML中的對象,好比說圖片,CSS樣式,JS文件,字體等
  6. 瀏覽器發送Ajax 請求

從上面咱們已經知道從輸入URL到展現頁面的整個大體過程,下面咱們會針對其中幾個關鍵步驟再次深刻分析bash

DNS 解析過程

在上面咱們已經知道了,咱們輸入一個URL,發起請求,其實接收請求的最終一個服務器,而每一個服務器都一個IP地址,因此通常一個域名對一個一個IP地址(也有對應多個IP地址的,咱們暫時值分析對一個IP地址的狀況), 可是瀏覽器怎麼知道域名到底對應的是那個IP地址呢,這個就是涉及到怎麼去域名解析了。 域名解析主要是以下過程:服務器

  1. 經過瀏覽器緩存來查找,若是緩存中存在,則就不會繼續查找,直接用查找到的IP地址,咱們能夠在 Chrome 中查看咱們瀏覽器中緩存的全部的DNS.
  2. 若是瀏覽器沒有查找到,咱們就會在咱們電腦中去查找是否保存了對應的域名信息
  3. 若是本地沒有保存,就會從路由器去查找
  4. 若是路由器都沒有,就會去ISP 中查找

從上面分析可知,咱們輸入一個域名須要去作DNS解析找到IP,可是在咱們的代碼中,常常將一些靜態資源放在CDN中,每一個CDN地址咱們都要去作下DNS解析,這個會浪費時間,咱們能夠經過預先進行DNS解析,而後在請求的時候,DNS已經解析完成就不用等待了cookie

<!--在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

連接:www.zhihu.com/question/24…

來源:知乎

爲何是四次揮手

參考文章 TCP四次揮手(圖解)-爲什麼要四次揮手

TCP協議是一種面向鏈接的、可靠的、基於字節流的運輸層通訊協議。TCP是全雙工模式,這就意味着,當主機1發出FIN報文段時,只是表示主機1已經沒有數據要發送了,主機1告訴主機2,它的數據已經所有發送完畢了;可是,這個時候主機1仍是能夠接受來自主機2的數據;當主機2返回ACK報文段時,表示它已經知道主機1沒有數據發送了,可是主機2仍是能夠發送數據到主機1的;當主機2也發送了FIN報文段時,這個時候就表示主機2也沒有數據要發送了,就會告訴主機1,我也沒有數據要發送了,以後彼此就會愉快的中斷此次TCP鏈接。若是要正確的理解四次分手的原理,就須要瞭解四次分手過程當中的狀態變化。


做者:李太白不白

來源:CSDN

原文:blog.csdn.net/daguairen/a…

  1. 當主機1 發出FIN報文時,只是告訴主機2,我已經沒有數據須要發送了, 可是仍是能夠接收主機2的數據(第一次)
  2. 當主機2發出報文時,只是告訴主機1,我已經接收到信號,知道你沒有數據再要發送了, 可是主機2仍是能夠繼續發送數據給主機1(第二次)
  3. 當主機2也真的沒有數據要發送給主機1時,就會發送報文給主機1, 告訴主機1我也沒有數據須要發送了(第三次)
  4. 主機1收到報文後,再次發送報文給主機2,說明能夠關閉鏈接了(第四次)

總結

從上面分析可知,每次請求資源都須要進行TCP鏈接,會有三次握手操做,才表示鏈接成功,鏈接成功後,服務器纔會向客戶端發送數據,若是每次請求資源時都才進行鏈接,是很浪費時間的,咱們能夠在請求資源以前,先預先鏈接,在真正請求的時候,就已經鏈接上,以前發送資源就能夠,咱們能夠利用以下方式:

參考文章Head標籤裏面的dns-prefetch,preconnect,prefetch和prerender

<link rel="preconnect" href="//example.com">
<link rel="preconnect" href="//cdn.example.com" crossorigin>
複製代碼

瀏覽器會進行如下步驟:

  1. 解釋href的屬性值,若是是合法的URL,而後繼續判斷URL的協議是不是http或者https不然就結束處理
  2. 若是當前頁面host不一樣於href屬性中的host,crossorigin其實被設置爲anonymous(就是不帶cookie了),若是但願帶上cookie等信息能夠加上crossorign屬性,corssorign就等同於設置爲use-credentials

緩存處理

咱們已經創建了TCP鏈接,服務端已經能夠往客戶端(瀏覽器)發送資源了,可是若是若是已經請求過一次資源了,可是咱們刷新頁面,咱們還須要從新請求資源,這樣也太浪費請求了,瀏覽器解決再次請求有緩存 策略,緩存就是再次請求資源能儘可能從已經請求的資源中獲取最好從而減小了請求次數,也就是不須要再次進行TCP鏈接,可是若是瀏覽器每次都查看緩存中否已經有了資源就再也不次請求,這樣也會形成可能咱們獲取到的資源不是最新的,因此針對着這兩種狀況,瀏覽器緩存有以下兩種策略:

  1. 強緩存
  2. 協商緩存(弱緩存)

下面咱們來針對着兩種策略來進行簡單的分析。

強緩存

參考文章當咱們在談論HTTP緩存時咱們在談論什麼

強緩存主要是瀏覽器根據請求頭部的兩個字段來判斷的:

  1. expires
  2. cache-control

強緩存命中 from memory cache & from disk cache

在測試的時候,看到命中強緩存時,有兩種狀態,200 (from memory cache) cache & 200 (from disk cache),因而去找了一下這二者的區別:

  1. memory cache: 將資源存到內存中,從內存中獲取。
  2. 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)資源失效,瀏覽器就會調用協商緩存策略,協商緩存策略主要是經過以下的兩個請求頭部來處理:

  1. last-modified (if-modified-since) -> http 1.0
  2. 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-modifiedetag的功能,應該同樣,爲何在HTTP 1.1 會出現etag 的概念呢,etag 主要是解決了以下問題:

  1. 一些文件也許內容並不改變(僅僅改變的修改時間),這個時候咱們不但願文件從新加載。(Etag值會觸發緩存,Last-Modified不會觸發)( 從express 生成的etag 的規則來看,這個問題並不存在 )
  2. If-Modified-Since能檢查到的粒度是秒級的,當修改很是頻繁時,Last-Modified會觸發緩存,而Etag的值不會觸發,從新加載。
  3. 某些服務器不能精確的獲得文件的最後修改時間。

若是同時設置了last-modifiedetag 標籤,那誰的優先級更高呢

若是同時設置了last-modifiedetag 標籤,那誰的優先級更高呢? 規定是etag優先生效, 那爲何etag 爲何會優先於last-modified 呢?是由瀏覽器決定的?

通過分析,不是由瀏覽器決定的,而是有服務器 決定的。瀏覽器只是在請求資源的時候攜帶last-modifiedetag 的請求頭到服務器,接下來就由服務器來決定緩存是否能夠用, 咱們能夠查看下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具體是怎樣來判斷緩存是否生效

  1. 若是請求頭部沒有攜帶if-modified-sinceif-none-match頭部,就直接判斷緩存失效
var modifiedSince = reqHeaders['if-modified-since']
  var noneMatch = reqHeaders['if-none-match']
  if (!modifiedSince && !noneMatch) {
    return false
  }
複製代碼
  1. 若是請求頭部有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
  }
複製代碼
  1. 而後來判斷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

  1. 最後來判斷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.

繼續...

相關文章
相關標籤/搜索