Three.jsWeb 3DPerformanceGLBBlender3D Pipeline

我的 3D 網頁傳了 1.9MB,其中 90% 從來沒有被壓縮過

·閱讀約 5 分鐘

我一直知道 3D 網頁很重,但沒有真的去量過。

直到有一天我打開 DevTools 的網路面板,看 Midnight Run 到底傳了什麼。

資源傳輸量磁碟大小
xinyi-night-loop-city.glb1,329 KB1,329 KB
midnight-assets.glb385 KB385 KB
three.module.js142 KB564 KB
MidnightRun.js20 KB64 KB
GLTFLoader.js14 KB44 KB
合計1,894 KB
Midnight Run 首次載入的實際傳輸量(瀏覽器實測)

兩個 GLB 加起來 1,714KB,佔整頁的 90%。

但真正讓我停下來的不是那個 90%,是右邊那一欄。

JS 被壓了四倍,GLB 一個位元組都沒少

three.js 在磁碟上是 564KB,實際只傳了 142KB——伺服器把它壓掉了四分之三。GLTFLoader 也一樣,44KB 傳了 14KB。

但那兩個 GLB,磁碟多大就傳多大。

我去看了線上的 response header,答案很直接:

資源content-typecontent-encoding
index.jsapplication/javascriptbr
kof-3d-studio.glbmodel/gltf-binary(無)
同一個站上,兩種資源的壓縮待遇

Cloudflare 是依 content-type 的白名單決定要不要壓縮的。application/javascript 在名單裡,model/gltf-binary 不在。

所以整個網站最大的那個檔案,是唯一一個完全沒有享受到壓縮的檔案。而且這件事不會有任何警告——它安靜地存在了好幾個月。

先問最便宜的問題:如果壓了會怎樣?

在研究 Draco 那些東西之前,我想先知道最單純的壓縮能做到什麼程度。所以我直接對兩個 GLB 跑 gzip 和 brotli:

檔案原始gzipbrotli
kof-3d-studio.glb2,926 KB407 KB293 KB(−90%)
xinyi-night-loop-city.glb1,329 KB613 KB567 KB(−57%)
純粹的通用壓縮,不改檔案內容

3D Studio 那個場景,光是 brotli 就砍掉九成。

這個數字大得有點荒謬,但它有道理:未壓縮的 GLB 裡放的是一大片 32 位元浮點數,相鄰的頂點座標高度相似,通用壓縮器最擅長吃這種東西。

問題是,在 Cloudflare Pages 上我沒辦法叫它壓這個 content-type。決定權不在我手上。

所以只剩一條路:把壓縮烤進檔案本身。

三種把壓縮烤進 GLB 的方式

glTF 生態系有三個常見選項,我用 gltf-transform 各跑一次:

方案3D StudioMidnight Run 城市
原始2,926 KB1,329 KB
quantize(降精度)993 KB(−66%)1,102 KB(−17%)
meshopt675 KB(−77%)658 KB(−50%)
Draco489 KB(−83%)437 KB(−67%)
同樣的模型,三種壓縮方式

Draco 在兩邊都贏。如果只看這張表,結論很明顯:用 Draco。

但這張表少算了一件事。

解碼器不是免費的

壓過的檔案瀏覽器看不懂,要先載入解碼器。而這兩個方案的解碼器差很多:

解碼器大小
Draco(wasm + wrapper)245 KB
meshopt28 KB
three.js 內附的解碼器大小

Draco 的解碼器比 meshopt 大了將近九倍。把它加回去之後,答案就翻轉了:

頁面方案模型解碼器總計
3D Studio原始2,927 KB2,927 KB
Draco489 KB245 KB734 KB
meshopt675 KB28 KB704 KB
Midnight Run原始1,714 KB1,714 KB
Draco505 KB245 KB750 KB
meshopt771 KB28 KB799 KB
首次載入的真實總量(模型 + 解碼器)

3D Studio 那一頁,meshopt 反而比 Draco 少 30KB——雖然它的壓縮率明顯比較差。

Midnight Run 則相反,Draco 贏 49KB。差別在於這一頁有兩個模型、幾何量比較大,足以把那 245KB 的解碼器攤平。

換算成一句可以帶走的規則:Draco 要贏,它省下的必須超過 meshopt 大約 216KB。低於這個數,那顆比較大的解碼器就把優勢吃光了。

而且解碼器會被瀏覽器快取,所以「使用者會看幾頁 3D」也是變數。單一頁面的一次性造訪,和一個有五個 3D 場景的網站,最佳解不會一樣。

貼圖是另一半的故事

Draco 和 meshopt 都只壓幾何,不碰貼圖。信義區那個城市檔案裡,貼圖佔了 25%:

項目JPEGWebP
內嵌貼圖合計334 KB125 KB(−63%)
佔整個 GLB 比例25%11%
GLB 內嵌貼圖轉成 WebP

幾何和貼圖各壓各的,兩個一起做才是完整答案:

處理大小
原始1,329 KB
Draco437 KB
Draco + WebP228 KB(−83%)
信義區城市:疊加所有手段

一個我沒預期的發現:那 2.9MB 裡沒有一張貼圖

量到一半我才發現,3D Studio 的 2,926KB 裡,貼圖數量是零。

整個檔案是純粹的幾何——85,976 個頂點,位置、法線、UV 全部用 32 位元浮點數存。

這解釋了為什麼 brotli 對它特別有效,也解釋了為什麼 quantize 就能砍掉三分之二:那些浮點數根本不需要那麼高的精度。一個 11 公尺寬的場景,位置精度到小數點後七位是沒有意義的。

如果我當初有量過,這個場景從一開始就不會是 2.9MB。

所以現在該做什麼

這篇是量測,不是修好。我還沒把壓縮接進正式的載入流程——那會動到兩個 3D 場景的初始化程式碼,而且 Draco 和 meshopt 該選哪一個,還牽涉到我願不願意為了兩頁的差異各帶一個解碼器。

但數字已經很清楚了:

頁面現在壓縮後減少
3D Studio2,927 KB約 734 KB−75%
Midnight Run1,714 KB約 541 KB−68%
如果全部做完

在慢速行動網路上,這是好幾秒的差別。而且是使用者在看到任何東西之前的那幾秒。

如果你也要量自己的 3D 網頁

照這個順序,每一步都比前一步貴,所以做到夠用就停:

一、先看 DevTools 的網路面板,比對「傳輸量」和「大小」兩欄。如果兩欄一樣,代表那個檔案沒有被壓縮,先去查它的 content-type。

二、確認你的 CDN 有沒有壓縮那個 content-type。這是唯一一個零客戶端成本的優化,能做就一定要做。

三、量一次 quantize。它不需要任何解碼器,對浮點數過剩的場景效果驚人。

四、再考慮 meshopt 或 Draco,而且一定要把解碼器算進總量。壓縮率最高的方案不一定是傳輸量最小的方案。

五、貼圖分開處理。WebP 幾乎是免費的六成。

整個過程我用的是 gltf-transform,一個指令跑一種方案,比想像中快得多。真正花時間的不是壓縮,是願意先去看一眼那兩欄數字。

文中量測的場景:Midnight Run

參考資料