Skip to content

瀏覽器從網址解析、建立安全連線到完成頁面渲染的流程示意圖

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

「在瀏覽器輸入網址並送出後,到底發生了什麼事?」這個主題內其實牽扯到很多事情,原本想說把流程整理一下就好,結果一路挖下去,才發現從網址解析、網路連線到畫面渲染,每一段都還能再拆成好幾個主題。

這篇會先站在比較高的地方看完整流程,讓我們知道每個階段在做什麼,以及遇到效能問題時可以往哪裡查。至於各階段的實作細節,會留給後面的文章慢慢深挖。畢竟功能都還沒做出來,產品也還賣不出去,就先把所有優化加好加滿,通常只會讓自己更忙而已!

那就開始這趟旅程吧!

先看一次完整的 Navigation ​

以下會以 Chromium 架構為主。其他瀏覽器的 process 配置與內部實作可能不同,但從解析網址、建立連線、取得文件到產生畫面的主要階段仍然相近。

參考 Chrome 的 Inside look at modern web browser(part 2),一次新的頁面 Navigation 可以先整理成以下流程:

  1. 使用者在網址列輸入內容後,Browser Process 的 UI Thread 會判斷這段文字應該當成 URL,還是交給預設搜尋引擎搜尋。
  2. 確認目標 URL 後,瀏覽器開始 Navigation。若沒有可直接重用的連線,就要經過 DNS resolution、TCP connection 與 HTTPS 所需的 TLS handshake,再送出 HTTP request。
  3. 如果伺服器回傳 redirect response,瀏覽器會依狀態碼與相關 header 決定新的目標,再進行下一次 Navigation request。
  4. 收到 response 後,瀏覽器會參考 Content-Type、MIME type sniffing 結果與安全檢查,判斷內容要交給 Renderer Process 顯示,還是當成下載檔案處理。
  5. Browser Process 選定 Renderer Process 後,雙方會透過 IPC 進行 Navigation commit。從這個時間點開始,網址列、上一頁與下一頁等瀏覽器 UI 才會切換到新的文件狀態。
  6. Renderer Process 解析 HTML、套用 CSS、執行 JavaScript,並經過 Layout、Paint、Rasterization 與 Compositing 產生畫面。頁面也會繼續載入圖片、字型等其他資源。
  7. 文件與相關資源完成載入後,Renderer Process 會通知 Browser Process 更新頁面狀態,瀏覽器才會在適當時機停止網址列上的 loading 圖示。

這裡要特別區分兩件事:Navigation commit 不代表畫面已經渲染完成,而畫面第一次出現也不代表所有資源都載入完畢。實際分析效能時,還是要透過 DevTools 與 Performance API 觀察每個階段花了多少時間。

開始前,先認識 Process 與 Thread ​

在繼續往下之前,要先有一個基本概念:process 是作業系統分配資源與隔離執行環境的單位,thread 則是在 process 裡實際排程工作的單位。同一個 process 可以有多條 thread,而不同 process 之間通常會透過 IPC(Inter-Process Communication)交換資料。

Chromium 不會把所有工作都塞進同一個 process,而是拆成 Browser、Renderer、GPU、Network Service 等不同角色。這樣可以改善穩定性與安全隔離,不過也會增加記憶體使用量與 IPC 成本。

一個 Tab 也不一定只對應一個 Renderer Process。受到 Site Isolation、跨站 iframe、裝置資源與 process 數量上限等因素影響,同一個 Tab 可能使用多個 Renderer Process,多個頁面也可能共用同一個 process。

想繼續了解這些 process 如何分工,以及為什麼不是拆得越多就越好,可以接著看瀏覽器 Multi-process 架構。

Single-process 與 Multi-process 瀏覽器架構的分工差異

瀏覽器怎麼拆解 URL? ​

以下面這個 URL 為例:

text
https://blog.itskylen.com:443/tech/browser-navigation/?source=ironman#navigation

可以拆成幾個常見部分:

  • https:scheme,決定這個 URL 使用的協定與解析規則。
  • blog.itskylen.com:host;如果是網域名稱,後續通常需要透過 DNS 取得可連線的 IP 位址。
  • 443:port。HTTP 與 HTTPS 的預設 port 分別是 80 與 443,使用預設值時通常可以省略。
  • /tech/browser-navigation/:path,用來識別來源上的資源位置;它是 URL path,不一定直接對應伺服器上的實體 UNIX 檔案路徑。
  • ?source=ironman:query,由應用程式自行定義參數用途。
  • #navigation:fragment,用來指出文件內的位置或狀態;一般 HTTP request 不會把 fragment 傳給伺服器。

瀏覽器會先把輸入內容解析成 URL,再根據 scheme、host、port 等資訊決定後續如何連線。假如輸入的不是有效 URL,網址列才可能改用預設搜尋引擎處理。

從網域名稱走到 HTTP Response ​

確認 URL 後,接下來會進入網路階段。這一段最容易被一句「瀏覽器發出 request」帶過,但實際上還包含 DNS、TCP、TLS、HTTP 版本協商、redirect、內容壓縮與 cache 等工作。

DNS Resolution ​

如果 host 是網域名稱,瀏覽器需要先取得可用的 IP 位址。不過這不代表每次都會從 Root Name Server 開始查詢:瀏覽器、作業系統與 recursive resolver 都可能已經有 cache。

DNS 也不一定是整次載入最花時間的階段。命中 cache 時可能幾乎看不到額外等待;cache miss、網路品質不佳或 resolver 回應較慢時,時間才會比較明顯。完整查詢流程可以參考瀏覽器如何把網域名稱轉換成 IP?。

建立連線並傳送 HTTP Request ​

取得 IP 位址後,瀏覽器才能繼續建立連線。以 HTTPS over TCP 來說,通常還會經過 TCP 與 TLS handshake;如果已有可重用的連線,則不一定每次都重新執行完整流程。

HTTP 版本也會影響多個 request 如何共用連線。HTTP/1.1 可以使用 persistent connection,但同一條連線仍可能遇到 application-layer 的 head-of-line blocking;HTTP/2 則把 message 拆成 frame,讓多個 stream 可以在同一條 TCP connection 上交錯傳輸。詳細差異可以看 HTTP/1.0、HTTP/1.1 跟 HTTP/2 之間到底差在哪?。

如果 request 或 response 帶有 body,也不能只靠 GET、POST 直接推斷會使用幾個 TCP segment。HTTP message 怎麼被切成 TCP segment,會受到資料大小、TCP、TLS 與網路環境影響;100 Continue 也只有 Client 送出 Expect: 100-continue 時才會進入對應流程,而且常見瀏覽器通常不會自行送出這個 header。

壓縮與 Cache ​

伺服器可以根據 request 的 Accept-Encoding 選擇 gzip、Brotli 等 content coding,再透過 Content-Encoding 告訴瀏覽器如何解壓縮。這能減少網路上的傳輸量,但不代表原始 JavaScript、CSS 或圖片就不需要繼續控制大小。完整協商流程可以參考 HTTP 內容壓縮:透過 Accept-Encoding 減少傳輸量。

如果 response 允許快取,瀏覽器也可能直接重用仍然 fresh 的內容,或使用 ETag、Last-Modified 向伺服器進行 revalidation。不過 HTTP Cache、Cache Storage 與 BFCache 解決的是不同問題,不能全部當成同一套 cache。這一段可以接著看瀏覽器快取機制有哪些?。

收到 Response 後,瀏覽器怎麼處理? ​

開啟 Chrome DevTools 的 Network 面板並選擇一筆 request,可以先從 General 區塊看到這次請求的 URL、method、status code 與 remote address。這裡顯示的是 HTTP request 的摘要,不是底層實際傳送了幾個 TCP segment。

Chrome DevTools Network 面板顯示 Request URL、Method、Status Code 與 Remote Address

收到 response header 與部分資料後,瀏覽器會判斷內容類型並執行安全檢查。假如這次是 HTML document,瀏覽器不需要等整份檔案下載完成才開始處理,而是可以一邊接收資料、一邊解析並建立 DOM。

HTML parser 遇到 stylesheet、script、圖片或字型等內容時,還會觸發後續資源請求。這些資源被發現的時間、下載優先度與執行方式,都可能影響畫面多快出現。

CSS、JavaScript 與資源發現 ​

CSS 裡的 @import 或 background-image,通常要等瀏覽器取得並解析 stylesheet 後才會被發現;直接寫在 HTML 裡的 <link> 則能更早被 parser 或 preload scanner 發現。若有確定會使用、但正常流程較晚才被找到的關鍵資源,可以評估使用 preload、modulepreload 等方式提示瀏覽器,詳細差異可以看優化資源的載入時機:Resource Hints。

一般的 classic script 會在下載與執行期間暫停 HTML parsing;加上 async、defer,或改用 module script 後,下載與執行時機都會不同。要怎麼選,取決於 script 是否依賴 DOM、其他 script 與固定執行順序,可以參考 script 中 async、defer 與 module 的差異。

Renderer Process 如何把內容變成畫面? ​

Renderer Process 取得 HTML、CSS 與 JavaScript 後,會逐步建立 DOM、計算 style、執行 Layout 與 Paint,再經過 Rasterization 和 Compositing 組成畫面。這不是一條每次都從頭跑到尾的固定流水線;後續更新會依變動內容,決定是否需要重新 Layout、Paint 或只做 Compositing。

想知道 DOM、CSSOM、Layout 與 Paint 各自負責什麼,可以繼續看瀏覽器渲染流程:從 DOM、CSSOM 到 Layout 與 Paint。

圖片也會直接影響傳輸量、解碼成本與畫面穩定性。可以先從圖片格式怎麼選?PNG、JPEG、SVG、WebP 與 AVIF開始,根據圖片內容、畫質與透明度需求選擇合適格式。

選好格式後,再透過響應式圖片怎麼做?srcset、sizes 與 picture,讓瀏覽器依照圖片顯示尺寸、viewport、DPR 與格式支援度選擇適合的候選資源。最後可以接著看延遲圖片載入需要注意什麼,判斷哪些非首屏圖片適合延後載入,以及如何利用圖片尺寸與 placeholder 降低版面位移。這樣就能依序處理「格式、尺寸、載入時機」三個問題。

預先準備下一次 Navigation ​

前面介紹的是使用者送出網址或點擊連結後,瀏覽器才開始執行 Navigation。那既然最後都要走過網路請求、解析與渲染,有沒有辦法在使用者真的前往下一頁之前,先完成一部分工作呢?

Speculation Rules API 可以讓網站描述使用者接下來可能前往的 document。瀏覽器可以依規則進行 prefetch;當下一次 Navigation 的機率夠高時,也可以使用 prerender,提前載入並準備整個頁面,等使用者真的前往時再 activate。

不過 Speculation Rules 是提示,不是命令,瀏覽器可以依使用者設定、裝置資源與其他條件忽略規則。如果預測錯誤,提前消耗的頻寬、CPU、記憶體與 Server 資源也可能全部變成浪費,所以不能看到連結就全部 prerender!詳細用法與限制可以參考如何透過 Speculation Rules 預先準備下一個頁面?。

優化前,先把問題量出來 ​

從網址列按下 Enter 到畫面出現,中間有太多階段可能影響速度。DNS、TCP、TLS、Server response、資源下載、JavaScript、Layout 與 Paint 都可能是瓶頸,但不會每個網站都卡在同一個地方。

所以比起先背一份「一定要做的優化清單」,更實際的做法是先用 DevTools Network 與 Performance 面板找出時間花在哪裡,再回到對應階段處理。這樣做的優化才是在解決真正的問題,而不是讓專案多出一堆暫時用不到的複雜度!

參考資料 ​

按 Enter 或空白鍵放大圖片,按 Escape 關閉放大檢視。