長文,梳理優化措施的作用位置。
此文主要講的事情是如何讓使用者快點看到首屏頁面,其主要影響因素是延遲和解析渲染耗時。有關安全部分其實也是優化的一部分。我們著重說下網路部分。
大致過程:DNS 域名解析、建立 TCP 連線、下載資源、解析頁面。文章描述的優化會盡量限制在當時的分析的過程下。
1.DNS域名解析
一般來講,我們輸入的 url 是域名,而為了識別一個實體,TCP/IP 使用 IP 地址來唯一確定一臺主機到因特網的連線,DNS會幫助我們完成域名到IP地址對映的工作。以www.aaa.com
為例,解析過程大致如下:
過程
-
瀏覽器
- 瀏覽器查詢瀏覽器快取,沒有。
-
本機層
- 瀏覽器客戶端向系統詢問伺服器IP地址,呼叫本機內的DNS解析程式,檢查自己本地的hosts檔案是否有這個域名對映關係,沒有。
- 查詢本機的DNS解析器快取,沒有。
-
路由器快取
- 可能還存在路由器快取這一層
-
本地DNS伺服器
- 本機的DNS解析程式向本地的DNS伺服器發起請求,一般為TCP/IP引數中設定的首選DNS伺服器,是知道IP地址的,一般會UDP協議。
- 本地DNS伺服器查詢是否在本地區域檔案中,沒有。
- 本地DNS伺服器查詢DNS快取中是否存在,沒有。
- 本地DNS伺服器會根據是否設定轉發器判斷是向上一級DNS伺服器(其解析規則同理)還是直接向根DNS伺服器(知道根DNS伺服器的IP地址)傳送請求。
-
與DNS伺服器
- 收到請求後,根DNS伺服器並不直接解析地址,但是知道每個頂級域中的一臺伺服器的地址(如
com
域名伺服器)。如果為迭代查詢方式,此頂級域DNS伺服器的ip被返回給本地DNS伺服器。 - 本地DNS伺服器提取到頂級域DNS伺服器資訊後,會再向其發出請求。頂級域DNS伺服器收到請求後,會先查詢自己的快取,沒有,則將負責的二級域名伺服器(如
aaa.com
域名伺服器)返回給本地DNS伺服器,以此類推直到查到目標域名的對映資訊或查詢失敗。 - 查到對映資訊後返回到本機,中間各層會進行快取。
- 收到請求後,根DNS伺服器並不直接解析地址,但是知道每個頂級域中的一臺伺服器的地址(如
-
查詢方式:
- 遞迴方式:一路查下去中間不返回,得到最終結果才返回資訊。
- 迭代方式:就是上面的本地DNS伺服器與其他域名伺服器直接的查詢方式,查到一個可能知道的伺服器地址,將此地址返回,重新傳送解析請求。
- 一般預設的方式從本機到本地DNS伺服器是遞迴,DNS伺服器之間是迭代查詢。
優化
當然針對DNS的優化就是減少DNS解析的時間,由於瀏覽器快取機制的存在,我們只需要對首次訪問進行優化(雖然我們現在只是請求了一個html檔案,但是html檔案裡還會有我們後續要請求的css/js/img等),即適當減少要解析的域名個數,考慮到其他優化機制可以將頁面及頁面內資源釋出到2-4個域名上。
2.建立連線
TCP連線
好了,瀏覽器終於拿到伺服器IP了,客戶端想要與伺服器間通訊並傳遞訊息需要開啟TCP(一種傳輸層協議)連線。
過程
- 客戶端建立socket,向伺服器目標埠傳送連線建立請求,資料段包含位碼SYN(建立聯機標誌位) = 1,隨機數seq(順序號碼)= x,和其他TCP標誌和選項。
- 伺服器有一個專門處理連線請求的welcome socket,接收到連線建立請求,置位碼SYN和ACK(確認標誌位)為1,ack(確認號碼)= x + 1,隨機數seq = y,並返回。
- 客戶端檢查ack是否等於x + 1,等於時,將ACK置為1,SYN置為0,將ack置為y + 1傳送至伺服器端。
- welcome socket檢查ack等於y + 1和ACK等於1後,建立新的socket,此socket由源IP/源埠、目標IP/目標埠標識,之後客戶端傳送的資料都被引導向此新的socket,至此,TCP連線建立。
簡單來講:
// client:
send({SYN: 1, seq: x, ...others})
|
↓
//server:
send({SYN: 1, ACK: 1, ack: x + 1, seq: y, ...others})
|
↓
//client:
ack === x + 1 ? send({ACK: 1, SYN: 0, ack: y + 1, ...others}) : 'hehe'
|
↓
//server:
ack === y + 1 && ACK === 1 ? new Socket() : ''
複製程式碼
SSL/TLS
如果啟用了HTTPS進行加密,在使用TLS前還需要協商建立加密通道。
過程
- 客戶端:TCP連線建立之後,再以純文字形式傳送一些規格說明,隨機數
Random1
,TLS協議版本,支援的加密套件列表,支援或希望使用的其他TLS選項。 - 伺服器:
- 取得TLS協議版本以備將來通訊使用,從客戶端提供的加密套件列表中選擇一個,生成隨機數
Random2
傳送給客戶端; - 附上自己的證照,將響應傳送給客戶端;
- 同時,也可傳送一個請求,要求客戶端提供證照以及其他TLS擴充套件引數。
- 取得TLS協議版本以備將來通訊使用,從客戶端提供的加密套件列表中選擇一個,生成隨機數
- 客戶端:
- 同上,可能會向伺服器傳送自己的證照。
- 客戶端收到伺服器的證照後,通過證照鏈關係從根CA(證照的簽發機構)驗證證照的合法性,驗證通過後取出證照中的伺服器公鑰,生成隨機數Random3,再用伺服器公鑰加密
Random3
(pre master key),傳送給伺服器; - 告訴伺服器可以開始加密透明信了;
- 客戶端用
三個隨機數
和約定的加密方法
生成對話金鑰
。將前面的握手資訊生成完成摘要,使用對話金鑰
加密,傳送告訴伺服器我已完成握手。
- 除了公鑰加密隨機數和對話金鑰加密的摘要外,其他所有的資料都是明文形式傳送。
- 伺服器:
- 用私鑰解密出客戶端發來的隨機數,通過驗證訊息的MAC檢測訊息完整性,用相同的方式生成
對話金鑰
。 - 解密客戶端傳送的完成報文,驗證
對話金鑰
是否正確。
- 告訴客戶端,要開始加密了;
- 同樣再返回給客戶端一個加密的完成訊息。
- 用私鑰解密出客戶端發來的隨機數,通過驗證訊息的MAC檢測訊息完整性,用相同的方式生成
- 客戶端用它之前生成的
對話金鑰
解密這條訊息,確定對話金鑰是否正確
,正確則建立通道並且開始傳送應用資料。
其中:
對話金鑰
又可稱為協商金鑰
。對話金鑰
是對稱金鑰,對稱加解密速度很快。- 伺服器公鑰和私鑰是非對稱金鑰,非對稱加解密速度很慢。
- 使用非對稱加密生成可靠的對稱金鑰,使用對稱金鑰進行後續資料的加密。
- 上述帶序號報文可能一次傳送,也可能分次連續傳送。
- SSL和TLS可以當作一個東西。
- 伺服器也可以不使用CA頒發的證照,而使用自己的證照。
優化
我們要對TCP和SSL/TLS握手耗時進行優化。有以下幾個因素:
- 資料往返延遲:主要受地理位置影響,使用較近的伺服器進行資料傳輸會減少資料往返的時間,我們可以通過在不同的地區部署伺服器(如:CDN,其也會用到DNS解析,可能在DNS解析階段就完成了對客戶端訪問域名到距離最近的伺服器的對映),將資料放到接近客戶端的地方,可以減少網路往返時間。
- 證照鏈:其實資料往返延遲優化不只是針對TCP握手階段的,後續基於TCP的資料傳輸都會收益,如SSL/TLS握手和後續的請求響應。那麼證照鏈是影響SSL/TLS握手的一個重要因素,證照鏈是伺服器向客戶端傳送的證照內的資訊,由站點證照、中間證照頒發機構的證照、
根證照組成(比較類似DNS域名解析伺服器之間的關係)。- 原因:
- TCP慢啟動:由於TCP慢啟動(為避免擁塞,TCP連線初始只能傳送較少的分組,然後等待客戶端確認,然後翻倍,經過幾次往返直至到達閾值)和TLS/SSL握手資料傳送一般位於TCP連線慢啟動階段的關係,證照資料過多會超過TCP連線的初始值,會造成資料往返次數的成倍增加。
- 證照鏈驗證過長:由於客戶端瀏覽器在驗證證照可靠性時,會遞迴驗證鏈條中的每個節點至根證照,也會增加握手時間。
- 方法:
- 減少中間證照頒發機構的數量,優化至只有站點證照和一箇中間證照頒發機構。
- 不要新增根證照資訊,瀏覽器內建信任名單中有根證照。
- 原因:
- 握手次數:前兩點優化都是針對的握手時間的優化,握手次數也是影響延遲的重要因素。我們在後面談到大量請求的時候再說這一點。
- 初始擁塞視窗:適當增大初始擁塞視窗大小,即增大TCP連線初始可傳送的分組大小。
3.獲得頁面響應
重定向響應
如果伺服器返回了跳轉重定向(非快取重定向),那麼瀏覽器端就會向新的URL地址重新走一遍DNS解析和建立連線。 所以應該避免不必要的重定向。
頁面資源響應
在獲得了html響應之後,瀏覽器開始解析頁面,進入準備渲染的階段。下載優化同樣放在後面談到大量請求的時候再說這一點。
4.解析渲染頁面
我們需要將這個過程先分為兩個部分來看,頁面資源載入和渲染。
頁面資源載入
瀏覽器在解析頁面的過程中會去請求頁面中諸如js、css、img等外聯資源。
4.1 建立連線
同樣這些資源的載入也是需要建立TCP連線的,直接使用也需要進行DNS解析和握手。
優化
此處的請求次數與頻率相對於第一次請求頁面資源時要高很多,所以這裡著重闡述下成批量的請求的優化。
瀏覽器目前使用的HTTP協議版本大多是1.1和2,二者有些不同,但是底層都是使用TCP進行資料傳輸。由於TCP握手耗時,和SSL/TLS更加耗時,我們需要減少整個載入過程中需要建立的連線的次數和耗時。
- 複用:針對HTTP1.1的最好方法是啟用長連線:HTTP 1.1提供了預設開啟長連線功能,相對於短連線(每請求一個資源建立然後斷開一次TCP連線),同一客戶端socket(瀏覽器可能會開多個埠並行請求)針對同一socket(域名+埠)後續請求都會複用一個TCP連線進行傳輸,直到關閉這個TCP連線。
- 加速:針對SSL/TLS握手有會話恢復機制,驗證通過後,可以直接使用之前的對話金鑰,減少握手往返。
4.2 載入之前
在伺服器返回響應時,又存在幾種情況,如:伺服器負載大,伺服器當機,無法及時或較快響應請求,伺服器地理位置過遠或跨運營商導致延遲很高。
優化
這裡跟建立連線部分的優化其實是公用的,但是單純的正常建立連線消耗資源較少,所以我們在這個再較完成的闡述一下。
- 增加頻寬:但是大部分情況下伺服器頻寬並不是影響延遲的主要因素。
- 智慧DNS解析:根據客戶端的IP地址,將域名解析為最近的或不跨運營商的伺服器的IP地址,解決地理位置和跨運營商的延遲問題。
- CDN:使用某種分析方式根據節點伺服器的地理位置、負載情況、資源匹配情況從遍佈各地的節點伺服器中找出最合適的靜態資源伺服器。
- 負載均衡:使用DNS負載均衡、IP負載均衡、反向代理負載均衡等方式從一堆伺服器(叢集相同職責)或一組伺服器(分散式職責區分)中選擇最合適的伺服器處理請求。
- 這幾種技術可能是相互結合的,比如CDN會用到DNS智慧解析和負載均衡等。
- 其中使用了跳轉重定向方式的會重新進行DNS解析和握手,其中一部分優化實際是在域名的DNS解析部分完成的。
4.3 開始載入
好了,伺服器終於可以愉快的返回資料了。
HTTP 1.1
過程
- 雖然HTTP 1.1有長連線,一個TCP連線可以用來請求多個資源,但是這些資源的下載是序列的,比如使用這個TCP通道請求1.css、2.css、1.js,只有在前者傳輸成功完整完成後才會進行下一個的傳輸。
- 雖然瀏覽器一般會請求建立多個TCP連線(多個埠向伺服器一個埠請求資源,新的TCP連線的建立會進行新的握手),去並行的請求頁面資源加快整體的下載速度,然而對每個域名的並行連線是有數量限制的(保護伺服器負載,並受主機埠限制),所以我們還是要進行一些優化。
優化
- 減少
- 減少頁面中需要發起的請求總數,如我們常規使用的程式碼合併,雪碧圖(精靈圖/Sprite合併小圖示),將圖片轉為base64寫入其他檔案,避免空的img src屬性等。
- 切割拆分資料,讓首屏資料優先載入等。
- 增加:增加資源的分佈域名,部署在不同域名下,“突破”瀏覽器並行連線限制(結合DNS部分,不易過於分散,且過多連線會共享頻寬,且移動端的解析更加緩慢)。
HTTP 2
由於HTTP 2提供了多路複用的功能,基於二進位制資料幀和流的傳輸,使通過一個TCP連線進行資料分散、亂序、並行傳輸成為現實,即我們所有的資源都可以通過一個TCP連線不阻塞的並行傳輸。
所以我們針對HTTP 1.1的減少請求數量所做的合併優化、增加資源分佈域名都成為了無效優化,可以丟掉。同時由於檔案不用合併,進行檔案更新時我們也不用再修改單個開發模組更新所有(合併檔案)模組了。
4.4 載入中
總的來說是很簡單的過程,客戶端接收伺服器傳輸返回的響應。
優化
傳輸的資料大小越小,那麼傳輸就越快,延遲就越小。
-
更小
- Gzip:啟用Gzip可對響應體進行壓縮,可減少70%大小的資料體積。
- 減少cookie:去除不必要的cookie,設定合適過期時間。
- 捨棄cookie:對於靜態檔案請求我們可以不要cookie,即HTTP1.1中提到的,分佈在其他域名下,子域名設定合理的domian(cookie作用域)。
- 首部壓縮:HTTP2還提供了首部壓縮功能,即通過雙方共有的一些字典,將首部資訊(狀態行、請求/響應頭)“對映”為更簡短的資料。
- 圖片:使用合適的圖片大小和圖片格式,可以節省大小。
-
快取:小到最小的情況當然是不接受資料傳輸,使用本地快取。一般使用伺服器前一次返回的響應頭部欄位進行控制。
- 強快取:強快取不會向伺服器傳送請求。
- Expires:http1.0欄位,使用伺服器時間做標識。
- Cache-Control:max-age=
seconds
,使用相對於請求的時間,不超過這個時長,直接使用快取。還有其他的值。
- 協商快取:
- Last-Modified/If-Modified-Since:資源最後修改時間,秒。瀏覽器客戶端傳送If-Modified-Since欄位,伺服器響應Last-Modified欄位。
- ETag/If-None-Match:資源的識別符號,客戶端傳送If-None-Match欄位,伺服器響應ETag欄位,並比較兩者,決定返回快取重定向還是其他,這個標識只比較內容,不關心資源時間。
- 強快取:強快取不會向伺服器傳送請求。
-
合理拆分頁面資源,比如外聯js和css就可以獨立於html進行快取。
4.5 關閉TCP
在資源下載完畢之後,需要關閉TCP連線。這段沒有什麼可以優化的。
過程:
- TCP客戶端傳送一個FIN = 1(結束標誌位)和隨機數seq = a,用來關閉客戶到伺服器的資料傳送。
- 伺服器收到這個關閉請求,返回ACK = 1 ,ack = a + 1,此時伺服器之前的資料可能還沒有傳輸完成。
- 資料傳輸完成後,伺服器傳送一個FIN = 1和隨機數seq = b給客戶端。
- 客戶端返回ACK = 1,ack = b + 1,並等待一段時間,確保伺服器沒有返回沒收到確認報文的重傳申請,後關閉連線。
- 伺服器收到確認報文後,驗證關閉連線。
總結
HTTP2 真好用。合理使用快取。
頁面解析渲染
上述的資源載入是發生在頁面解析過程中的。那麼瀏覽器的頁面解析渲染是怎麼樣的一個過程呢?
過程
簡要來講就是:
- 處理HTML標記,構建DOM樹。
- 處理CSS標記,構建CSSOM樹。
- 將DOM樹和CSSOM樹融合成渲染樹(會忽略不需要渲染的dom)。
- 根據渲染樹來佈局,計算每個節點的幾何資訊。
- 在螢幕上繪製各個節點。
- 中間遇到各種資源時,會進行資源的下載。
問題
- 資源下載
- css下載時會阻塞渲染(帶有media屬性除外)。
- 遇到 script 標籤時,DOM構建停止,此時控制權移交至js,直到指令碼(下載)執行完畢,此時瀏覽器有優化一般會下載其他資源,但是不會解析。如果js中有對CSSOM的操作,還會先確保CSSOM已經被下載並構建。
- 圖片資源下載不會產生阻塞。
- 重繪重排導致重新進行渲染樹的生成
- 重排(迴流):會重新計算佈局,通常由元素的結構、增刪、位置、尺寸變化引起,如:img下載成功後,替換填充頁面img元素,引起尺寸變化;也會由js的屬性值讀取引起,如讀取offset、scroll、cilent、getComputedStyle等資訊。
- 重繪:簡單外觀的改變會引起重繪,如顏色變化等。
- 重排一定重繪。
優化
- dom
- 簡化dom結構,減少DOM樹和渲染樹構建成本,減少頁面元素個數,如使用列表表格資料分頁,簡單表格不要使用複雜第三方元件等方式。
- js
- 將js指令碼標籤放在頁面body底部,減少對其他過程的阻塞。
- 延遲執行:對不修改頁面的外鏈script使用defer屬性,使指令碼並行下載不阻塞,下載後不立刻執行,而在所有元素解析之後執行。
- 減少和合並不必要的dom相關操作,如使用DocumentFragment、修改classname而不是各項style等,減少對重繪和重排的觸發。
- css
- 將css放入head中,提前載入,並防止html渲染後重新結合css引起頁面閃爍。
- 減少css層級和css選擇器複雜度,提高解析速度,雖然瀏覽器有優化。
- 使用更高效能的css樣式,如flex代替float。
- 開啟複合層,如使用3d變換、opacity等,使該元素及其子元素不導致外部的重排,但是也有坑。
- 合理使用脫離文件流的樣式,減少對外部重排的影響,如absolute。
- 檔案數量
- 減少首次下載的檔案數量大小,使用圖片懶載入,js的按需載入等方式,也可以節省使用者流量,甚至使用storage儲存進行js、css檔案的快取。
- 拆分頁面資源,首屏資料優先載入等。
5.其他優化措施
我們還可以採取一些和延遲、渲染無關的優化措施:
- 使用PWA,讓使用者在沒有得到資料時也能看到頁面。
- 對頁面某些ajax請求資料進行storage儲存。
- 載入進度、骨架圖、佔點陣圖等類似讓使用者感覺好一點的措施。
- 及時更新升級伺服器,優化措施依賴於伺服器支援。