天天看點

從輸入cnblogs.com到部落格園首頁完全展示發生了什麼

之前面試時候經常被問及從輸入一個網址到頁面完全展示出來都發生了什麼,支支吾吾回答沒有底氣,仔細研究了一下,發現裡面學問還真不少。這些被浏覽器封裝起來的東西,了解之後才對前端的一些流行做法恍然大悟。

閱讀目錄

  • DNS 解析成 IP 位址
  • 發送 http 請求
  • TCP 傳輸封包
  • IP 尋址
  • 封裝成幀
  • 實體傳輸
  • 頁面渲染主流程
  • dom樹和render樹的關系
  • 布局render樹(layout)
  • 繪制(paint)

之前面試時候經常被問及這個問題,支支吾吾回答沒有底氣,仔細研究了一下,發現裡面學問還真不少。

從輸入 cnblogs.com 到部落格園首頁完全展現這個過程可以大緻分為 網絡通信 和 頁面渲染 兩個步驟。

網絡通信走的五層網際網路協定棧(OSI标準是七層模型,但實際實作通常是五層)。畫了一張圖:

從輸入cnblogs.com到部落格園首頁完全展示發生了什麼

五層網際網路協定棧

DNS屬于應用層協定。用戶端會先檢查本地是否有對應的 ip 位址,如果有就傳回,否則就會請求上級 DNS 伺服器,知道找到或到根節點。這一過程可能會非常耗時,使用 dns-prefetch 可使浏覽器在空閑時提前将這些域名轉化為 ip 位址,真正請求資源時就避免了這個過程的時間。例如京東首頁的處理:

從輸入cnblogs.com到部落格園首頁完全展示發生了什麼

京東首頁dns-prefetch處理

HTTP也是應用層協定。HTTP(HyperText Transport Protocol)定義了一個基于請求/響應模式的、無狀态的、應用層的協定,用于從網際網路伺服器傳輸超文本到本地浏覽器。絕大多數的Web開發,都是建構在HTTP協定之上的Web應用。用戶端組織并發送 http 請求封包,包含 method、url、host、cookie 等資訊,下面是通路部落格園首頁時 http 請求封包的樣子:

GET https://www.cnblogs.com/ HTTP/1.1
Host: www.cnblogs.com
Connection: keep-alive
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36
Upgrade-Insecure-Requests: 1
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Cookie: __gads=ID=b62b1e22b7de2e02:T=1493954370:S=ALNI_MYRebVRavER2PJmwdeFwpl33ACNoQ;
If-Modified-Since: Mon, 27 Nov 2017 12:21:04 GMT
 
           

請求頭裡的每個字段都有各自的作用,具體含義可查閱 http 協定相關文章。

TCP 将 http 長封包劃分為短封包,通過“三次握手”與伺服器建立連接配接,進行可靠傳輸。“三次握手”建立連接配接的過程和打電話極像:

用戶端:喂,我要和 Server 通話

服務端:你好,我是 Server,你是 Client 嗎

用戶端:沒錯,我是 Client

連接配接建立成功,接下來就可以正式傳送資料了。

資料傳完之後斷開tcp連接配接還要通過“四次揮手”,大概意思如下:

用戶端:Server 小寶貝,我話說完了,你挂電話吧

服務端:我不挂,我不挂,你先挂,你不挂我也不挂

---------------- Client 一陣無語 --------------

服務端:你挂了嗎

用戶端:行,那我先挂了

至此完成了一次完整的資源請求響應。

需要注意的是,浏覽器對同一域名下并發的tcp連接配接數是有限制的,2個到10個不等。為了解決這個資源加載瓶頸,有幾種流行的優化方案:

# 資源打包,合并請求

比如頁面樣式全部打包在一個 css 檔案内,頁面邏輯全部打包在一個 js 檔案内,圖檔拼合成雪碧圖,這樣可有效減少頁面的資源請求數量。webpack 是時下最流行的子產品打包工具之一,它可以将頁面内所有資源(包括js,css,圖檔,字型等等)都打包進一個 js 檔案,不明覺厲。

# 域名拆分,資源分散存儲

當浏覽器向伺服器請求一個靜态資源時,會先發送該域名下的 cookies,伺服器對于這些 cookie 根本不會做任何處理,是以它們隻是在毫無意義的消耗帶寬,是以應該確定對于靜态内容的請求是無 cookie 的請求(也就是所謂的 cookie-free)。将站點的 js、css、圖檔等靜态檔案放在一個專門的域名下通路,由于該域名與主站域名不同,是以浏覽器就不會把主域名下的 cookies 傳給該域,進而減少網絡開銷,特别是細碎靜态檔案特别多的情況下效果顯著。

另一方面,由于浏覽器是基于域名的并發連接配接數限制,而不是頁面。是以将資源部署在不同的域名下可以使頁面的總并發連接配接數得到線性提升。

# Connection: keep-alive,複用已建立的連接配接

在 http 早期,每個 http 請求都要打開一個 tcp 連接配接,請求完就關閉這個連接配接,導緻每個請求都要來一遍“三次握手”和“四次揮手”,進而磨磨唧唧多出來大量無謂的等待時間。就好比出去吃飯,等飯等半個小時,端上來十分鐘吃完了,結賬排隊又等了半個小時,要是剛進來就吃現成的吃完就跑那多爽啊。keep-alive 幹的就是這件事,當第一個請求資料傳輸完畢之後,伺服器說“用戶端你不要關閉這個連接配接,直接換下個請求,我不想再握你的破手了”。這樣下個請求就直接傳輸資料而不用先走“三次握手”的流程了。這好比你又去吃飯,吃你最喜歡的紅燒肉,飯店在今天第一個客人點紅燒肉的時候就炒了一大鍋紅燒肉,你點餐的時候直接吃現成的就行了,吃完直接跑,哈哈美滋滋。

# 控制緩存

将靜态資源強制緩存在用戶端,通過添加檔案指紋等方式使用戶端隻請求發生了變更的資源,可有效降低靜态資源請求數量。具體可參看前端靜态資源緩存控制政策。

# 延遲加載,懶加載,按需加載

很多頁面浏覽量雖然很大,但其實很大比例使用者掃完第一屏就直接跳走了,第一屏以下的内容使用者根本就不感興趣。 對于超大流量的網站,這個問題尤其重要。這時可根據使用者的行為進行按需加載,使用者用到了就去加載,用不到就不去加載。

以上都是從減少建立tcp連接配接數量的角度去優化頁面性能,之後會分享更多前端性能優化方面的實用方法。

Internet Protocol 是定義網絡之間彼此互聯規則的協定,主要解決邏輯尋址和網絡通用資料傳輸格式兩個問題。

所有連接配接到網際網路上的裝置都會被配置設定一個唯一的 IP 位址,就像網購時填寫的收貨位址一樣。由于一個網絡裝置的 IP 位址可以更換,但是 MAC 硬體位址(就像身份證号)一般是固定不變的,是以首先使用 ARP 協定來找到目标主機的 MAC 硬體位址。當通信的雙方不在同一個區域網路時,需要多次中轉(路由器)才能找到最終的目标,在中轉的過程中還需要通過下一個中轉站的 MAC 位址來搜尋下一個中轉目标。

傳輸層傳來的 TCP 封包會在這一層被 IP 封裝成網絡通用傳輸格式——IP資料包,IP 資料包是真正在網絡間進行傳輸的資料基本單元。

通過邏輯尋址定位到前面應用層 DNS 解析出來的 IP 位址的主機網絡位置,然後把資料以 IP 資料包的格式發送到那去。

資料鍊路層負責将 IP 資料包封裝成适合在實體網絡上傳輸的幀格式并傳輸。設計資料鍊路層的主要目的就是在原始的、有差錯的實體傳輸線路的基礎上,采取差錯檢測、差錯控制與流量控制等方法,将有差錯的實體線路改進成邏輯上無差錯的資料鍊路,向網絡層提供高品質的服務。當采用複用技術時,一條實體鍊路上可以有多條資料鍊路。

上面這麼多層其實都是在為不同的目的對要傳輸的資料進行封裝處理,而實體層則是通過各種傳輸媒體(雙絞線,電磁波,光纖等)以信号的形式将上面各層封裝好的資料實體傳送過去。

至此一個 http 請求漂洋過海終于到達了伺服器,接下來就是從實體層到應用層向上傳遞,将封裝的資料一層層剝開,伺服器在應用層拿到最原始的請求資訊後快速處理完,然後就開始向用戶端發送響應資訊。這次是以伺服器為起點,用戶端為終點再走一遍五層協定棧。

伺服器的響應消息跋山涉水終于到達了浏覽器,接下來就是頁面渲染(更具體可參看浏覽器内部工作原理)。

頁面的渲染工作主要由浏覽器的渲染引擎來完成(這裡以Chrome為例)。

下面是渲染引擎在取得内容後的基本流程:

解析html建構dom樹 -> 解析css建構render樹 -> 布局render樹 -> 繪制render樹

渲染引擎首先開始解析html,并将标簽轉化為dom樹中的dom節點。接着,它解析外部css檔案及style标簽中的樣式資訊,這些樣式資訊以及html标簽中的可見性指令将被用來建構另一棵樹——render樹。render樹建構好了之後,将會執行布局過程,該過程将确定render樹每個節點在螢幕上的确切坐标。最後是繪制render樹,即周遊render樹的每個節點并将它們繪制到螢幕上。

偷了一張圖檔(Chrome和Safari所用核心webkit頁面渲染主流程):

從輸入cnblogs.com到部落格園首頁完全展示發生了什麼

webkit頁面渲染主流程

為了更好的使用者體驗,渲染引擎将會盡可能早地将内容繪制在螢幕上,而不會等到所有的html都解析完成後再去建構、布局和繪制render樹,它是解析完一部分内容就繪制一部分内容,同時可能還在通過網絡下載下傳其餘内容(圖檔,腳本,樣式表等)。比如說,浏覽器在代碼中發現一個 img 标簽引用了一張圖檔,于是就向伺服器發出圖檔請求,此時浏覽器不會等到圖檔下載下傳完,而是會繼續解析渲染後面的代碼,等到伺服器傳回圖檔檔案,由于圖檔占用了一定面積,影響了後面段落的布局,浏覽器就會回過頭來重新渲染這部分代碼。

render樹節點和dom樹節點相對應,但這種對應關系不是一對一的,不可見的dom元素不會被插入render樹,例如head元素、script元素等。另外,display屬性為none的元素也不會在渲染樹中出現(visibility屬性為hidden的元素将出現在渲染樹中,這是因為visibility屬性為hidden的元素雖然不可見但保留了元素的占位)。

又偷了一張圖:

從輸入cnblogs.com到部落格園首頁完全展示發生了什麼

render樹與dom樹

當渲染對象被建立并添加到render樹後,它們并沒有位置和大小,計算這些值的過程稱為layout(布局)。

布局的坐标系統相對于根渲染對象(它對應文檔的html标簽,可用

document.documentElement

拿到),使用top和left坐标。根渲染對象的位置是 (0,0),它的大小是viewport即浏覽器視窗的可見部分。布局是一個遞歸的過程,由根渲染對象開始,然後遞歸地通過一些或所有的層級節點,為每個需要幾何資訊的渲染對象進行計算。

為了不因為每個小變化都全部重新布局,浏覽器使用一個 dirty bit(頁面重寫标志位)系統,一個渲染對象發生了變化或是被添加了,就标記它及它的children為dirty——需要layout。

當layout在整棵渲染樹觸發時,稱為全局layout,這可能在下面這些情況下發生:

  • 一個全局的樣式改變影響所有的渲染對象,比如字号的改變。
  • 視窗resize。

layout也可以是增量的,這樣隻有标志為dirty的渲染對象會重新布局(也将導緻一些額外的布局)。增量layout會在渲染對象dirty時異步觸發,例如,當網絡接收到新的内容并添加到dom樹後,新的渲染對象會添加到render樹中。

繪制階段,周遊render樹并調用渲染對象的paint方法将它們的内容顯示在螢幕上。和布局一樣,繪制也可以是全局的(繪制完整的樹)或增量的。在增量的繪制過程中,一些渲染對象以不影響整棵樹的方式改變,改變的渲染對象使其在螢幕上的矩形區域失效(invalidate),這将導緻作業系統将其看作dirty區域,并産生一個paint事件,作業系統很巧妙的處理這個過程,并将多個區域合并為一個。

浏覽器總是試着以最小的動作響應一個變化,是以一個元素顔色的變化将隻導緻該元素的重繪,元素位置的變化将導緻元素的布局和重繪,添加一個dom節點,也會導緻這個元素的布局和重繪。一些主要的變化,比如增加html元素的字号,将會導緻緩存失效,進而引起整個render樹的布局和重繪。

等到繪制完畢,頁面就完全地展現在我們面前了。

看似再簡單不過的操作,背後支撐的技術鍊已經複雜到不可想象。上面隻是粗淺的輪廓,其中的每一步深挖進去都是一門大學問。不過咱們前端了解一下就行了,沒必要較這個勁,不然就舍本逐末了。

覺得不錯就點個推薦吧:)

繼續閱讀