懶人包
- 韓國個人開發者 10 月 28 日上線的線上寺廟「산사」,讓大家化身小沙彌在 3D 法堂裡合掌、叩拜、轉念珠,不能聊天。開站一天湧進數萬人(各家報導 5 萬到 10 萬),伺服器被擠爆。
- 座位從 20 個一路加到 1 萬個,最後改成 100 間、每間 100 人才撐住。
- 我讀了它的前端 bundle:Vite + Three.js 畫 3D,即時同步走 Cloudflare Workers 上的 WebSocket,訊息只有移動、坐下、叩拜、念珠這幾種。
- 我照這個架構做了繁中版「線上禪堂」,上線在 html.yanchen.app/zen-hall,兩個分頁同房互看、合掌會通知對方,E2E 通過。
- 同一台電腦壓測 2000 人:全擠一間 p95 延遲 4,164 ms,分成 20 間只要 451 ms。同一房的廣播量跟人數平方成正比,分房是讓它有上限的做法。
這是 Threads 上 @dajak.dev 的一則貼文,內容是韓國最近爆紅的線上寺廟:太多人跑去線上拜佛,連伺服器都被擠爆,作者笑到打了一串ㅋㅋㅋㅋ。配的影片是一整片小沙彌坐在佛像前。
笑完之後我好奇的是工程那一半:一個不能聊天、只有大家坐著的網站,為什麼會擠爆?所以我讀了原站前端,照同樣的架構仿做一間,再用壓測把「擠爆」重現出來。
산사 是什麼?為什麼在韓國爆紅?
산사(山寺)網頁標題是「산사 — 온라인 절」,描述寫著「말없이 같이 앉아 있는 온라인 절」,意思是「不說話、一起坐著的線上寺廟」。進去會變成一個 Q 版小沙彌,可以走到蒲團坐下、合掌、叩拜、轉念珠、聽心經。沒有聊天室。
| 項目 | 內容 | 來源 |
|---|---|---|
| 開發者 | 個人開發者「메타몽」(Metamong) | 農民新聞 |
| 上線 | 2026 年 10 月 28 日 | HuffPost Korea |
| 人潮 | 約 1.2 萬人時伺服器出狀況,當晚一天破 10 萬人 | 農民新聞,引開發者 Threads |
| 人潮(另一說) | 公開一天湧入 5 萬人,伺服器癱瘓 | HuffPost Korea |
| 容量演進 | 20 席 → 100 → 1,200 → 1 萬席,最後是 100 個頻道、每個 100 人 | 農民新聞、HuffPost Korea |
為什麼紅?HuffPost Korea 把它放在「戰爭、AI、氣候災難帶來不確定感,人們想找個地方讓心休息一下」的脈絡裡,同一篇也引了 2025 年韓國社會調查:13 歲以上有 38.2% 的人平常會感到孤獨。這個數字是背景,報導沒有直接說它造成산사爆紅。
它的設計本身就說明了爽點:有人陪,但不用社交。同一間房最多坐 100 個陌生人,你能做的只有向對方合掌、叩拜。原貼文作者的解讀比較直接:「韓國人火氣真的太大了,需要找個地方消消氣。」
韓國同一週也有人做了線上聖堂、線上教會(아시아투데이)。線上拜佛在韓國也不是新東西,2000 年代後期就有「사이버 법당」(網路法堂)。
산사 用什麼技術做的?
我把原站的 HTML 和 JS bundle 抓下來讀。以下是 bundle 裡看得到的東西:
| 層 | 用什麼 | bundle 裡的證據 |
|---|---|---|
| 建置 | Vite | /assets/index-<hash>.js 加 modulepreload 的打包格式 |
| 3D | Three.js | GLTFLoader 載入 monk.glb、buddha.glb;大量角色用 InstancedMesh |
| 遠端角色 | 低模版本 | 另有 monk-remote-lod.glb |
| 即時同步 | Cloudflare Workers 上的 WebSocket | 連線網址是 wss://sansa-realtime.<帳號>.workers.dev/presence |
| 備援 | Supabase Realtime | 只在沒設定 Worker 網址時才用 |
| 音訊 | 心經音檔 | heart-sutra-v1.mp3 |
hello、move{x, z, yaw, pose}、seat、stand、bow、bead、room-switch、ack、sync。沒有文字訊息,因為不能聊天。
後端是不是用 Durable Objects 做分房,前端看不出來,我不下結論。
我做的繁中版:線上禪堂
我照這個架構做了一個單檔版本,叫「線上禪堂」,已經放在 html.yanchen.app/zen-hall。
| 功能 | 做法 |
|---|---|
| 3D 場景 | Three.js 0.169(importmap 從 jsDelivr 載入),法堂、佛像、35 個蒲團、Q 版小沙彌都用基本幾何體拼 |
| 操作 | 點地板走路、點蒲團坐下、拖曳轉視角;按鈕有合掌、叩拜、念珠、念經、起身 |
| 聲音 | Web Audio 即時合成木魚和頌缽,不用版權音檔 |
| 同步 | 網址加 ?ws=ws://host:8787/presence 就連真的伺服器;不加就用 BroadcastChannel,同一個瀏覽器的分頁互通,再加 26 位模擬香客 |
真多人的部分我在本機驗。伺服器是 99 行的 Node.js(ws 套件),兩個分頁連同一台伺服器:
Playwright E2E 的實際輸出:
offline room label: 清蓮堂 1 號房 · 27/100 人(離線示範)
A label: 清蓮堂 1 號房 · 2/100 人 | A sees peers: 1
B label: 清蓮堂 1 號房 · 2/100 人 | B sees peers: 1
B toast after A palms: "有人向你合掌問候 🙏"
A sees B pose: beads
errors: none
E2E PASS
第一次跑是 FAIL。WebSocket 模式下伺服器發的 id 是數字,前端拿它呼叫 startsWith 直接噴錯;離線模式的 id 是字串,所以單機測不出來。改成先轉字串才過。
線上拜佛為什麼會擠爆伺服器?
關鍵在「同一間房裡,每個人都要看到其他所有人」。
一個人走一步,伺服器要通知房裡其他 N−1 個人。N 個人都在動,伺服器每秒要送出的量就跟 N × N 成正比。人數翻倍,廣播量變四倍。
所以分房的效果不是線性的。同樣 2000 人:
| 配置 | 每人要收幾人的狀態 | 廣播量(相對值) |
|---|---|---|
| 全擠一間 | 2,000 | 2000² = 400 萬 |
| 20 間 × 100 人 | 100 | 20 × 100² = 20 萬 |
我的伺服器還多做了一件事:不是收到一個動作就轉發一次,而是每 0.1 秒(10Hz)每房只打一包,裡面是這段時間內有動的人。這樣訊息「包數」不隨人數平方成長,但「包裡的內容」還是 N²,分房才是根本解。核心就這一段:
// 每 tick 每房只送一包:這一 tick 內有變動的人 + 事件
setInterval(() => {
for (const room of rooms.values()) {
if (!room.dirty.size && !room.events.length) continue;
const packet = { type: 'tick', states: [...room.dirty.values()], events: room.events };
for (const { ws } of room.members.values()) if (ws.readyState === 1) send(ws, packet);
room.dirty = new Map();
room.events = [];
}
}, 1000 / TICK_HZ);
分房的邏輯更短:找第一間還沒滿的房,都滿了就開新的。
function pickRoom() {
for (const [id, r] of rooms) if (r.members.size < ROOM_CAP) return id;
return rooms.size + 1;
}
壓測:2000 人全擠一間 vs 分 20 間
壓測程式開 N 個 WebSocket 客戶端,每人每秒動 4 次,量伺服器每秒送出多少、CPU 多少,以及「我動了一下 → 伺服器回播給我」的延遲。房間上限設成 100,000 就等於全擠一間。
我跑的指令如下(server.mjs 與 loadtest.mjs 放在我本機,依賴只有 ws 套件,伺服器核心就是上一節那兩段):
npm i ws
node loadtest.mjs 2000 100 8 # 2000 人,每房 100 人,跑 8 秒
node loadtest.mjs 2000 100000 8 # 2000 人,全擠一間
實際結果(Node.js v24.11.1,Windows 11,客戶端和伺服器在同一台機器):
| 人數 | 配置 | 房數 | 伺服器送出 | 伺服器 CPU | 延遲 p50 | 延遲 p95 | 收到回播次數 |
|---|---|---|---|---|---|---|---|
| 1000 | 每房 100 人 | 10 | 18.3 MB/s | 41% | 137 ms | 435 ms | 17,824 |
| 1000 | 全擠一間 | 1 | 73 MB/s | 81% | 151 ms | 652 ms | 12,619 |
| 2000 | 每房 100 人 | 20 | 37.9 MB/s | 84% | 167 ms | 451 ms | 36,068 |
| 2000 | 全擠一間 | 1 | 86 MB/s | 101% | 1,105 ms | 4,164 ms | 4,168 |
- 分房時延遲幾乎不動。1000 人到 2000 人,p95 從 435 ms 到 451 ms。人數加倍只是房數加倍,每個人收到的量不變。
- 全擠一間到 2000 人就崩了。CPU 吃滿,p95 延遲 4.2 秒,在同樣 8 秒內收到的回播只剩分房時的約九分之一。玩家看到的就是「別人卡住不動、自己按了沒反應」。
自己做一個會踩到哪些坑?
- 線上版目前只有離線示範。要變成真的多人同房,得把 presence server 部署出去。산사的前端連的是 Cloudflare Workers,我的
server.mjs是一般 Node.js 程序,要搬上去得改寫成 Worker 的寫法。 - 一房上限要先定。上限決定每個人要收多少資料,也決定單房最壞情況。산사最後選 100,我的伺服器預設也是 100。
- 每秒送幾次要先定。我用 10Hz,每人每秒動 4 次。坐著不動的人不進
dirty,所以全房都在打坐時流量很小;全部同時走動才是尖峰。 - WebGL 環境。我用 Playwright MCP 開的瀏覽器沒有 WebGL,畫面是黑的;改用 Node 版 Playwright 加 SwiftShader 才截得到 3D 畫面。
- 不要拿原站的素材。佛像、心經音檔、角色模型都是原作者的。我的版本全部用幾何體和 Web Audio 合成。
常見問題
為什麼不能聊天?
這是原作的設計,描述就寫「말없이 같이 앉아 있는」(不說話、一起坐著)。互動只有合掌、叩拜這類動作。
分房之後,朋友想坐同一間怎麼辦?
산사是讓使用者自己選頻道。我的伺服器在連線網址帶 ?room=3 就會進 3 號房,滿了才自動分到別間。
只靠 10Hz 批次廣播不分房夠嗎?
不夠。批次能把「包數」降下來,但每一包裡還是要放房裡所有在動的人,總資料量仍跟人數平方成正比。我的壓測就是開著批次在跑,全擠一間到 2000 人還是崩。
這組壓測數字可以當正式環境容量嗎?
不行。客戶端和伺服器跑在同一台 Windows 電腦,延遲裡也包含最多 100 ms 的 tick 等待。它能證明的是「同樣人數,分房比全擠一間穩」這個趨勢。
延伸資源
- 산사 原站:原作網站。
- 農民新聞報導:開發者메타몽、人數與座位演進。
- HuffPost Korea 報導:上線日期與爆紅背景。
- 線上禪堂:我做的繁中版,目前是離線示範。