手機上能放哪些 3D 功能,在動工前就能算出來。記憶體吃滿的後果不只是變慢,嚴重時 3D 場景會整個消失。所以先定上限,再把每個功能換算成多少 MB;加總超出的功能先降級,降級後仍超出的只給桌機。分級是算出來的,不是上線後看哪裡先崩再砍。
預算由業主先訂,開發團隊逐項換算
3D 網站開發的手機預算,要在專案初期就談定,像預算表一樣逐項分配。一個建案看屋網站的順序可以當例子:手機的 Chromium 模擬記憶體約 2.6 GB,第一輪降到約 0.9 GB,之後再往更低的整段流程高點收緊。後續新增的樹、夜景與漸進清晰,每一項都要算進這個數字。

逐項調整後,整段流程的量測值由約 910 至 960 MB 降到約 510 至 710 MB,尖峰約 745 MB。這些都是 Chromium 模擬手機的數字,量測雜訊約 ±20 至 40 MB。做法是下面這幾項逐項換算:
- 解碼:解碼 worker 每批載完就關,不常駐。
- 上傳後釋放:貼圖與幾何送進 GPU 之後,釋放記憶體裡的 CPU 端副本。
- 降階:手機的環境貼圖、陰影、水面鏡面與全景快取,各自降一階。
- 貼圖尺寸:由業主決定,手機用 1024 像素,桌機依裝置挑選。
- 幾何:手機端的幾何切成每塊不超過 65,536 個頂點,索引改用 16 位元,位置存成格點整數,法線存成 Int16。
幾何有個容易漏掉的細節:16 位元、3 個分量的屬性跨距是 6 byte,會被另轉一份,所以一律用 4 個分量。這類量化在 glTF 有正式擴充,Kapoulkine 等,2018–2020說明這是以較低精度換記憶體與頻寬,品質影響常見可忽略,但不保證,仍要回到畫面上確認。
各自沒事,合起來才爆
逐項換算不能只做加法。同一個建案網站裡,樹與夜景單獨打開時,記憶體增量都很小;同時打開,增量跳了一截。
| 打開的功能 | 流程中每一步增加的記憶體 |
|---|---|
| 只開樹 | 0 至 5 MB |
| 只開夜景 | 0 至 5 MB |
| 兩者同時開 | 約 40 MB(尖峰多約 45 MB) |
原因是行程的記憶體高水位,而不是兩個增量相加,所以每個功能單獨量過還不夠,還要和已經打開的功能同時再量一次。決策因此是:手機留樹,夜景城市只給桌機,業主同意。之後另做一版手機輕量夜景,只點亮塔樓,長流程的尖峰增量約為 0 MB。
- 單獨量一次與同一份 build 的關閉旗標相比,整段流程的高點多出幾 MB
- 與已打開的功能同時量各自沒事,不代表合起來沒事
- 對照預算上限合計高點在上限內就留,超出就往下分流
- 合計高點在上限內留在手機例如手機版的樹
- 超出上限,但降階後回到上限內降階後留下貼圖尺寸、陰影、鏡面與快取各降一階
- 降階後仍超出上限只給桌機例如夜景城市
- 功能的重點可以縮小改做手機輕量版例如只點亮塔樓的夜景
依建案看屋網站在 Chromium 模擬手機上的量測。
實例:淇豪廣告官網首頁的 3D 城市
淇豪廣告官網首頁的 3D 城市場景,原本有 8 張 4096 × 4096 的貼圖。換算只要一行:8 張 × 4096 × 4096 像素 × 每像素 4 byte × mipmap 的 4/3,約 683 MiB,與當時記下的約 681 MB 相符。貼圖降到 2048 × 2048 後,核算約 171 MiB,記下的是約 170 MB,下載量由 3.38 MB 降到 2.84 MB。
再往下一步,是把貼圖改成 GPU 壓縮格式,記憶體約 30 MB。格式怎麼選,GLB 檔案大小與貼圖有專文,這裡只看換算後的結果。KTX2 容器搭配 Basis Universal 的貼圖,由引擎轉碼成平台支援的區塊壓縮格式(Callow 等,2020),規格本身沒有給節省的數字,約 30 MB 是這個專案自己的結果。這個數字只回答記憶體,壓縮後的畫質要另外逐張確認。
- 約 681 MB原始 8 張起點4096
- 約 170 MB邊長降為 20482048
- 約 30 MB再改 GPU 壓縮格式定案2048
淇豪廣告官網首頁 3D 城市,8 張貼圖。記憶體為核算與記錄值;不含壓縮後的畫質評估。
進場前先預編譯 shader 與貼圖,進入城市時才不會在第一幀卡住。城市完全就緒之後,介面才進場,不是一邊載一邊跳。3D 載入失敗時,頁面退成靜態封面,首屏下方另有伺服器端渲染的內容區塊,讓搜尋引擎讀得到。
3D 首頁有沒有拖慢網站,可以對照 LCP:首屏中最大的影像、文字區塊或影片的繪製時間,良好門檻是 2.5 秒以內(Walton 等,2019)。
大面積地面貼皮會吃掉整個預算
地面是最容易吃掉預算的單一項目。一張 4096 的未壓縮貼圖,解到 GPU 約 85 MB,已經超過手機低階一級 64 MB 的整個貼圖預算。地面貼圖一直在畫面上,屬於使用中的貼圖,快取清掃清不掉,是 iOS 記憶體不足的直接風險。改用 GPU 壓縮格式後,4096 約 21 MB,2048 約 5 MB,所以桌機用 4096,其餘用 2048。
第二個地雷是重複。烘成一張的地面有物理上限:攤成 4096 的貼圖,每像素約 2.6 公分,5 公分的碎石只有約 2 個像素。要近看細節只有兩條路:執行期疊一張可平鋪的細節法線,1024 的圖平鋪 40 次只要約 1.3 MB;或改用整包 GLB 環境,但手機容易記憶體不足。
另一個戶外場景的地面約 60 公尺見方,1024 的底圖每 3 公尺左右重複一次,車身才約 3.6 公尺,俯視就看到格線。解析度沒有問題,所以不換資產:同一張圖用另一個比例再取樣,以低頻雜訊混合,並用同一個雜訊微調明度與粗糙度。成本是多一次貼圖取樣。
長時間操作要管 GPU 記憶體
第一次載入通過,不代表用久了不會崩。3D 配置器的使用者不斷換款式與顏色,場景可能在長時間操作後整個消失,原因是 GPU 記憶體累積造成 WebGL context lost。先量哪些有界:換款式多半只切可見度、不載新檔,來回切換 60 次,幾何數在約翻倍後打平。顏色貼圖的快取卻只進不出,換 12 種顏色,貼圖數單調上升,快取由約 50 MB 長到約 290 MB。
解法是依裝置分級的預算,加上標記後清掃。低、中、高三級的上限分別是 64 MB、128 MB 與 256 MB,超過才動手,只驅逐不在使用中、且超過 30 秒保活期的貼圖,一路清到上限的 80%。全部被保活擋住時,30 秒後重試,最多 2 次。觸發點是快取長大後 2.5 秒與分頁隱藏,不設常駐輪詢,閒置時什麼都不做。
標記要涵蓋主場景(含被隱藏的部件)與快取部件的所有貼圖欄位、shader 的 uniform(淡入用的舊色、閃片),以及暫存在 userData 的貼圖(切換材質後要還原的粗糙度圖)。只掃 material.map,會把淡入的舊色和閃片掃掉,畫面白閃。另一個要點:context lost 的失效計數器不能只增不減,否則累到 3 次就永久白畫面;存活 30 秒就該清零。兩次清掃後,快取回到約 200 MB。這個數字是頁面內統計。
載入慢,不一定是檔案大
大型場景網站的載入可能卡在進度條中段。以桌機實測 45 秒的載入為例,約 20 秒花在同步等 shader 編譯,約 14 秒花在預熱剖切版本。MDN 文件的建議是先把全部著色器編譯完再連結,並用 KHR_parallel_shader_compile 避免阻塞(MDN Web Docs,2026),這份文件只有原則,沒有實測數字。
修法有兩個。剖切面常駐:編譯前先掛上三個面,不用時停在極遠處,開關只改常數,讓每個材質只有一個 program。正式版關掉 shader 錯誤檢查,平行編譯才真的平行。結果是桌機載入由 45 秒降到 8 秒。
有兩件事不能做。開樓層時換剖切面陣列,因為面數是 program 的 key,換一次就全場重編。日夜兩張環境貼圖的尺寸也必須一致,否則切換時全場重編,實測凍結 1.3 至 3 秒。
順不順,不只看記憶體
記憶體在預算內,畫面仍可能不順。互動回應的通用指標是 INP,良好是 200 毫秒以下,500 毫秒以上為差(Wagner 等,2022)。以地圖為主畫面的互動網站,縮放時卡頓,主因常不在 3D 繪圖,而在 DOM 與樣式。用追蹤工具把卡頓的幀拆成 JavaScript、樣式重算、排版與繪製,可以抓到兩個根因。
其一,地名淡出把一個 CSS 自訂屬性寫在父層。屬性一變,整棵子樹的地名與地標都要重算樣式,淡出那一段每幀要 10 至 15 毫秒。改成每個地名只寫自己的 opacity,這是非繼承屬性。其二,每幀讀 clientWidth 與 getBoundingClientRect,會逼瀏覽器先排版這一幀前面寫的標籤位置。只改一處,成本會換成下一個讀的人吃下,所以三個模組全改用 ResizeObserver 快取的畫面大小,可見區每 8 幀量一次。
在 CPU 降速 4 倍的模擬手機上,交錯 A/B、開垂直同步,縮放時超過一幀預算的格由 2.4% 降到 0.5%。規則也寫成全站準則:隨縮放連續變的視覺,只寫在要動的那個元素自己的非繼承屬性上。

有些手機問題在桌機模擬器裡永遠重現不了。例如手機版的底部抽屜,滑到最底再往上滑會失靈,而且手指放在文字上才會中,放在空白處正常。原因是 iOS WebKit 把 touch-action 算進畫圖時的事件區域,動態切換之後,文字那一塊的區域沒有更新,仍是 pan-x,直向手勢被擋。桌機 Chrome 與 Playwright 的 WebKit 沒有 iOS 的事件區域,所以在那裡量不到。
做法是先寫一支量測腳本,把行為固定下來當防回歸,再在真機上逐條排除,並列出已查過、不是原因的清單避免重查。修法改成半高時對 touchmove 呼叫 preventDefault,不再動態切換 touch-action,這一條也寫進了專案規則。
畫面由好幾塊非同步資料組成時,每塊各自到達,就會把下面的內容往下推。做法是內容先藏著,各元件照樣載入並回報「定了」,全部定了才淡入,最多等 4 秒,代價是顯示時間由最慢的一塊決定。這類頁面的 CLS 可能落在 0.15 至 0.45;CLS 是非預期版面位移中最大一波位移分數的總和,良好是 0.1 以下,高於 0.25 為差(Mihajlija 等,web.dev)。每頁的通過門檻訂在 0.05,比通用的良好門檻更嚴,並用腳本在慢速網路下逐頁列出每一次位移與來源區塊。
怎麼驗,以及量測會踩到的坑
預算要能驗證才算數。量測誤差怎麼控制,見3D 效能量測;整體流程與驗收,見Web 3D 效能工程。與記憶體預算直接相關的做法與陷阱如下:
- 基準用同一份 build 的關閉旗標,不用線上版,單項增量才不會混進其餘改動。
- 先量雜訊。建案網站的量測雜訊約 ±20 至 40 MB,尖峰約 ±40 MB,小於雜訊的差異不下結論。
- 模擬不等於實機。本文的記憶體數字出自 Chromium 模擬或頁面內統計;iOS 的行為只能上真機確認。
- 幀率要在前景分頁量。多數瀏覽器會暫停背景分頁的 requestAnimationFrame 回呼(MDN Web Docs,2026),據此推論,背景分頁量到的幀率不可信。
這份預算放進一條完整的看屋路徑是什麼樣子,見建案 3D 看屋的路徑。
不適合的情況
- 手機不是主要入口:手機端只要確認 3D 載入失敗時會退成靜態封面,不必逐項換算整套預算。
- 沒有人能拍板預算:每一次降階都是畫質、功能與記憶體的取捨,必須由業主決定,開發團隊只負責換算與量測。
- 沒有固定的量測基準:不能用同一份 build 開關單一功能做對照時,單項增量會被雜訊蓋掉,換算無從做起。
- 問題其實是互動卡頓:記憶體在預算內卻不順,要先拆幀成本,而不是繼續砍記憶體。
下一步:先備齊這幾項
- 要支援的最低手機機型與瀏覽器範圍。
- 要放進手機的功能清單,依重要性排序。
- 現有的模型與貼圖,或其來源。
- 能拍板記憶體上限與畫質取捨的人。
備齊後,可以聯絡開發團隊,談手機 3D 的範圍,從第一個功能開始逐項換算。




