我的 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。
所以現在該做什麼
這篇是量測,不是修好。我還沒把壓縮接進正式的載入流程——那會動到兩個 3D 場景的初始化程式碼,而且 Draco 和 meshopt 該選哪一個,還牽涉到我願不願意為了兩頁的差異各帶一個解碼器。
但數字已經很清楚了:
| 頁面 | 現在 | 壓縮後 | 減少 |
|---|---|---|---|
| 3D Studio | 2,927 KB | 約 734 KB | −75% |
| Midnight Run | 1,714 KB | 約 541 KB | −68% |
在慢速行動網路上,這是好幾秒的差別。而且是使用者在看到任何東西之前的那幾秒。
如果你也要量自己的 3D 網頁
照這個順序,每一步都比前一步貴,所以做到夠用就停:
一、先看 DevTools 的網路面板,比對「傳輸量」和「大小」兩欄。如果兩欄一樣,代表那個檔案沒有被壓縮,先去查它的 content-type。
二、確認你的 CDN 有沒有壓縮那個 content-type。這是唯一一個零客戶端成本的優化,能做就一定要做。
三、量一次 quantize。它不需要任何解碼器,對浮點數過剩的場景效果驚人。
四、再考慮 meshopt 或 Draco,而且一定要把解碼器算進總量。壓縮率最高的方案不一定是傳輸量最小的方案。
五、貼圖分開處理。WebP 幾乎是免費的六成。
整個過程我用的是 gltf-transform,一個指令跑一種方案,比想像中快得多。真正花時間的不是壓縮,是願意先去看一眼那兩欄數字。
文中量測的場景:Midnight Run