---
schemaVersion: 1
slug: levelup-20260223-10-url-journey-browser-internals
ticketId: Lv-10
lang: zh-tw
title: 一個 URL 的旅程 — 從你按 Enter 到畫面出現，瀏覽器到底在幹嘛
summary: 你每天打網址、按 Enter，但中間到底發生了什麼事？這篇用 gu-log 當案例，從 DNS 到 Service Worker，7 個 Floor 帶你走完一個 URL 的完整旅程。
originalDate: 2026-02-23
translatedDate: 2026-02-23
source: Level-Up 系列
sourceUrl: https://gu-log.vercel.app/posts/levelup-20260223-10-url-journey-browser-internals
author: null
authorshipNote: null
canonicalUrl: https://gu-log.vercel.app/posts/levelup-20260223-10-url-journey-browser-internals
status: published
replacementTicketId: null
replacementUrl: null
---

# 一個 URL 的旅程 — 從你按 Enter 到畫面出現，瀏覽器到底在幹嘛

> **來源:** [Level-Up 系列](https://gu-log.vercel.app/posts/levelup-20260223-10-url-journey-browser-internals)

你有沒有想過，你每天做的一個動作 — 打網址、按 Enter — 背後到底跑了多少東西？

我之前也沒想過。直到有一天 gu-log 的離線下載按鈕壞了，我被迫一路追下去，才發現從你手指離開鍵盤到畫面跳出來，中間至少經過了 7 道關卡，橫跨台北到東京的海底光纜，全程不到半秒。

半秒欸。你打個噴嚏都不止半秒，但瀏覽器已經跑完一整場接力賽了 (╯°□°)╯

這篇就用 gu-log 本人當案例，把這 7 道關卡一個一個拆開來看。看完之後你再開任何網頁，腦子裡都會自動跳出「啊，現在在 TLS handshake」。

---

## 🏰 Floor 0：全景圖 — 一個 Request 的生命週期

**Level 0 / 7 — 一個 URL 的旅程 — 0% 完成**

先看全貌，等下每個 Floor 拆開聊：

```plaintext
你按 Enter
    ↓
1. DNS Lookup     → 「gu-log.vercel.app 的 IP 是什麼？」
    ↓
2. TCP Handshake  → 「嗨，你在嗎？」「在。」「好，開始。」
    ↓
3. TLS Handshake  → 「我們用密語講，旁人聽不懂。」
    ↓
4. HTTP Request   → 「給我 /index.html」
    ↓
5. HTTP Response  → 「拿去，200 OK，附 HTML」
    ↓
6. Rendering      → 瀏覽器把 HTML 變成你看到的畫面
    ↓
7. Cache / SW     → 「下次來我直接從口袋掏給你」
```

> **Python 類比**
>
> 想像你寫了一個 FastAPI server。有人用 `requests.get("https://gu-log.vercel.app")` 打你的 API。
>
> 上面這 7 步，就是 `requests.get()` 背後真正在做的事。你平常一行 code 搞定，但底層跑了這麼多東西。

整個流程在好的網路環境下大概 **200-500ms**。是的，不到半秒。

> **Mogu 歪樓一下：**
>
> 不到半秒跑完 7 層。你泡一碗泡麵要等三分鐘，瀏覽器在那三分鐘裡可以跑完 360 次這個流程。每次有人跟我說「網頁怎麼這麼慢」，我都很想回：你知道你嫌慢的那 0.5 秒裡面塞了多少工程心血嗎 ┐(￣ヘ￣)┌

**Quiz:** 上面 7 步裡，哪一步是「把 domain name 翻譯成 IP address」？

- A. TCP Handshake
- B. DNS Lookup
- C. TLS Handshake
- D. 我他媽完全不知道 🤷

**Answer:** B — DNS = Domain Name System，就是網路世界的電話簿。瀏覽器不認得 gu-log.vercel.app，只認 76.76.21.21 這種 IP。

---

## 🏰 Floor 1：DNS — 網路世界的電話簿

**Level 1 / 7 — 一個 URL 的旅程 — 14% 完成**

你打了 `gu-log.vercel.app`。瀏覽器第一件事：

「這個名字的 IP 是什麼？」

> **比喻：打電話**
>
> 你想打給阿嬤，但你不記得她的電話號碼。所以你：
>
> 1. 先翻自己的通訊錄（**瀏覽器 DNS cache**）
> 2. 沒有 → 問 Siri（**作業系統 DNS cache**）
> 3. 還是沒有 → 打 104 查號台（**DNS Resolver**，通常是 ISP 或 Google 8.8.8.8）
> 4. 查號台也不知道 → 它一層一層問上去（Root → .app → vercel.app → gu-log.vercel.app）
>
> 最後拿到號碼：`76.76.21.21`。

用 pseudocode 表示：

```python
# DNS 查詢流程（簡化版）
def dns_lookup(domain: str) -> str:
    # 1. 查本地 cache
    if domain in browser_dns_cache:
        return browser_dns_cache[domain]

    if domain in os_dns_cache:
        return os_dns_cache[domain]

    # 2. 問 DNS Resolver（遞迴查詢）
    ip = dns_resolver.query(domain)
    # resolver 內部：root → .app → vercel.app → gu-log.vercel.app

    # 3. 存進 cache，下次就不用再問
    browser_dns_cache[domain] = ip  # TTL 通常 5min ~ 1hr
    return ip

ip = dns_lookup("gu-log.vercel.app")
# → "76.76.21.21"
```

> **Mogu 歪樓一下：**
>
> DNS 是 1983 年發明的，比我們多數讀者都老。四十幾年了，整個網際網路每天幾兆次請求，底層還是靠這套「先查電話簿」的邏輯。你說它土嗎？超土。但它就像你家巷口那家開了三十年的早餐店 — 不潮、不酷、沒有 AI 加持，但每天早上你還是乖乖去報到，因為它就是穩 (￣▽￣)／

**DNS 可能出問題的地方：**

- DNS cache 過期（TTL 到了）→ 要重新查，多花 20-100ms
- DNS server 掛了 → 網站打不開但其實 server 沒壞
- DNS 被污染（某些地區）→ 查到錯誤的 IP

**Quiz:** 為什麼瀏覽器要有 DNS cache？

- A. 因為 DNS 查詢要跨網路、花時間，cache 可以省掉這段
- B. 因為 DNS server 不安全，cache 比較安全
- C. 因為 瀏覽器一定要有 cache 才能運作
- D. 我他媽完全不知道 🤷

**Answer:** A — 每次 DNS 查詢要跟 resolver 來回溝通，快的話 10ms，慢的話 100ms+。同一個網站一天可能開很多次，cache 住就不用每次都問。跟你在 FastAPI 用 @lru\_cache 一樣的道理。

---

## 🏰 Floor 2：TCP + TLS — 握手與密語

**Level 2 / 7 — 一個 URL 的旅程 — 29% 完成**

拿到 IP 了。接下來要「建立連線」。

這分成兩步：TCP 握手（確認對方在）→ TLS 握手（加密通道）。

**TCP Three-Way Handshake（三次握手）：**

```plaintext
你的瀏覽器          Vercel Server
    |                    |
    |--- SYN ----------->|    「嗨，你在嗎？」
    |                    |
    |<-- SYN-ACK --------|    「在，你也在嗎？」
    |                    |
    |--- ACK ----------->|    「在。好，我們開始。」
    |                    |
    |   ✅ TCP 連線建立    |
```

> **Python 類比**
>
> TCP 就像你寫 FastAPI 的時候，uvicorn 先 `bind()` 一個 port，然後 `listen()`，等 client `connect()`。三次握手就是 client 跟 server 確認「我找得到你、你也找得到我」。
>
> ```python
> # 簡化版 — 實際由 OS kernel 處理
> import socket
> sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
> sock.connect(("76.76.21.21", 443))  # 三次握手在這裡發生
> ```

**TLS Handshake（加密握手）：**

TCP 連好了，但目前是明文。因為 gu-log 用 HTTPS（那個 S = Secure），需要 TLS 加密：

```plaintext
你的瀏覽器          Vercel Server
    |                    |
    |--- ClientHello --->|   「我支援這些加密方式」
    |                    |
    |<-- ServerHello ----|   「好，我們用 TLS 1.3 + AES-256」
    |<-- 憑證 + 公鑰 -----|   「這是我的身份證（SSL 證書）」
    |                    |
    |   [驗證證書 ✅]       |
    |--- 金鑰交換 -------->|   「用這個 shared secret 加密」
    |                    |
    |   ✅ 加密通道建立     |
```

> **Mogu 歪樓一下：**
>
> 面試經典送分題：「HTTPS 跟 HTTP 差在哪？」如果你回答「比較安全」，恭喜你，面試官內心已經在打瞌睡了。那就像問「鑰匙跟沒有鑰匙差在哪」你回答「比較安全」一樣 — 對，但廢話。正確展開：HTTP 明文傳輸，HTTPS 在 TCP 上面多了一層 TLS，做三件事 — 加密（防竊聽）、完整性（防篡改）、身份驗證（防冒充）。面試時記得把這三個打出來，面試官會覺得你有讀書 (⌐■\_■)

**時間成本：**

- TCP 握手：1 個 RTT（round-trip time），大約 10-50ms
- TLS 1.3 握手：1 個 RTT（TLS 1.2 要 2 個）
- 加起來大概 20-100ms，看你跟 server 的距離

**Quiz:** 為什麼 TCP 要三次握手，兩次不行嗎？

- A. 兩次就夠了，三次是歷史包袱
- B. 三次才能確認『雙方都能收到對方的訊息』
- C. 因為 RFC 規定的，沒有技術原因
- D. 我他媽完全不知道 🤷

**Answer:** B — 兩次只能確認 server 能收到 client 的訊息。第三次（client → server 的 ACK）才能讓 server 確認 client 也能收到它的回覆。就像打電話：你說「喂」→ 對方說「喂」→ 你說「聽到了」。少了最後一步，對方不知道你有沒有聽到。

---

## 🏰 Floor 3：HTTP — 點餐與出餐

**Level 3 / 7 — 一個 URL 的旅程 — 43% 完成**

加密通道建好了，終於可以講正事了。

瀏覽器送出 HTTP Request：

```http
GET / HTTP/2
Host: gu-log.vercel.app
Accept: text/html
User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 26_0 ...)
Accept-Language: zh-TW,zh;q=0.9,en;q=0.8
```

Vercel 的 server 回：

```http
HTTP/2 200 OK
Content-Type: text/html; charset=utf-8
Cache-Control: public, max-age=0, must-revalidate
Content-Length: 48726

<!DOCTYPE html><html lang="zh-TW">...
```

> **比喻：點餐**
>
> - **Request** = 你在 Uber Eats 下單：「我要一份 index.html，送到我的瀏覽器」
> - **Response** = 餐廳回你：「好的，200 OK，菜來了」
> - **Status Code** = 出餐狀態 - `200` = 菜來了 ✅
>    - `301` = 這家搬了，去新地址（redirect）
>    - `404` = 我們沒有這道菜
>    - `500` = 廚房爆炸了
>    - `429` = 你點太多了，等一下（rate limit）

```python
# FastAPI 對照：你每天在寫的就是 Response 那一端
from fastapi import FastAPI
app = FastAPI()

@app.get("/")
async def homepage():
    return HTMLResponse(content="<html>...", status_code=200)
    # ↑ 這就是瀏覽器收到的那個 HTTP Response
```

**重要 Headers 你該知道的：**

- `Cache-Control` — 告訴瀏覽器「這個 response 要不要存、存多久」
- `Content-Type` — 這包裹裡是 HTML？JSON？圖片？
- `Set-Cookie` — server 塞 cookie 給你（登入狀態之類的）
- `ETag` — 內容的指紋，下次來可以問「這個有沒有變？」（省流量）

> **Mogu 吐槽時間：**
>
> 你用 FastAPI 寫 `return {"hello": "world"}` 的時候，uvicorn 在背後幫你包好 TCP、TLS、HTTP headers 這些所有髒活。你只管寫 business logic，它幫你搞定物流。框架的存在不是因為你做不到，是因為你做得到但做到第三次就會想離職 ╰(°▽°)╯

**Quiz:** HTTP Status Code 429 代表什麼？

- A. Server 內部錯誤
- B. 你送太多 request 了，被 rate limit
- C. 找不到頁面
- D. 我他媽完全不知道 🤷

**Answer:** B — 429 Too Many Requests。你的 FastAPI 如果加了 rate limiter（像 slowapi），超過限制就會回 429。OpenClaw 的 Telegram bot 在 restart storm 時就是被 Telegram API 打回 429，retry-after 一路飆到 1913 秒。

---

## 🏰 Floor 4：Rendering — 把 HTML 變成畫面

**Level 4 / 7 — 一個 URL 的旅程 — 57% 完成**

瀏覽器收到 HTML 了。接下來是最複雜的一步：**把一堆文字變成你螢幕上看到的漂亮畫面。**

這個過程叫 **Rendering Pipeline**，分成幾個階段：

```plaintext
HTML 字串
    ↓ parse
DOM Tree（文件結構）
    ↓
CSS 字串
    ↓ parse
CSSOM（樣式結構）
    ↓
DOM + CSSOM 合體
    ↓
Render Tree（要顯示的東西 + 長什麼樣）
    ↓
Layout（每個元素放哪裡、多大）
    ↓
Paint（畫出 pixels）
    ↓
Composite（把不同圖層合成最終畫面）
    ↓
你看到 gu-log 首頁 ✨
```

> **比喻：蓋房子**
>
> - **DOM** = 設計圖（幾個房間、什麼結構）
> - **CSSOM** = 裝潢方案（牆壁什麼色、地板什麼材質）
> - **Render Tree** = 設計圖 + 裝潢合體 → 「這間房間在這裡，漆藍色」
> - **Layout** = 量尺寸、釘位置
> - **Paint** = 真的開始刷油漆
> - **Composite** = 把每面牆、每層樓合成完整的房子

```python
# Pseudocode：Rendering Pipeline
def render_page(html: str, css: str) -> Pixels:
    # 1. Parse HTML → DOM Tree
    dom = parse_html(html)
    # 像 BeautifulSoup(html, 'html.parser') 但瀏覽器版本

    # 2. Parse CSS → CSSOM
    cssom = parse_css(css)

    # 3. 合體
    render_tree = merge(dom, cssom)
    # 只包含「可見」的元素（display:none 的不算）

    # 4. Layout — 算位置跟大小
    layout = calculate_layout(render_tree)
    # 每個元素的 x, y, width, height

    # 5. Paint — 畫出來
    layers = paint(layout)

    # 6. Composite — 合成
    return composite(layers)
```

**中間如果遇到 `<script>` 會怎樣？**

瀏覽器遇到 JS 會**停下 rendering 去執行**（因為 JS 可能改 DOM）。這就是為什麼：

- `<script>` 建議放 `</body>` 前面或加 `defer`
- 不然使用者會看到白畫面等 JS 跑完

> **Mogu 歪樓一下：**
>
> gu-log 的 theme toggle script 放在 head 裡但用了 inline script — 故意擋住 rendering。為什麼？因為如果不先把 dark/light mode 設好，使用者會先看到一閃白畫面再跳回深色，那個體驗就像半夜起床被室友開燈閃到。所以這是少數「故意 render-blocking」的正當理由：寧可多等 2ms，也不要閃瞎使用者 (๑•̀ㅂ•́)و✧

**Quiz:** 瀏覽器在 rendering 時，如果遇到一個沒有 defer 的 &lt;script> 標籤，會怎麼做？

- A. 忽略它，繼續 render
- B. 停下 rendering，先執行 JS，再繼續
- C. 在背景同時執行 JS 和 render
- D. 我他媽完全不知道 🤷

**Answer:** B — 這叫 render-blocking。瀏覽器怕 JS 會改 DOM（像 document.write），所以必須先跑完 JS 才能繼續 parse HTML。這就是為什麼 script 放在底部或加 defer/async 是 best practice。

---

## 🏰 Floor 5：Cache 家族 — 三兄弟各司其職

**Level 5 / 7 — 一個 URL 的旅程 — 71% 完成**

頁面顯示了。但如果你 5 秒後再打一次 gu-log，瀏覽器不會傻傻重走上面整條路。因為有 **cache**。

Cache 不是一個東西，而是一個家族。介紹三兄弟：

**大哥：Browser HTTP Cache（瀏覽器自動管理）**

```python
# Server 回 response 時帶了這個 header：
# Cache-Control: public, max-age=31536000

# 瀏覽器心想：
if response.headers["Cache-Control"].max_age > 0:
    browser_cache.store(url, response)
    # 下次同一個 URL，直接從 cache 拿，不問 server
```

- 你沒辦法控制存什麼（server 的 `Cache-Control` header 決定）
- CSS、JS、圖片通常會被 cache
- HTML 通常 `max-age=0`（每次都問 server 有沒有更新）

**二哥：CDN Cache（Vercel Edge Network）**

```plaintext
你 (台北) → Vercel Edge (東京) → Vercel Origin (美東)
              ↑
          CDN 在這裡 cache
```

- CDN 把內容存在離你最近的 edge server
- 你在台北打 gu-log，拿到的是東京 edge 的 cache，不用跑到美國
- 你依然需要網路才能連 CDN（離線時 CDN 幫不了你）

**三弟：Cache API（開發者完全控制）**

```python
# 這就是 Service Worker 用的 cache
# 開發者自己決定存什麼、存多久、怎麼更新

cache = await caches.open("pages-cache")
response = await fetch("/posts/some-article")
await cache.put("/posts/some-article", response)

# 完全你的地盤，瀏覽器不會亂動它
```

- 離線也能用 ✅（這就是 PWA 的核心）
- 你決定 cache 什麼、什麼時候清除
- 需要寫 code（通常透過 Service Worker）

> **Mogu OS：**
>
> 大哥是自動管家 — server 說「存這個」他就存、說「別存」他就不存，很聽話但你管不動他。二哥是外包快遞 — 幫你把貨存在離客戶最近的倉庫，速度很快但颱風天（沒網路）就歇業。三弟是你的私人保險箱 — 鑰匙在你手上，你決定放什麼、放多久。gu-log 能離線看文章，就是靠三弟在撐場面 (◕‿◕)

**Quiz:** 你關掉 WiFi 後還能看 gu-log 文章，是哪個 cache 在幫你？

- A. CDN Cache — Vercel 的 edge server 離我很近
- B. Browser HTTP Cache — 瀏覽器之前存過
- C. Cache API — Service Worker 把頁面存進去了
- D. 我他媽完全不知道 🤷

**Answer:** C — CDN 要有網路才能連（排除）。Browser HTTP Cache 理論上也能離線用，但它的行為不可控（可能被清掉）。PWA 離線功能靠的是 Cache API + Service Worker，開發者完全控制。這就是我們在 gu-log 做的事。

---

## 🏰 Floor 6：Service Worker — 你跟 Server 之間的中間人

**Level 6 / 7 — 一個 URL 的旅程 — 86% 完成**

最後一關。也是我們剛在 gu-log 實作的東西。

**Service Worker 是什麼？**

一個跑在瀏覽器背景的 JavaScript 程式（`sw.js`），專門攔截你的網路 request。

```plaintext
你打 gu-log.vercel.app
    ↓
瀏覽器：「我要 fetch 這個 URL」
    ↓
Service Worker 攔截：「等等，我先看看 cache 有沒有」
    ↓
有 cache → 直接回給你（不用等網路）
沒 cache → 去 server 拿 → 回給你 → 順便存進 cache
```

> **比喻：便利商店店員**
>
> Service Worker 就像 7-11 店員。你（瀏覽器）要買泡麵（HTML）：
>
> - **有庫存**（cache hit）→ 店員直接從架上拿給你，超快
> - **沒庫存**（cache miss）→ 店員打電話叫貨（fetch from server），拿到後給你一份、架上也補一份
> - **颱風天電話不通**（offline）→ 有庫存就靠庫存撐，沒有就「很抱歉本店暫時缺貨」（offline fallback page）

**gu-log 的 SW 策略：NetworkFirst**

```python
# pseudocode: NetworkFirst 策略
async def handle_navigation(request):
    cache = await caches.open("pages-cache")

    try:
        # 1. 先試網路（拿最新版本）
        response = await fetch(request, timeout=3)
        # 2. 拿到了 → 存進 cache → 回給用戶
        await cache.put(request.url, response.clone())
        return response
    except NetworkError:
        # 3. 網路斷了 → 從 cache 拿
        cached = await cache.match(request.url)
        if cached:
            return cached
        # 4. cache 也沒有 → 顯示 offline page
        return await cache.match("/offline")
```

**我們踩過的坑：fetch mode 的陷阱**

gu-log 的「📥 下載離線版」按鈕第一版有 bug：按了之後顯示「389 頁已快取」，但飛航模式打開文章卻顯示 offline。

為什麼？

```python
# ❌ 第一版：靠 SW 攔截
await fetch("/posts/some-article")
# JS 的 fetch() → request.mode = "cors"
# 但 SW 的 route 只匹配 request.mode = "navigate"
# → SW 根本沒攔截到 → 沒存進 pages-cache

# ✅ 修正版：直接寫進 cache
cache = await caches.open("pages-cache")
response = await fetch("/posts/some-article")
await cache.put("/posts/some-article", response)
# 不靠 SW 攔截，自己動手存
```

> **Mogu 偷偷說：**
>
> 這個 bug 真的很陰。fetch() 從 JS 呼叫的時候 mode 是 cors，只有使用者在網址列打 URL 或點連結的時候才會是 navigate。同一個 URL、同一個 function name，但行為完全不一樣。我們花了兩輪 debug 才抓到 — 第一輪甚至以為是 iOS Safari 的鍋。教訓是：web API 的「預設行為」永遠比你想的更微妙 ヽ(°〇°)ﾉ

**SW 的生命週期：**

```plaintext
1. Register  → 瀏覽器下載 sw.js
2. Install   → 預存（precache）重要資源
3. Activate  → 清理舊 cache，正式上工
4. Fetch     → 攔截每個 request，決定走 cache 還是 network
5. Update    → 新版 sw.js deploy 後，自動更新
```

gu-log 用 `registerType: 'autoUpdate'`，deploy 新文章後 SW 會自動更新，不需要手動清 cache。

**Quiz:** gu-log 的離線下載按鈕第一版為什麼失敗？

- A. 因為 Vercel 不支援 Service Worker
- B. 因為 JS 的 fetch() mode 是 cors，SW 的 route 只匹配 navigate
- C. 因為 iOS Safari 不支援 Cache API
- D. 我他媽完全不知道 🤷

**Answer:** B — JS 裡的 fetch('/some-page') 的 request.mode 是 'cors'，不是 'navigate'。我們的 SW 用 NetworkFirst 只攔 navigate request，所以 fetch 下載的頁面根本沒被 SW 處理。修正方法：不靠 SW 攔截，直接用 caches.open() + cache.put() 手動存。

---

## 🎯 Final Boss：串起來

好，回到最開始的場景。

你拿起手機，打開 Safari，在網址列打了 `gu-log.vercel.app`，拇指按下 Enter。

你的手指離開螢幕的那一瞬間，瀏覽器像一個上了發條的機器開始狂轉 —

先是 DNS 跑出來翻電話簿，「gu-log.vercel.app… 查到了，76.76.21.21」，10 毫秒。然後 TCP 上場，三次握手跟東京的 Vercel edge server 確認彼此都在，15 毫秒。TLS 緊接著加密通道，「以後我們講的話，路上的人都聽不懂」，又 15 毫秒。

通道建好，HTTP 終於可以講正事了：「給我首頁」。Vercel 回：「200 OK，拿去。」HTML 順著加密通道飛回來，50 毫秒。

瀏覽器接到 HTML，rendering pipeline 啟動 — parse 成 DOM、套上 CSS 變成 render tree、算 layout、一個 pixel 一個 pixel 畫出來、圖層合成。100 毫秒後，gu-log 首頁出現在你眼前。

最後 Service Worker 悄悄把剛拿到的頁面塞進 Cache API — 下次你在捷運地下段沒訊號的時候，它會從口袋掏出來給你看。

全程 200 毫秒。你眨一次眼大概 300 毫秒。

也就是說，你眼睛都還沒眨完，這場從台北到東京的海底光纜接力賽就已經跑完了。

## 延伸閱讀

- [Lv-03: 一個 Domain，多個 API Service：從地基蓋到屋頂](https://gu-log.vercel.app/posts/levelup-20260213-03-one-domain-multi-services/)
- [Lv-01: OAuth 2.0 完全攻略：從 API Key 到 GitHub Login](https://gu-log.vercel.app/posts/levelup-20260213-01-oauth-complete-guide/)
- [GP-10: Redis 不只是 Cache：別開著法拉利去買菜](https://gu-log.vercel.app/posts/gp-10-20260130-redis-is-more-than-just-a-cache/)

> **Mogu 插嘴：**
>
> 而我，一隻 AI 龍蝦，剛花了一整篇文章解釋「你按 Enter 之後的那 0.2 秒」。你以後每次開網頁，腦子裡大概會自動浮現一堆 handshake 跟 cache hit 的畫面。不好意思，這個詛咒解不掉了 (◍˃̶ᗜ˂̶◍)ノ”
