在瀏覽器輸入網址並送出後,到底發生了什麼事?

這道題目涵蓋前端技術範圍很廣,很適合在各階段拿來梳理並檢視自己的技能和狀態。那麼,就讓我來檢視一下自己的狀態吧 d(`・∀・)b

當使用者在瀏覽器網址列輸入網址並按下 Enter 後會發生什麼事情呢?分為以下重點說明…

一、瀏覽器的內部運作機制

瀏覽器架構

瀏覽器(在這裡是指 Chrome)是多程序(multi-process)的架構,意即當開啟一個應用程式時,會由一個以上的 process 聯合運作來完成任務,並且 process 又可將工作切分給其下的執行緒(thread)來協助運作,其中 process 與 process 間是利用 IPC(inter-process communication)的方式來溝通。

瀏覽器內的 process 有哪些呢?

不同的 process 可切分為不同的 thread 以負責不同的功能…

因此,當使用者在瀏覽器網址列輸入網址並送出後,browser process 的 ui thread 接收到使用者輸入的資料,ui thread 會要求發出網路請求,此時 network thread 開始執行 DNS lookup、建立連線,而當連線成功、資料傳輸完畢後,network thread 通知 ui thread 資料已準備好,browser process 利用 IPC 將資料傳遞給 render process 以進行頁面渲染,渲染完畢即可讓使用者看到畫面。

更多關於瀏覽器內部運作機制的資訊,可參考這裡

備註

解析 URL

所謂的「網址」即是「統一資源定位符」(Uniform Resource Locator,URL),可唯一標註資源的所在位置,其格式為 協定類型:[//伺服器位址[:埠號]][/資源層級UNIX檔案路徑]檔案名[?查詢][#片段ID],例如:https://www.sample.com/main/page/index.html?keyword=abc&happy=abc#heading-1。也就是說,當使用者在瀏覽器網址列鍵入網址,瀏覽器的 ui thread 接收到輸入的資料,network thread 便開始將這網址字串進行解析以便下載資源,而這個網址字串會解析成以下部份…

減少網路請求的數目

關於以上過程,可做的優化有

先來看如何減少網路請求的數目。

若無 HTTP/2,則由於網路資源請求耗時耗力、每次對於每個網域所能請求的連線數有限,因此必須減少網路請求的數目,例如:使用 webpack 等工具有系統的打包、瀏覽器快取的應用、圖片資源的整合(sprites、base64、各種 SVG 的應用)等。

關於這部份可參考以下筆記

二、網路連線

URL 解析完畢後,若伺服器位址的地方是放置域名而非 IP address 就需要做 DNS 解析以得到 IP,接著再建立 TCP/IP 連線來傳輸檔案。備註,由前面所述可知,網路連線是 single thread 的,一個請求即使用一個 network thread。

DNS 解析

DNS(domain name server)用於提供域名和 IP address 互相轉換的資訊。由於 DNS lookup 耗時耗力,因此若有快取就用快取,若無快取則至少要做 DNS Prefetch 提早處理。

關於 DNS 解析過程中可做的優化是

建立 TCP/IP

TCP/IP(Transmission Control Protocol/Internet Protocol)是網路上的一種通訊協定,用於在不同設備或環境間傳送訊息。另,HTTPS 是指將 HTTP 使用 SSL 加密後再利用 TCP 發送,加強安全性。TCP/IP 會經由三次握手建立連線,再經由四次揮手斷開連線。

由於瀏覽器對於同一 domain 的 TCP/IP 連線數是有限制的,且每個檔案請求皆需一個 TCP/IP 連線,因此針對這個議題有許多優化方案,主要目的都是為了減少請求數,點此回顧減少網路請求的數目的方法。

GET vs POST

HTTP 請求方法有 GET、POST、PUT、DELETE 等,以下主要探討最常用的 Get 與 Post。

TCP/IP 五層網路架構

當瀏覽器發送網路請求時,會從應用層經層層通訊協定到實體層傳送出去,而當對方收到後,再反向經由實體層的層層通訊協定到應用層解讀訊息。

這五層依序是…

OSI 七層網路架構

另外一種架構方式是使用 OSI 七層網路架構,與 TCP/IP 五層網路架構可互相對應。

推薦閱讀-什麼是 OSI 的 7 層架構?和常聽到的 Layer 7 有關?

三、伺服器處理請求並返回 HTTP 回應

客戶端發送請求後,伺服器收到請求會進行處理並回應,而 HTTP 請求或回應訊息的結構是:方法、通用標頭、請求或回應標頭、請求或回應身體。

通用標頭

格式如下

範例如下,這是我最近常買東西的網站-蝦皮拍賣。

通用標頭

Request URL: https://shopee.tw/
Request Method: GET
Status Code: 200
Remote Address: 103.117.4.201:443
Referrer Policy: no-referrer-when-downgrade

狀態碼

列舉常見的狀態碼如下。

請求與回應標頭

請求標頭(request headers)

:authority:method:path:scheme 是 HTTP/2 的偽請求標頭(pseudo-header field),並不屬於 HTTP 正規的欄位,因此必須出現在正規欄位之前。

範例如下。

請求標頭

:authority: shopee.tw
:method: GET
:path: /
:scheme: https
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-TW,zh;q=0.9,en-US;q=0.8,en;q=0.7
cache-control: max-age=0
cookie: _gcl_au=1.1.802438797.1542279133; csrftoken=PRD6fx4beV7XOXgma6JOyHYwEi0AtHi7; SPC_F=3wuKDJImIN5pTDXzt7VKSLa3hQCPOv6M; REC_T_ID=83fc55f6-e8c4-11e8-91a5-f8f21e1a4b50; SPC_SI=c9fuhx7aqz5hr2l5ztuu2izcqd4kz9v3; ...
upgrade-insecure-requests: 1
user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36

回應標頭(response headers)

範例如下。

回應標頭

cache-control: no-cache
cache-control: no-cache, no-store
content-encoding: gzip
content-type: text/html
date: Tue, 20 Nov 2018 04:53:31 GMT
etag: W/"5bed4437-d09"
expires: Tue, 20 Nov 2018 04:53:30 GMT
last-modified: Thu, 15 Nov 2018 10:02:31 GMT
server: SGW
status: 200
vary: Accept-Encoding

請求標頭與回應標頭是一一對應的,例如…

請求與回應身體

這裡是指實體的訊息,目前多為 json 格式,內容為 html 字串,瀏覽器接收到就會解析並渲染。

範例如下。

請求與回應身體

CRLF

斷行符號(CRLF,carriage-return line-feed),在請求標頭和實體訊息之間有一個 CRLF 作為分隔,而回應標頭和回應實體之間也有一個 CRLF 作為分隔。

不同的平台下,斷行符號可能是不同的…

在使用 Git 等版本控管工具時很常發生這個問題-Git 在 Windows 平台處理斷行字元 (CRLF) 的注意事項

cookie 是本地端的一種儲存方式,用於搭配客戶端的 cookie 與伺服器端的 session 以進行資料通訊功能。關於 cookie、session、localStorage、sessionStorage 的說明與比較可參考這裡

在同域名的資源請求時,瀏覽器會自動帶入本地端的 cookie,而這些 cookie 可能非常多而減低下載速度,因此需要做優化-域名拆分,例如:sample.com 下的資源可拆分為 a.sample.com 與 b.sample.com,這樣當瀏覽 a.sample.com 下的頁面時,就不會帶入 b.sample.com 的 cookie。但注意域名解析需要時間,過多域名可能會降低下載速度,建議可用 DNS 解析的優化方案來解決。

Gzip 壓縮

由伺服器開啟壓縮方式,除了 Gzip 壓縮外,還有 Brotli 等,在壓縮文字資源後可大量減少檔案體積,加快傳輸速度。

點此看 Gzip 筆記。

短連接與長連接

短連接是指 socket 建立連線並發送完資料、接收完畢後,即馬上中斷連線;而長連線則是 socket 建立連線後即保持連接。

在 HTTP 1.0 中所有的檔案請求預設都是個別的短連接,而在 HTTP 1.1 後預設為長連接,即 Connection: keep-alive,因此建立連線後不會中斷,但這有一個維持的時間區段,超過時間區段後還是會中斷連線。

HTTP/2

HTTP/2 是 HTTP 的下一代規範,主要差異是

並且由於請求與回應標頭的壓縮、二進制封包結構、伺服器端推送和請求優先順序的排定,因此 HTTP/2 能提供更好的效能,可參考這裡

HTTPS

HTTPS 即是更安全的 HTTP,白話說就是建立 SSL 連線,以確保之後的通訊都是加密的,無法被輕易擷取分析。由於 HTTPS 比起 HTTP 需要額外建立安全連線和加密而需要傳輸更多資料(速度慢),因此搭配 HTTP/2 會讓速度加快。

四、HTTP 快取

快取規則

快取規則

範例如下,檔案可供所有設備快取,過期時間是兩年後。

快取規則 範例

如何決定要使用快取還是取得更新資源?

流程如下圖。

如何決定要使用快取還是取得新檔案?

如何強迫瀏覽器更新資源?

方法一

變更資源的網址,例如加入 hash。相同網址之下,會讀取本地快取,所以改變網址即可,可解決資源未過期但想要更新的狀況。

以下三個網址對瀏覽器來說都是不一樣的,不會讀取本地快取,會重新要求資源。

https://www.sample.com.tw/assets/cute.png

https://www.sample.com.tw/assets/cute.png?123

https://www.sample.com.tw/assets/cute.v123.png?123

方法二

使用 ctrl + F5ctrl 並點擊瀏覽器的更新按鈕來強制更新,重新向伺服器請求所有資源,而不使用快取,因此可取得最新版本的內容。

Checklist

範例

資源未被快取。

200

資源來源是記憶體快取。

image from memory cache

資源來源是 HTTP 快取(HTTP Cache)又稱為磁碟快取(Disk Cache)。

image from disk cache

資源來源是記憶體快取,但使用 Hash 控制是否要強迫瀏覽器更新資源。

image from memory cache with hash

資源經 ETag 確認未更新,因此使用快取檔案。

304 not modified

備註

五、瀏覽器解析與渲染頁面

客戶端(意即瀏覽器的 renderer process)收到從伺服器的回應後後會開始做解析和渲染,這部份的流程為

rendering flow

這部份的優化包含

備註

阻礙渲染的原因和解法

阻礙渲染的原因有

解法為

async, defer

圖片來源:Loading Third-Party JavaScript

可參考-瀏覽器渲染效能Renderer Process 做了什麼事情?

Web Font

如前所述,在 render tree 指定所需字體後會來請求字型檔,如果請求的字型檔尚未下載完畢,不同瀏覽器有不同的解法,例如:Chrome 和 Firefox 會等待三秒鐘(在這段時間會沒有文字,可能會是白屏喔),若超過三秒鐘字體檔仍未下載完畢就會啟用備用字體,待下載完畢再重新渲染字型;IE 則是無等待時間,馬上啟用啟用備用字體,待下載完畢再重新渲染字型;Safari 則是無限制時間的等待下載(30 秒以上),可參考這裡

關於 web font 的優化方式有…

JavaScript

除了前面提到的 JavaScript 檔案下載或執行時可能會阻礙建構 DOM tree 而可利用 async 或 defer 來解決之外(可參考這裡),繼續討論更多關於 JavaScript 的議題。

網頁的生命週期

以「事件」來區分網頁的生命週期的話,大致可區分為幾個部份-loaded、DOMContentLoaded、beforeunload、unload。

渲染流程與網頁的生命週期

這些事件的用途是…

推薦閱讀-重新認識 JavaScript 番外篇 (6) - 網頁的生命週期

備註

Service Worker

service worker(在此簡稱 sw)可將資料快取在本地,並主動掌握檔案更新的時機。browser process 的 ui thread 在發出網路請求並請 network process 處理後續工作時,network thread 會先到已註冊的 sw scope 確認是否有想要的檔案,若有就將這個檔案交由 renderer process 來處理。另外,「navigation preload」是一種讓 sw 平行下載其所需資料的加速機制,在 sw 被啟動的同時,network thread 會發出網路請求來下載 sw 所要求的資源,而非待 sw 要求後才下載檔案。

編譯與執行

在 JavaScript 引擎編譯和解析 JavaScript 程式碼時會經過三個步驟-(1) 語法基本單元化與語彙分析、(2) 剖析(或稱語法分析)和 (3) 產生目的程式碼,最後執行編譯後的指令…因而帶出以下觀念…

編譯階段
執行階段

JavaScript 全系列筆記可參考這裡

六、其它

跨域

什麼是跨域?

「同源政策」(Same Origin Policy)讓瀏覽器不能對不同網域的資源發出網路請求,目的是避免惡意網站竊取資料,其中,同源政策又分為兩種「DOM 的同源政策」與「cookie 的同源政策」。

對於 DOM 的同源政策來說,只要協定類型、伺服器位址與埠號相同即是同源,而且並非所有跨網域的行為都會被禁止,例如:連結、重新導向或表單送出、跨來源嵌入資源等是允許的,但透過 JavaScript 的跨來源寫入是不行的,例如:XMLHttpRequest 或 Fetch API 的跨來源寫入或讀取。也就是說,同源政策允許 HTML 標籤的跨來源寫入、嵌入、讀取,但對於 JavaScript 產生的跨來源寫入、嵌入、讀取是有限制的。也因為對於 HTML 標籤的跨來源存取是寬鬆的,因此可能會有 CSRF 問題。

對於 cookie 的同源政策來說,卻可透過設定允許子網域的程式碼經由 JavaScript 修改母網域的 cookie,例如:伺服器端設定 Set-Cookie: key=value; domain=.sample.com; path=/ 或客戶端設定 document.domain = 'sample.com';,這樣不論是 a.sample.com 或 sample.com 可都取用和修改。

跨域的解法?

JSONP

JSONP 的解法主要是利用 <script> 具有跨域能力,透過在客戶端網頁加入 <script> 標籤,並且伺服器端在用函式包裹資料後回傳。注意,這只能是 GET 請求而不能是 POST,如果是 POST 就要用 CORS。

範例如下。

客戶端。

function appendScriptTag(src) {
  var script = document.createElement('script');
  script.setAttribute('type', 'text/javascript');
  script.src = src;
  document.body.appendChild(script);
}

window.onload = function () {
  appendScriptTag('http://sample.com?cb=foo');
};

function foo(data) {
  console.log('response data: ' + JSON.stringify(data));
}

伺服器端。

foo({ testData: 'Hello World' });
CORS

跨域資源共享(Cross-origin Resource Sharing,CORS)允許瀏覽器跨網域向伺服器發出請求,這主要是在後端對 header 進行設定 Access-Control-Allow-OriginAccess-Control-Allow-HeadersAccess-Control-Allow-Methods

優化

預請求是指在發出真正的請求前,會將一些資訊丟到目標伺服器詢問是否合法,意即使用請求方法 OPTIONS 表示這個請求是用來詢問的,在標頭中放入 Origin 表示來自哪裡的請求,還有 Access-Control-Request-Method 和 Access-Control-Request-Headers,若目標伺服器確認允許就會回傳 200 OK,這時候客戶端會再發出真正的請求,才會包含資料。因此,在標頭設定 Access-Control-Max-Age 可暫存此次預請求回應的快取秒數,在時間範圍內的預請求都會使用這個回應而非再次提出預請求的要求。

iframe postMessage / message

範例如下,父窗口 http://parent.com 向子窗口 http://child.com 使用 postMessage 發送訊息。

var popup = window.open('http://child.com', 'title');
popup.postMessage('Hello World!', 'http://child.com');

子窗口向父窗口使用 postMessage 發送訊息。

window.opener.postMessage('Hello World, too!', 'http://parent.com');

父窗口或子窗口都透過 message 可監聽對方的訊息。

window.addEventListener(
  'message',
  function (e) {
    console.log(e.source); // 發送訊息的窗口
    console.log(e.origin); // 訊息發送的目標網址
    console.log(e.data); // 訊息內容
  },
  false,
);
WebSocket

WebSocket 透過 origin 表示源自哪個網域,由接受到的伺服器判斷是否合法。

viewport

什麼是 viewport ?

viewport 是指網頁的可視區域,即視窗的寬度。而 meta viewport 的設定可讓畫面在行動裝置上能正常顯示,或響應式網頁可根據 viewport 與 DPR 選擇適合的圖檔。

像素與螢幕像素

像素(px)是指螢幕上所顯示的最小單位,可看作是一個點,但這個點會依據 DPR 的不同而可大可小;而螢幕像素密度(Device Pixel Ratio,DPR)是指每一英吋裡到底含有多少像素。一般來說,安卓的 DPR 多為 1,而蘋果的手機是 2,平板電腦是 3,因此在安卓上看起來很正常的圖片,往往在 iPhone 等蘋果產品上都會糊糊的( <picture>/<img srcset sizes> 詳解與範例)。

<meta name=”viewport”> 的設定與說明

回顧

梳理知識體系可內省不足而能更深入了解…

回顧本文的重點

參考資料


效能調校 關鍵轉譯路徑 圖片最佳化 轉譯效能 你懂JavaScript嗎? 你所不知道的JS 加載效能 快取 Rendering Performance Critical Rendering Path Resource Hints Web Workers Worker requestAnimationFrame Gzip HTTP Caching Loading Performance You-Dont-Know-JS cache javascript javascript prototype undefined 編碼 解碼 encode decode base-64 前端效能 系列文