從輸入網址到頁面呈現都發生了什么?


在前端開發中我們常常需要考慮首屏加載時間,為了盡可能減少首屏加載時間我們需要弄清楚從輸入網址到頁面最終呈現的過程中都發生了哪些事情,然后才能具體問題具體分析,最終達到提升網頁性能的目的。從輸入網址到頁面呈現過程中都發生了什么?據說這是一個非常經典的面試題,考察的問題面也很廣,今天我就從一個前端開發工程師的角度來解答一下這個問題,文中難免有些知識點介紹的不夠深,還望見諒!

從輸入網址到頁面呈現這個過程大致可分為以下這幾個部分:

  1. 網絡通信
  2. 頁面渲染

網絡通信

輸入網址

當我們在瀏覽器的地址欄輸入網址例如(http://www.baidu.com),http://代表使用超文本傳輸協議,www.baidu.com代表服務器地址,baidu.com代表域名。一個完整的URL包括協議、服務器地址(主機)、端口、路徑

負責域名查詢與解析的DNS服務

用戶通常使用主機名或域名來訪問某網站,而不是直接通過IP來訪問,因為字母數字配合的表示形式更符合人類的記憶習慣,可計算機卻不理解這些名稱,因此DNS服務應運而生,DNS協議提供通過域名查找IP地址,或逆向從IP地址反查域名的服務。
DNS查詢過程如下:

  1. 操作系統會先檢查本地的hosts文件是否有這個網址映射關系,如果有,就先調用這個IP地址映射,完成域名解析。
  2. 如果hosts里沒有這個域名的映射,則查找本地DNS解析器緩存,是否有這個網址映射關系,如果有,直接返回,完成域名解析。
  3. 如果hosts與本地DNS解析器緩存都沒有相應的網址映射關系,首先會找TCP/IP參數中設置的首選DNS服務器,在此我們叫它本地DNS服務器,此服務器收到查詢時,如果要查詢的域名,包含在本地配置區域資源中,則返回解析結果給客戶機,完成域名解析,此解析具有權威性。
  4. 如果要查詢的域名,不由本地DNS服務器區域解析,但該服務器已緩存了此網址映射關系,則調用這個IP地址映射,完成域名解析,此解析不具有權威性。
  5. 如果本地DNS服務器本地區域文件與緩存解析都失效,則根據本地DNS服務器的設置(是否設置轉發器)進行查詢,如果未用轉發模式,本地DNS就把請求發至13台根DNS,根DNS服務器收到請求后會判斷這個域名(.com)是誰來授權管理,並會返回一個負責該頂級域名服務器的一個IP。本地DNS服務器收到IP信息后,將會聯系負責.com域的這台服務器。這台負責.com域的服務器收到請求后,如果自己無法解析,它就會找一個管理.com域的下一級DNS服務器地址(baidu.com)給本地DNS服務器。當本地DNS服務器收到這個地址后,就會找baidu.com域服務器,重復上面的動作,進行查詢,直至找到www.baidu.com主機。
  6. 如果用的是轉發模式,此DNS服務器就會把請求轉發至上一級DNS服務器,由上一級服務器進行解析,上一級服務器如果不能解析,或找根DNS或把轉請求轉至上上級,以此循環。不管是本地DNS服務器用是是轉發,還是根提示,最后都是把結果返回給本地DNS服務器,由此DNS服務器再返回給客戶機。

從客戶端到本地DNS服務器是屬於遞歸查詢,而DNS服務器之間就是的交互查詢就是迭代查詢。

應用層 客戶端發送HTTP請求報文

HTTP報文包括:

  • 報文首部 (請求行+各種首部字段+其他)
  • 空行
  • 報文主體 (應被發送的數據)通常並不一定要有報文主體

下面對百度首頁請求報文首部進行分析:
請求行

請求方法GET 請求URI /   HTTP協議版本 1.1
GET / HTTP/1.1                                  

首部字段

請求資源所在服務器
Host: www.baidu.com
連接方式:持久連接     HTTP/1.1之前版本默認非持久連接
Connection: keep-alive
報文指令:要求所有中間服務器不返回緩存資源
Pragma: no-cache
控制緩存的行為:緩存前必須先確認其有效性,防止從緩存中返回過期的資源
Cache-Control: no-cache
用戶代理可處理的媒體類型
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8   q表示權重從而區分優先級
http客戶端瀏覽器信息
User-Agent: Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.87 Safari/537.36
可接受的內容編碼類型
Accept-Encoding: gzip, deflate, sdch
可接受的語言
Accept-Language: zh-CN,zh;q=0.8
相關信息或標記
Cookie: BAIDUID=3C67AA3EF6B3347D3AA986CE489268C4:FG=1; BIDUPSID=3C67AA3EF6B3347D3AA986CE489268C4;

傳輸層 確保傳輸報文可靠性的TCP協議

位於傳輸層的TCP協議為傳輸報文提供可靠的字節流服務。為了方便傳輸,將大塊的數據分割成以報文段為單位的數據包進行管理,並為它們編號,方便服務器接收時能准確地還原報文信息。TCP協議通過“三次握手”等方法保證傳輸的安全可靠。
“三次握手”的過程是,發送端先發送一個帶有SYN(synchronize)標志的數據包給接收端,在一定的延遲時間內等待接收的回復。接收端收到數據包后,傳回一個帶有SYN/ACK標志的數據包以示傳達確認信息。接收方收到后再發送一個帶有ACK標志的數據包給接收端以示握手成功。在這個過程中,如果發送端在規定延遲時間內沒有收到回復則默認接收方沒有收到請求,而再次發送,直到收到回復為止。
詳細過程如下圖
三次握手

網絡層 負責傳輸的IP協議

IP協議的作用是把TCP分割好的各種數據包傳送給接收方。而要保證確實能傳到接收方還需要接收方的MAC地址,也就是物理地址。IP地址和MAC地址是一一對應的關系,一個網絡設備的IP地址可以更換,但是MAC地址一般是固定不變的。ARP協議可以將IP地址解析成對應的MAC地址。當通信的雙方不在同一個局域網時,需要多次中轉才能到達最終的目標,在中轉的過程中需要通過下一個中轉站的MAC地址來搜索下一個中轉目標。具體過程如下圖:
IP協議

鏈路層 傳輸數據的硬件部分

在網絡層找到對方的MAC地址后,就將數據發送到數據鏈路層傳輸。至此請求報文已發出,客戶端發送請求的階段結束

服務器接收報文

接收端服務器在鏈路層接收到數據后,刪除該層的首部信息並向網絡層傳遞,網絡層將接收的數據向傳輸層傳遞,在傳輸層會將傳輸的數據按序號從組請求報文並傳送給應用層。當數據傳輸到應用層才能算真正接收到由客戶端發送過來的HTTP請求

應用層 服務器發送HTTP響應報文

下面對百度首頁響應報文首部進行分析:
狀態行

協議版本 狀態碼 狀態碼原因短語
HTTP/1.1 200 OK

首部字段

當前服務器上安裝的HTTP服務器程序信息
bfe:Baidu Front End。百度人自己寫的反向代理及防攻擊接入層
Server: bfe/1.0.8.18
響應日期時間
Date: Thu, 08 Dec 2016 14:48:19 GMT
說明報文實體的媒體類型
Content-Type: text/html; charset=utf-8
傳輸編碼方式:分塊編碼
Transfer-Encoding: chunked
鏈接方式:持久鏈接  http/1.1之后這個已經沒必要了
Connection: keep-alive
只接受對持相同自然語言的請求返回緩存
Vary: Accept-Encoding
緩存控制:僅向特定用戶返回響應
Cache-Control: private
Cxy_all: baidu+43a6e396a3ed26dc7d1de13c6af79e49
緩存過期時間
Expires: Thu, 08 Dec 2016 14:47:38 GMT
X-Powered-By: HPHP
X-UA-Compatible: IE=Edge,chrome=1
Strict-Transport-Security: max-age=172800
BDPAGETYPE: 1
BDQID: 0xc9d964a600018bb8
BDUSERID: 0
設置cookie
Set-Cookie: H_PS_PSSID=1451_21116_17001_21408_21417_21554_20929; path=/;

響應報文的傳輸方式與請求報文相同,簡單點說就是原路返回
在響應報文中我們通過Chrome DevTool的Network面板可以看到輸入的www.baidu.com會被重定向到https://www.baidu.com/,點擊重定向后的www.baidu.com,在右邊的Response面板中可以看到客戶端接收到的報文實體即返回的HTML頁面代碼

網絡通信流程圖
網絡通信流程圖

在網絡通信階段對前端優化建議:

  1. 減少HTTP請求數
    1. 合並資源,如合並 JavaScript 文件、CSS 文件,利用 CSS Sprite 合並圖片等
    2. 內聯圖片,data url節省了HTTP請求,但是如果這個圖像在網頁多個地方顯示會加大網頁的內容,延長下載時間。
  2. 域名提前解析,在頁面中不同域名的鏈接需指定預取域名:<link rel="dns-prefetch" href="http://this-is-a.com">,IE9+支持
  3. 避免重定向(重定向會增加http請求的次數)
  4. cookie優化,cookie越多會導致請求頭越大
  5. 啟用GZIP壓縮(Accept-Encoding:g-zip)
  6. 使用 CDN加速,減小服務器壓力
  7. 合理利用HTTP緩存,通過設置Expires

頁面渲染

客戶端在接收到html代碼之后,接下來的流程如下:

解析html以構建DOM樹

解析一個文檔即將其轉換為具有一定意義的結構(編碼可以理解和使用的東西)。解析的結果通常是表達文檔結構的節點樹,稱為解析樹或語法樹。
解析是以文檔所遵循的語法規則(編寫文檔所用的語言或格式)為基礎的。所有可以解析的格式都必須對應確定的語法(由詞匯和語法規則構成)。這稱為與上下文無關的語法。
解析的過程可以分成兩個子過程:詞法分析和語法分析。

  • 詞法分析是將輸入內容分割成大量標記的過程。標記是語言中的詞匯,即構成內容的單位。在人類語言中,它相當於語言字典中的單詞。
  • 語法分析是應用語言的語法規則的過程。

解析器通常將解析工作分給以下兩個組件來處理:

  • 詞法分析器(有時也稱為標記生成器),負責將輸入內容分解成一個個有效標記;
  • 而解析器負責根據語言的語法規則分析文檔的結構,從而構建解析樹。

由於不能使用常規的解析技術,瀏覽器就創建了自定義的解析器來解析 HTML。此解析算法由兩個階段組成:標記化和樹構建。
具體的解析過程可參考瀏覽器的工作原理中的標記化算法和構建樹算法

解析器的輸出“解析樹”是由 DOM 元素和屬性節點構成的樹結構。DOM 是文檔對象模型 (Document Object Model) 的縮寫。它是 HTML 文檔的對象表示,同時也是外部內容(例如 JavaScript)與 HTML 元素之間的接口。解析樹的根節點是“Document”對象。

當解析到link標簽時會請求相應的CSS文件,並將其CSS規則解析為StyleSheet對象,CSS文件中的其他外鏈資源如背景圖片等只有等到其規則與DOM樹某節點相匹配時才會加載
當解析遇到img標簽時會根據路徑向服務器相應的資源文件夾中請求圖片資源,但並不會等待圖片資源下載完再去解析接下來的html,而是並發執行即圖片資源仍在下載,html解析也在進行。如果沒有定義圖片的height和width屬性,那么瀏覽器為了能夠顯示每一個加載的圖像,它需要先下載圖像,然后解析出圖像的高度和寬度,並在顯示窗口留出相應的屏幕空間,這樣就會導致瀏覽器不斷地重新計算/調整頁面的布局,這可能會延遲文檔的顯示,並導致頁面重繪。
當解析遇到script標簽時,將啟動 JavaScript 引擎,這時將阻塞 DOM 樹的構建。因為 JavaScript 執行過程中, JavaScript 很可能會對 DOM 樹進行讀寫操作。直到 JavaScript 執行完畢(此時執行的是全局對象初始創建和全局上下文中代碼的執行),DOM樹才會恢復構建。

構建render樹

為了更好地用戶體驗效果,瀏覽器會在構建DOM樹的同時,也在構建render樹。呈現樹的每一個節點即為與其相對應的DOM節點的CSS框,框的類型與DOM節點的display屬性有關,block元素生成block框,inline元素生成inline框。每一個呈現樹節點都有與之相對應的DOM節點,但DOM節點不一定有與之相對應的呈現樹節點,比如display屬性為none的DOM節點,而且呈現樹節點在呈現樹中的位置與他們在DOM樹中的位置不一定相同,比如float與絕對定位元素。在構建render樹的時候需要為DOM樹匹配CSS規則,在這個階段因為匹配規則是從右往左匹配的,所以css的編寫規則很重要。不好的CSS選擇器寫法會影響到頁面渲染的效率,具體是如何編寫高效的CSS規則的可參考這篇文章CSS選擇器性能分析

布局render樹

在創建render樹時,並不包含位置和大小信息。計算這些值的過程稱為布局或重排。布局是一個遞歸的過程,它從根元素開始,然后遞歸遍歷部分或所有的框架層次結構,為每一個需要計算的呈現器計算幾何信息。
布局通常具有以下模式:

  1. 父呈現器確定自己的寬度。
  2. 父呈現器依次處理子呈現器,並且:
    1. 放置子呈現器(設置 x,y 坐標)。
    2. 如果有必要,調用子呈現器的布局,這會計算子呈現器的高度。
  3. 父呈現器根據子呈現器的累加高度以及邊距和補白的高度來設置自身高度,此值也可供父呈現器的父呈現器使用。
  4. 將其 dirty 位設置為 false

繪制render樹

在繪制階段,系統會遍歷render樹,並調用呈現器的“paint”方法,將呈現器的內容顯示在屏幕上。繪制工作是使用用戶界面基礎組件完成的。和布局一樣,繪制也分為全局(繪制整個呈現樹)和增量兩種。在增量繪制中,部分呈現器發生了更改,但是不會影響整個樹。更改后的呈現器將其在屏幕上對應的矩形區域設為無效,這導致 OS 將其視為一塊“dirty 區域”,並生成“paint”事件。
繪制順序:

  1. 背景顏色
  2. 背景圖片
  3. 邊框
  4. 子代
  5. 輪廓

頁面變化造成的影響

在發生變化時,瀏覽器會盡可能做出最小的響應。因此,元素的顏色改變后,只會對該元素進行重繪。元素的位置改變后,只會對該元素及其子元素(可能還有同級元素)進行布局和重繪。添加 DOM 節點后,會對該節點進行布局和重繪。一些重大變化(例如增大“html”元素的字體)會導致緩存無效,使得整個呈現樹都會進行重新布局和繪制。

在頁面渲染階段對前端優化建議:

  • 建議將 CSS 文件放在頁首,以便構建 DOM 樹;而將 JavaScript 文件盡量放在頁面下方,防止阻塞構建 DOM 樹;而 JavaScript 的 onload 事件里,不要寫太多影響首屏渲染的、操作 DOM 樹的 JavaScript 代碼。
  • 精簡 JavaScript 和 CSS 代碼,並進行代碼壓縮,使得css和js資源更快的下載
  • 編寫高效的CSS代碼
  • 重要的圖片或者想讓用戶優先看到的圖片使用img標簽,次要的圖片使用background引入

參考文獻:

  1. 《圖解HTTP》
  2. 瀏覽器的工作原理
  3. 《高性能網站建設指南》

由於個人水平有限,不夠詳細或有誤的地方還望指出,共同進步才是最好的結果


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM