我的 3D 網頁傳了 1.9MB,其中 90% 從來沒有被壓縮過
我一直知道 3D 網頁很重,但沒有真的去量過。
直到有一天我打開 DevTools 的網路面板,看 Midnight Run 到底傳了什麼。
| 資源 | 傳輸量 | 磁碟大小 |
|---|---|---|
| xinyi-night-loop-city.glb | 1,329 KB | 1,329 KB |
| midnight-assets.glb | 385 KB | 385 KB |
| three.module.js | 142 KB | 564 KB |
| MidnightRun.js | 20 KB | 64 KB |
| GLTFLoader.js | 14 KB | 44 KB |
| 合計 | 1,894 KB |
兩個 GLB 加起來 1,714KB,佔整頁的 90%。
但真正讓我停下來的不是那個 90%,是右邊那一欄。
JS 被壓了四倍,GLB 一個位元組都沒少
three.js 在磁碟上是 564KB,實際只傳了 142KB——伺服器把它壓掉了四分之三。GLTFLoader 也一樣,44KB 傳了 14KB。
但那兩個 GLB,磁碟多大就傳多大。
我去看了線上的 response header,答案很直接:
| 資源 | content-type | content-encoding |
|---|---|---|
| index.js | application/javascript | br |
| kof-3d-studio.glb | model/gltf-binary | (無) |
Cloudflare 是依 content-type 的白名單決定要不要壓縮的。application/javascript 在名單裡,model/gltf-binary 不在。
所以整個網站最大的那個檔案,是唯一一個完全沒有享受到壓縮的檔案。而且這件事不會有任何警告——它安靜地存在了好幾個月。
先問最便宜的問題:如果壓了會怎樣?
在研究 Draco 那些東西之前,我想先知道最單純的壓縮能做到什麼程度。所以我直接對兩個 GLB 跑 gzip 和 brotli:
| 檔案 | 原始 | gzip | brotli |
|---|---|---|---|
| kof-3d-studio.glb | 2,926 KB | 407 KB | 293 KB(−90%) |
| xinyi-night-loop-city.glb | 1,329 KB | 613 KB | 567 KB(−57%) |
3D Studio 那個場景,光是 brotli 就砍掉九成。
這個數字大得有點荒謬,但它有道理:未壓縮的 GLB 裡放的是一大片 32 位元浮點數,相鄰的頂點座標高度相似,通用壓縮器最擅長吃這種東西。
問題是,在 Cloudflare Pages 上我沒辦法叫它壓這個 content-type。決定權不在我手上。
所以只剩一條路:把壓縮烤進檔案本身。
三種把壓縮烤進 GLB 的方式
glTF 生態系有三個常見選項,我用 gltf-transform 各跑一次:
| 方案 | 3D Studio | Midnight Run 城市 |
|---|---|---|
| 原始 | 2,926 KB | 1,329 KB |
| quantize(降精度) | 993 KB(−66%) | 1,102 KB(−17%) |
| meshopt | 675 KB(−77%) | 658 KB(−50%) |
| Draco | 489 KB(−83%) | 437 KB(−67%) |
Draco 在兩邊都贏。如果只看這張表,結論很明顯:用 Draco。
但這張表少算了一件事。
解碼器不是免費的
壓過的檔案瀏覽器看不懂,要先載入解碼器。而這兩個方案的解碼器差很多:
| 解碼器 | 大小 |
|---|---|
| Draco(wasm + wrapper) | 245 KB |
| meshopt | 28 KB |
Draco 的解碼器比 meshopt 大了將近九倍。把它加回去之後,答案就翻轉了:
| 頁面 | 方案 | 模型 | 解碼器 | 總計 |
|---|---|---|---|---|
| 3D Studio | 原始 | 2,927 KB | — | 2,927 KB |
| Draco | 489 KB | 245 KB | 734 KB | |
| meshopt | 675 KB | 28 KB | 704 KB | |
| Midnight Run | 原始 | 1,714 KB | — | 1,714 KB |
| Draco | 505 KB | 245 KB | 750 KB | |
| meshopt | 771 KB | 28 KB | 799 KB |
3D Studio 那一頁,meshopt 反而比 Draco 少 30KB——雖然它的壓縮率明顯比較差。
Midnight Run 則相反,Draco 贏 49KB。差別在於這一頁有兩個模型、幾何量比較大,足以把那 245KB 的解碼器攤平。
換算成一句可以帶走的規則:Draco 要贏,它省下的必須超過 meshopt 大約 216KB。低於這個數,那顆比較大的解碼器就把優勢吃光了。
而且解碼器會被瀏覽器快取,所以「使用者會看幾頁 3D」也是變數。單一頁面的一次性造訪,和一個有五個 3D 場景的網站,最佳解不會一樣。
貼圖是另一半的故事
Draco 和 meshopt 都只壓幾何,不碰貼圖。信義區那個城市檔案裡,貼圖佔了 25%:
| 項目 | JPEG | WebP |
|---|---|---|
| 內嵌貼圖合計 | 334 KB | 125 KB(−63%) |
| 佔整個 GLB 比例 | 25% | 11% |
幾何和貼圖各壓各的,兩個一起做才是完整答案:
| 處理 | 大小 |
|---|---|
| 原始 | 1,329 KB |
| Draco | 437 KB |
| Draco + WebP | 228 KB(−83%) |
一個我沒預期的發現:那 2.9MB 裡沒有一張貼圖
量到一半我才發現,3D Studio 的 2,926KB 裡,貼圖數量是零。
整個檔案是純粹的幾何——85,976 個頂點,位置、法線、UV 全部用 32 位元浮點數存。
這解釋了為什麼 brotli 對它特別有效,也解釋了為什麼 quantize 就能砍掉三分之二:那些浮點數根本不需要那麼高的精度。一個 11 公尺寬的場景,位置精度到小數點後七位是沒有意義的。
如果我當初有量過,這個場景從一開始就不會是 2.9MB。
所以我把它做完了
量完之後我選了 meshopt,然後把三個 GLB 全部重新壓過。
選 meshopt 不是因為它壓得比較小——Draco 在 Midnight Run 那頁還是贏 49KB。是因為它的解碼器是一個可以直接打包進 bundle 的 JS 模組,不像 Draco 需要另外部署 WASM 檔案並設定 decoder path。少一個部署環節、少一個可能失敗的網路請求,而且解碼快得多——對一個遊戲頁面來說,這比那 49KB 值錢。
壓縮做成一個可以重跑的腳本,貼圖轉 WebP、幾何用 meshopt,並且會偵測已經壓過的檔案直接跳過,所以重跑不會把編碼疊起來。
npm run optimize:3d
OPTIMIZED public/lab/3d-studio/models/kof-3d-studio.glb 2996752 -> 691620 bytes (-77%)
OPTIMIZED public/games/midnight-run/models/xinyi-night-loop-city.glb 1360852 -> 460520 bytes (-67%)
OPTIMIZED public/games/midnight-run/models/midnight-assets.glb 393920 -> 114764 bytes (-71%)載入端只改了一行:三個 new GLTFLoader() 換成一個共用的 createGltfLoader(),裡面掛上 MeshoptDecoder。壓過的檔案沒有解碼器會直接拒絕載入,所以把這個配對放在同一個地方,之後不會有人漏掉。
實際結果:
| 頁面 | 壓縮前 | 壓縮後 | 減少 |
|---|---|---|---|
| 3D Studio | 2,926 KB | 701 KB | −76% |
| Midnight Run | 1,714 KB | 588 KB | −66% |
壓縮後的數字包含了 26KB 的解碼器。兩個場景我都重新跑過一次確認畫面正常——285 個 mesh 的工作室場景、還有信義區的城市與車輛,全部照舊。
整個實作花的時間比量測還少。難的從來不是壓縮,是先去看那兩欄數字。
如果你也要量自己的 3D 網頁
照這個順序,每一步都比前一步貴,所以做到夠用就停:
一、先看 DevTools 的網路面板,比對「傳輸量」和「大小」兩欄。如果兩欄一樣,代表那個檔案沒有被壓縮,先去查它的 content-type。
二、確認你的 CDN 有沒有壓縮那個 content-type。這是唯一一個零客戶端成本的優化,能做就一定要做。
三、量一次 quantize。它不需要任何解碼器,對浮點數過剩的場景效果驚人。
四、再考慮 meshopt 或 Draco,而且一定要把解碼器算進總量。壓縮率最高的方案不一定是傳輸量最小的方案。
五、貼圖分開處理。WebP 幾乎是免費的六成。
整個過程我用的是 gltf-transform,一個指令跑一種方案,比想像中快得多。真正花時間的不是壓縮,是願意先去看一眼那兩欄數字。
文中量測的場景:Midnight Run