板子效能現況 · 2026-08-04

28.1 毫秒裡,有 11.1 毫秒在 three.js

要跑到 58fps,每一幀只能用 17.2 毫秒。JS 主執行緒自己就吃掉 23.4 毫秒 —— 已經超過整個預算。而那裡面有將近一半是 three.js,我們自己寫的遊戲碼只佔 4 毫秒。

rk3576_u · Mali-G52 · 1920×1080 · WebView 116 開發版打包,靜態伺服 + adb reverse 場上殭屍固定 26 隻
一句話

瓶頸是 CPU 上的 JavaScript,不是 GPU、不是三角形、不是遊戲邏輯。而 JS 裡的錢幾乎都付給了框架與繪圖驅動:three.js 11.1ms + 原生 GL 5.6ms=16.7ms,正好等於整個 58fps 的預算。我們自己的遊戲碼只有 4ms —— 就算把遊戲碼全部刪光也達不到目標。

目前每幀
28.1ms
35.6 fps
目標每幀
17.2ms
58 fps(板子天花板 57–59)
還要砍掉
10.9ms
整幀的 39%
目前握有
2.5ms
批 1 已完成 0.58 + 姿勢上界 1.90
three.js 佔 JS
47.6%
11.1ms / 23.4ms
一、幀預算

28.1 毫秒花在哪

下面這條就是一幀。紅線是 58fps 的預算線 —— 線右邊的全部都要消失

three.js 11.11
原生/GL 5.62
遊戲碼 4.30
UI 1.3
套件 1.1
非 JS 4.7
58fps 預算線 · 17.2ms
0 ms28.1 ms
three.js(含 WebGLRenderer) 原生 / GL driver 我們的遊戲碼 React / UI instanced-mesh 套件 非 JS(GPU、合成、等 vsync)
二、直接量測

CPU profile 的分佈

這一節不是開關差分推的,是直接抓 CPU profile、再用 sourcemap 還原到真實原始檔得到的。表格互不重疊,加起來就是 JS 總量。

來源每幀 ms佔 JS是什麼
three.js11.1147.6%WebGLRenderer、場景圖矩陣、動畫插值
原生 / GL driver5.6224.1%貼圖上傳、buffer 寫入、狀態綁定、編譯
React / UI1.255.3%HUD 元件(含開發模式專用開銷)
渲染 Adapters1.124.8%殭屍 0.71、道具 0.33、其餘零星
instanced-mesh 套件1.084.6%逐 instance 矩陣與骨骼上傳
遊戲邏輯 / Engine1.054.5%AI、物理、生成、迴圈
Canvas 2D 疊層0.954.0%Overlay/ 那四個檔
其他 RenderSystem + legacy + 引擎工具1.185.1%
JS 主執行緒合計23.36100%整幀 28.1ms,其餘 ~4.7ms 不是 JS

three.js 那 11.1 毫秒的內部

分區每幀 ms主要函式
WebGLRenderer 與其他5.20renderBufferDirect · setProgram · 各種綁定
場景圖:矩陣更新與相乘3.23updateMatrixWorld 2.19 · multiplyMatrices 0.53
動畫:Mixer 與關鍵影格插值2.68update 1.21 · evaluate 1.08 · slerpFlat 0.39

後兩列合計 5.91ms,全部由殭屍的逐隻骨骼更新驅動 —— 每隻怪每幀各跑一次 mixer.update()updateMatrixWorld()這是整份報告裡最大的可處理區塊。

🔴 這一節的前一版是錯的,錯法值得記下來

第一版我照打包產物的 chunk 名字分類,得到「Canvas 2D 疊層 5.70ms、第二大」。那是錯的 —— 真正的數字是 0.95ms,差了六倍。

原因:WebGL2DContext-*.js 這個 chunk 的名字只是打包器挑的進入點檔名,裡面實際裝著 three.js 的 WebGLRenderer(用字串常數驗證:SHADOWMAP_TYPE_PCFPMREMACES_FILMIC 全部只出現在這個 chunk 裡)。我把框架的成本記到了疊層頭上。

修法是開 sourcemap 重新打包、把每個取樣點還原回原始檔,也就是現在這張表。教訓:chunk 名字不是模組歸屬。

二之二、穩不穩

不穩,而且不穩的方式最難受

板子上連續擷取 60 秒、2097 幀的每一幀耗時(一般遊玩,不是量測場景)。平均 34.9 fps —— 但平均值在這裡沒有意義:

16.7ms=60fps 33.3ms=30fps 連續 240 幀(約 8 秒)的實際每幀耗時
每一幀落在哪一格幀數比例玩起來是
1 個 vsync(≤20ms)89442.6%那一幀是 60fps
2 個 vsync(20–37ms)103849.5%那一幀是 30fps
3 個 vsync(37–54ms)1024.9%明顯頓一下
4 個以上(>54ms)633.0%
🔴 這就是「感覺卡」的來源,不是平均 fps 太低

幀長幾乎五五對分在 60fps 與 30fps 兩格之間。畫面每一幀都在這兩個速度之間跳,而人眼對「忽快忽慢」比對「一直慢」敏感得多 —— 穩定的 30fps 會比現在這種 34.9 fps 好看。

另外還有 5.05% 的幀超過 50ms(106 次/60 秒),最差單幀 466.7ms(將近半秒)。那是實實在在的頓挫。

每幀耗時p50p95p99最差
毫秒33.350.083.3466.7
⚠️ 一個目前做不到的事:遊戲裡看不到走勢

開發版左上角只有一個 FPS 數字。單一數字看不出上面這種「五五對分」的抖動 —— 它會在 30 和 60 之間跳,或顯示一個沒有意義的平均。即時走勢圖已經排進待做。

三、子系統

各開關實際關掉之後值多少

這一節是交錯 ABAB 開關量的,條件與上一節不同(較早的版本、殭屍 38 隻、開發伺服器)。只能拿來排序,不能跟上面的 ms 相加。

子系統關掉值多少判定
殭屍(全部)+23.7 fps最大 但全部拿掉也只有 9.6ms,光靠它到不了 58
陀螺+4.8 fps第二大,但動它會直接影響玩法主體
背景+2.5 fps可考慮
UI Canvas+2.2 fps比疊層本身的 0.95ms 還大,因為這個開關也擋掉了它引發的 three.js 繪製
殭屍描邊+1.7 fps幅度散
特效 / 票券 / 障礙 / 發射台 / 陀螺描邊各 ≤1.0 fps全部加起來也補不上缺口
影子同步+0.3 fps不用碰
四、已排除

這四個方向查過了,不要再花時間

不是 GPU 填色率
畫面像素砍到四分之一,只換到 +1.5 fps。GPU 有餘力。
不是三角形太多
少畫 10 萬個三角形只值 +5 fps;而少一個場景物件就值 +4.5 fps。
不是遊戲邏輯
AI、物理、碰撞全部暫停,只留繪製 —— 幾乎零差異。
不是怪太多
只畫 24 / 12 / 4 隻(邏輯照跑全部),fps 幾乎沒動。
五、已完成

批 1:描邊併進同一個物件

殭屍描邊原本是另一組獨立的渲染物件,帶著自己的矩陣與骨骼貼圖 —— 完整第二份上傳。這批把它併成同一個物件的第二個材質群組,場上殭屍相關物件從 14 個降到 6 個。

實測效益
+0.58ms
+0.93 fps
量測
8
交錯 ABAB,7/8 對正向
95% 區間
+0.4~1.5
fps
跨廠牌審查
2
三項必修全修
🔴 這裡有一個我先前報錯的數字

第一輪只跑 4 對,量到 +1.70ms,我就報出去了。加到 8 對之後收斂到 +0.58ms

原因是同一份程式碼、不同時段跑,段與段之間就會差到 1.95 fps —— 雜訊跟訊號一樣大。教訓:這塊板子上任何小於 2 fps 的宣稱,需要 8 對以上才作數。

但這批問出一件更有用的事

對照兩個數字:把物件整個關掉不畫,一個約值 0.4 fps;只是把物件併起來、繪製照舊,一個只值 0.1 fps。

⇒ 真正貴的是「畫」,不是「管理」。先前把「少一個物件」講成單一原因是不夠精確的 —— 裡面有兩塊,而這批只吃到便宜的那塊。

五之二、試過但量不出來

三個負面結果

這一節的東西都做出來了、也都在板子上量了,結果是量不到。負面結果跟正面結果一樣要留紀錄,否則下一個人會再做一次。

做了什麼實測判定
姿勢共用池
同型別、同動作、同時間點的怪只算一次姿勢
+0.55 fps
區間 −0.08~+1.17
跨過 0 姿勢計算確實從 26 次降到 16 次(−38%),但換不到可量到的時間
動畫時間量化
把動畫時間對齊格子以提高共用率
26→15 次
(15Hz)
沒有價值 不量化就已經 26→16;15Hz 只多省 1 次、12Hz 多省 2 次。不必為它付畫面代價
整個關掉描邊
用現成開關直接量上界
~+0.9 fps上界很低 三角形 −28%、draw call −6,fps 幾乎沒動。「改描邊做法」這條線比先前以為的低很多
🔴 三個負面結果合起來指向同一件事:尺不夠準

每一個改動的效果都在 0.5~1 fps,而同一份程式碼、不同時段跑,段間差可達 2.7 fps(標準差 0.83)。

量測誤差比被量的東西還大 ⇒ 每一批的結論都會是「可能有、可能沒有」,而決定會建立在猜測上。

所以先修尺:固定場景

雜訊的主要來源找到了 —— 怪的數量在 21~38 之間自由變動,而 fps 對怪數極度敏感。做了一個只存在於開發版的量測場景(怪數、種類、位置全部固定,不生成也不死亡),在板子上量:

同一塊板子、同一份 build、各 6 段 × 20 秒段間標準差最大差怪數draw call
一般遊玩2.1146.10 fps26 / 37 / 3864–83
固定場景0.9732.32 fps26(唯一)59–65

標準差砍半、最大差砍到三分之一。但 0.97 仍然和要量的效果同一量級,而且 draw call 還在 59–65 之間跳 —— 還有東西沒釘住,正在找。

下一步改用兩把尺

工作量計數器(呼叫了幾次動畫更新、幾次矩陣重算、幾次骨骼上傳)驗「這個改動有沒有做到它宣稱的事」—— 這是確定性的、零雜訊

fps 只用來驗「好幾個改動疊起來值不值得」,不再對單一改動下 fps 結論

五之三、目前唯一確定為正的方向

姿勢那條線值 1.90 毫秒

前一節的三個負面結果讓人以為「殭屍骨骼」這條路是死的。不是 —— 在固定場景裡把共用力道開到極限之後,效果非常明確。

固定場景 26 隻、4 輪交錯、每段 20 秒目前設定開到極限
實際計算的姿勢數22 / 266 / 26−16
矩陣重算次數20491295−37%
動畫更新次數226−16
fps39.9443.22+3.27
逐輪差 +1.37 / +4.68 / +3.69 / +3.37 —— 4/4 同向= 1.90 ms/幀
為什麼先前量不到

批 3 的預設設定幾乎沒有共用到(26 隻裡只共用掉 4 隻)。不是這條路沒價值,是力道沒開。

同時這個實驗也否證了一個先前的判斷 —— 有人推測「矩陣重算主要來自繪圖框架每幀對整個場景的那一次,跟我們算幾次姿勢無關」。實測是姿勢少算 14 次、矩陣重算就少 652 次,關係非常直接(每次遞迴約 47 個節點,剛好是一副骨架的量級)。

但那個「極限」不能直接出貨 —— 正在做的是另一個做法

上面的極限是靠「把動畫時間對齊到 1 秒的格子」逼出來的,那會讓動畫一頓一頓。

正在做的替代方案是相位分桶:現在每隻怪的動畫起始點都不一樣(用 id 算的),所以永遠湊不成一組;改成出生時分到 K 個相位桶其中一個,16 隻小怪在 K=6 時就變成最多 6 組。

這個取捨要看畫面才能決定

好處:時間仍然連續前進,動畫不會一頓一頓代價:同一桶的怪會同手同腳。K 越小省越多,也越明顯。

靜態截圖看不出「一群怪同步走路」,所以驗收材料包含連續幀格狀圖。這是產品判斷,不是工程判斷。

六、評估後不做

VAT(把骨骼動作預先烘成貼圖)

問題調查結果
全部 10 種怪都烘不可行 需要 121.6 MB 常駐記憶體(含法線 243 MB),板子吃不下;其中 4 種頂點數還超過貼圖上限
只做一種怪(man,場上約 16 隻)技術可行,貼圖 1.72 MB
能拿回多少上界約 2ms(+3 fps),而缺口是 10.9ms
VAT 自己的成本每頂點每幀多 1–2 次貼圖取樣 —— 3000 頂點 × 16 隻 = 每幀 4.8 萬次,描邊再一次。淨值有可能是負的
會失去什麼殭屍轉頭看最近陀螺的效果會不見(那是當下才知道的資料,烘不進去)。頭部上仰可以烘進去
未查的風險影子走另一條 CPU 骨架複製路徑,身體改 VAT 之後兩邊姿勢可能對不上

建議不做。不是因為難,是投報率:新增一條烘焙管線與一份進版控的產物,換 2ms 上界、賠掉轉頭、還帶一個沒查的影子問題。

七、下一步

建議的優先序

要有心理準備的算術

three.js 加上原生 GL driver 就是 16.7ms,而 58fps 的整個預算是 17.2ms。我們自己寫的遊戲碼全部加起來只有 4ms,就算刪光也達不到目標。

⇒ 真正能動的只有兩件事:讓框架少做事(少算姿勢、少更新矩陣)與讓它少畫一次(少 draw call、少狀態切換)。單一改動不會達標,必須累積。

八、誠實區

這份報告量不到的東西