[每日一題]一道面試題是如何引發深層次的靈魂拷問?

saucxs發表於2020-12-24

關注公眾號「鬆寶寫程式碼」,精選好文,每日面試題

加入我們一起學習,day day up

作者:saucxs | songEagle

來源:原創

一、前言

有這麼一道面試題,如下:

面試題:請詳細介紹一下從輸入 URL 到頁面載入完成的過程 ?
這道題的覆蓋面可以非常廣,很適合作為一道承載知識體系的題目。

每一個前端人員,如果要往更高階發展,必然會將自己的知識體系梳理一遍,沒有牢固的知識體系,無法往更高處走!

你不信這道題承載的知識體系龐大?往下看

二、分析題幹

在對於這道題上,如果對於面試官想要知道的是:簡單敘述還是深入敘述 。

所以需要回答到關鍵詞上,不然多而雜,效果不好,抓不住重點。

接下來我們從主幹流程和深入的詳細敘述分別介紹,我覺得這道面試題可能需要 15 分鐘才能講完。

主幹流程回答:是基本功體現,知識歸納能力,面面俱到,點到為止 。

詳細闡述:考察的各個知識點的掌握能力以及掌握到什麼程度 。

三、主幹流程

在將瀏覽器渲染原理、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 網路請求執行緒

image

可以看出來 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——伺服器端錯誤,伺服器未能實現合法的請求

image

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 報文結構的簡要分析:

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 的互動,可以看下圖總結

iamge

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/command + 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 優先順序高。

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

image

九、解析頁面流程

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

1、流程簡述

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

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

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

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

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

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

如下圖:

image

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>

瀏覽器的處理如下:

image

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

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

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

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

4. DOM 構建:因為HTML標記定義的就是不同標籤之間的關係,這個關係就像是一個樹形結構一樣

例如:body 物件的父節點就是 HTML 物件,然後段略 p 物件的父節點就是 body 物件
最後的 DOM 樹:

image

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 樹就是

image

4、構建渲染樹

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

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

image

5、渲染

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

image

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

(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!'));

7、簡單層和複合層

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

簡單介紹下:

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

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

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

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

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

8、Chrome 的除錯

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

image

image

9、資源外鏈的下載

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

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

  1. 遇到外鏈的處理
  2. css 樣式資源
  3. js 指令碼資源
  4. img 圖片類資源

以下針對每種情況進行詳細說明:

  1. 遇到外鏈的處理

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

  1. 遇到 css 樣式資源

css 資源處理特點:

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

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

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

  1. 遇到 js 指令碼資源

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

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

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

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

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

簡單的說:

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

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

  1. 遇到 img 圖片類資源

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

10、loaded 和 domcontentloaded

對比:

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

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

十、Reference

1、https://github.com/saucxs/full_stack_knowledge_list

2、https://blog.csdn.net/sinat_21455985/article/details/53508115

3、https://book.douban.com/subject/26960678/

4、https://github.com/kaola-fed/blog/issues/271

其他

鬆寶寫程式碼

songEagle開發知識體系構建,技術分享,專案實戰,實驗室,帶你一起學習新技術,總結學習過程,讓你進階到高階資深工程師,學習專案管理,思考職業發展,生活感悟,充實中成長起來。問題或建議,請公眾號留言。

關注福利

1、內推福利

回覆「校招」獲取內推碼

回覆「社招」獲取內推

回覆「實習生」獲取內推

後續會有更多福利

2、學習資料福利

回覆「演算法」獲取演算法學習資料

3、每日一題

正在更新中

相關文章