網頁即時通訊的地圖——以及被小看的輪詢
嗨,我是凱文大叔。
開發網頁應用,遲早會遇到「即時」需求:聊天室訊息要馬上出現、通知鈴鐺要自己亮起來、儀表板數字要自己動、AI 回答要像打字機一樣逐字吐出來。
問題是——HTTP 天生不是為這件事設計的。HTTP 是「請求—回應」模型:瀏覽器不開口,伺服器就不能說話。伺服器手上有新資料,卻沒有管道主動塞給你。
所有網頁即時技術,本質上都在回答同一個問題:怎麼讓伺服器能「主動」通知瀏覽器?
答案不只一種,而且選錯的代價很真實:拿輪詢做聊天室,使用者一多伺服器就被打爆;拿 WebSocket 做每天更新一次的公告,白白扛著連線管理的複雜度。更麻煩的是,現在很多人讓 AI 寫即時功能——AI 寫得出能動的程式,但它選的技術對不對,你得自己看得懂才知道。
接下來三期,我會把這張地圖鋪完整:
- 本期:輪詢家族(短輪詢、長輪詢)——最古老、最簡單,也最常被誤用
- 下期:SSE——被低估的單向串流,AI 時代意外翻紅的主角
- 第三期:WebSocket、WebRTC,加上 Web Push 與 WebTransport 這些新世代成員,最後給你一張選型總表
先修正一個直覺:「底層不都是輪詢嗎?」
很多人(包括以前的我)有個直覺:管你什麼協定,底層反正都是輪詢。
這個直覺對一半。短輪詢確實是應用層的輪詢——你寫的程式每隔幾秒問一次。但 SSE 和 WebSocket 是持久連線:TCP 連線建好之後一直放著,資料到了就送過來。
「那作業系統不是還要一直檢查連線上有沒有資料?」——現代伺服器用的是 epoll/kqueue 這類事件通知機制:不是作業系統逐一巡邏一萬條連線問「你有資料嗎」,而是網卡收到封包後由中斷驅動,一路把「這條連線有資料了」的事件推給應用程式。一萬條安靜的連線,成本接近零。
所以正確的說法是:短輪詢是「你去問」,持久連線是「它來說」。這個差別直接決定了延遲、伺服器負載和流量成本——也是整個系列的主軸。

輪詢家族:什麼場景其實該用它
先幫輪詢平反:它不是落後技術,是適用場景被誤解的技術。
短輪詢(Short Polling):每隔固定時間發一個請求問「有新資料嗎」。
適合它的場景,共同特徵是更新頻率低、即時性要求鬆:
- 儀表板每 30 秒刷新一次統計數字
- 批次任務的進度條(匯出報表、影片轉檔)
- 每天只變動幾次的公告或設定
- Serverless/無狀態架構——函式跑完就結束,根本沒有地方掛長連線,輪詢反而是最自然的解
長輪詢(Long Polling):請求發出後伺服器先不回,等到有新資料(或超時)才回應,客戶端收到後立刻再發下一個請求。介於短輪詢和持久連線之間:延遲接近即時,但每輪仍有重建請求的開銷。它是 2000 年代「Comet」技術的核心,今天仍活在很多 SDK 的降級路徑裡——WebSocket 連不上時,退回長輪詢。

工商時間「功能會動」和「架構選對」中間差的那一層,就是這系列在講的東西。 我的 Hahow 課程 《駕馭 AI 的全端實戰養成班:從零打造企業級智慧應用系統》 用同一套 AI CRM 專案,帶你從 Spring Boot、React、資料庫權限,一路做到 Spring AI 的串流回覆——下期要講的 SSE 打字機效果,課程裡就是實戰單元。 |
後段:把輪詢寫到生產等級
輪詢人人會寫,但「能上線的輪詢」有幾個專業細節。每一招我都附一句可以直接丟給 AI 的提示詞——看懂原理之後,實作就交給它。
1. 流量算給你看
100 個使用者、每 5 秒輪詢一次 = 每秒 20 個請求打在伺服器上——而其中 99% 的回應是「沒有新資料」。輪詢的成本公式很殘酷:負載跟「使用者數 × 頻率」成正比,跟「實際有沒有新資料」無關。這就是為什麼高頻場景要換技術。
🤖 丟給 AI:「幫我估算這個輪詢功能對伺服器的負載,並評估要不要改用 SSE。」
2. 條件請求:讓「沒變」的回應近乎免費
搭配 ETag/If-None-Match,資料沒變時伺服器只回 304 Not Modified,不傳整包資料:
// 前端:帶上次的 ETag 去問
const res = await fetch('/api/board', {
headers: lastEtag ? { 'If-None-Match': lastEtag } : {}
});
if (res.status === 304) return; // 沒變,什麼都不用做
lastEtag = res.headers.get('ETag'); // 有變,更新畫面與 ETag
render(await res.json());
🤖 丟給 AI:「幫我的輪詢加上 ETag 條件請求,資料沒變就回 304。」
3. 頁面不在前景就別問
使用者切去別的分頁,你的輪詢還在跑就是純浪費。用 Page Visibility API 暫停:
document.addEventListener('visibilitychange', () => {
if (document.hidden) stopPolling();
else { pollOnce(); startPolling(); } // 回來先立刻補一次
});
🤖 丟給 AI:「用 Page Visibility API 讓我的輪詢在分頁隱藏時自動暫停。」
4. 出錯要退避,還要加亂數
伺服器一出錯,成千個客戶端同時重試會把它踩死(thundering herd)。指數退避+隨機抖動(jitter)是標配:
let delay = 5000;
async function poll() {
try {
await fetchBoard();
delay = 5000; // 成功就重置
} catch {
delay = Math.min(delay * 2, 60000); // 失敗指數退避,上限一分鐘
}
setTimeout(poll, delay + Math.random() * 1000); // 加抖動錯開大家
}
🤖 丟給 AI:「幫我的輪詢加上指數退避和隨機抖動。」
5. 長輪詢的伺服器端:別佔住執行緒
長輪詢最大的坑是把伺服器執行緒「掛著等」。Spring MVC 用 DeferredResult 讓等待不佔工作執行緒:
@GetMapping("/api/notifications/poll")
public DeferredResult<List<Notification>> poll(@RequestParam long since) {
// 30 秒超時,超時回空陣列,客戶端收到後再發下一輪
DeferredResult<List<Notification>> result =
new DeferredResult<>(30_000L, List.of());
notificationHub.register(since, result); // 有新通知時 setResult 即刻回應
return result;
}
客戶端收到回應(不管有沒有資料)就立刻再發下一個請求,形成「永遠有一個請求在伺服器待命」的循環。
有人會問:用 Callable 或 @Async 把工作丟到另一條執行緒,不也是非同步?差別在等待期間有沒有執行緒被佔著。Callable 只是把「空等」從 Tomcat 執行緒搬到你自己的執行緒池——1000 人掛著就燒 1000 條執行緒,什麼都沒解決。DeferredResult 則是連「等」都不佔執行緒:方法 return 之後,沒有任何執行緒在替這個請求做事,直到未來某條執行緒(例如別人發訊息的那個請求)呼叫 setResult(),回應才被寫回去。長輪詢等的是事件、不是計算——這正是前面 epoll「事件通知」精神在應用層的翻版:不派人空等,事件來了才動手。
🤖 丟給 AI:「用 Spring MVC 的 DeferredResult 幫我把這支 API 改成長輪詢。」
本期帶走三句話
- 即時技術都在解同一題:讓伺服器能主動說話;短輪詢是「你去問」,其他都是「它來說」。
- 低頻、鬆即時、serverless——這些場景輪詢反而是對的選擇,別急著上重武器。
- 能上線的輪詢 = 條件請求 + 前景偵測 + 指數退避 + 抖動;長輪詢記得別佔執行緒。
下期講 SSE——你每天在 ChatGPT 看到的打字機效果,背後就是它。單向串流為什麼在 AI 時代翻紅、又有哪些反向代理的坑,下期見。
—— 凱文大叔
延伸閱讀:
