如何將 HTML5 效能發揮到極致
HTML5作為新興領域越來越熱。然而在移動裝置硬體效能弱於PC的背景下,對效能的需求顯得更為重要,而HTML5效能優化前與優化後有著極大的差別,如何優化才能提高效能,對此熟知的人很少。本文以LayaAir引擎為例,通過程式碼示例詳細闡述如何利用引擎對HTML5作出效能的極致優化。
主題包括:
- 程式碼執行基本原理
- 基準測試
- 記憶體優化
- 圖形渲染效能
- 減少CPU使用量
- 其他優化策略
第1節:程式碼執行基本原理
LayaAir引擎支援AS3、TypeScript、JavaScript三種語言開發,然而無論是採用哪種開發語言,最終執行的都是JavaScript程式碼。所有看到的畫面都是通過引擎繪製出來的,更新頻率取決於開發者指定的FPS,例如指定幀頻率為60FPS,則執行時每個幀的執行時間為六十分之一秒,所以幀速越高,視覺上感覺越流暢,60幀是滿幀。
由於實際執行環境是在瀏覽器中,因此效能還取決於JavaScript直譯器的效率,指定的FPS幀速在低效能直譯器中可能不會達到,所以這部分不是開發者能夠決定的,開發者能作的是儘可能通過優化,在低端裝置或低效能瀏覽器中,提升FPS幀速。
LayaAir引擎在每幀都會重繪,在效能優化時,除了關注每幀執行邏輯程式碼帶來的CPU消耗,還需要注意每幀呼叫繪圖指令的數量以及GPU的紋理提交次數。
第2節:基準測試
LayaAir引擎內建的效能統計工具可用於基準測試,實時檢測當前效能。開發者可以使用laya.utils.Stat類,通過Stat.show() 顯示統計皮膚。具體編寫程式碼如下例所示:
Stat.show(0,0); //AS3的皮膚呼叫寫法 Laya.Stat.show(0,0); //TS與JS的皮膚呼叫寫法
Canvas渲染的統計資訊:
WebGL渲染的統計資訊:
統計引數的意義:
FPS:
每秒呈現的幀數(數字越高越好)。
使用canvas渲染時,描述欄位顯示為FPS(Canvas),使用WebGL渲染時,描述欄位顯示為FPS(WebGL)。
Sprite:
渲染節點數量(數字越低越好)。
Sprite統計所有渲染節點(包括容器),這個數字的大小會影響引擎節點遍歷,資料組織和渲染的次數。
DrawCall:
DrawCall在canvas和WebGL渲染下代表不同的意義(越少越好)。
Canvas下表示每幀的繪製次數,包括圖片、文字、向量圖。儘量限制在100之下。
WebGL下表示渲染提交批次,每次準備資料並通知GPU渲染繪製的過程稱為1次DrawCall,在每1次DrawCall中除了在通知GPU的渲染上比較耗時之外,切換材質與shader也是非常耗時的操作。 DrawCall的次數是決定效能的重要指標,儘量限制在100之下。
Canvas:
三個數值 —— 每幀重繪的畫布數量 / 快取型別為“normal”型別的畫布數量 / 快取型別為“bitmap”型別的畫布數量”。
CurMem:僅限WebGL渲染,表示記憶體與視訊記憶體佔用(越低越好)。
Shader:僅限WebGL渲染,表示每幀Shader提交次數。
無論是Canvas模式還是WebGL模式,我們都需要重點關注DrawCall,Sprite,Canvas這三個引數,然後針對性地進行優化。(參見“圖形渲染效能”)
第3節:記憶體優化
物件池
物件池,涉及到不斷重複使用物件。在初始化應用程式期間建立一定數量的物件並將其儲存在一個池中。對一個物件完成操作後,將該物件放回到池中,在需要新物件時可以對其進行檢索。
由於例項化物件成本很高,使用物件池重用物件可減少例項化物件的需求。還可以減少垃圾回收器執行的機會,從而提高程式的執行速度。
以下程式碼演示使用
Laya.utils.Pool:
ar SPRITE_SIGN = 'spriteSign'; var sprites = []; function initialize() { for (var i = 0; i < 1000; i++) { var sp = Pool.getItemByClass(SPRITE_SIGN, Sprite) sprites.push(sp); Laya.stage.addChild(sp); } } initialize();
在initialize中建立大小為1000的物件池。
以下程式碼在當單擊滑鼠時,將刪除顯示列表中的所有顯示物件,並在以後的其他任務中重複使用這些物件:
Laya.stage.on("click", this, function() { var sp; for(var i = 0, len = sprites.length; i < len; i++) { sp = sprites.pop(); Pool.recover(SPRITE_SIGN, sp); Laya.stage.removeChild(sp); } });
呼叫Pool.recover後,指定的物件會被回收至池內。
使用Handler.create
在開發過程中,會經常使用Handler來完成非同步回撥。Handler.create使用了內建物件池管理,因此在使用Handler物件時應使用Handler.create來建立回撥處理器。以下程式碼使用Handler.create建立載入的回撥處理器:
Laya.loader.load(urls, Handler.create(this, onAssetLoaded));
在上面的程式碼中,回撥被執行後Handler將會被物件池收回。此時,考慮如下程式碼會發生什麼事:
Laya.loader.load(urls, Handler.create(this, onAssetLoaded), Handler.create(this, onLoading));
在上面的程式碼中,使用Handler.create返回的處理器處理progress事件。此時的回撥執行一次之後就被物件池回收,於是progress事件只觸發了一次,此時需要將四個名為once的引數設定為false:
Laya.loader.load(urls, Handler.create(this, onAssetLoaded), Handler.create(this, onLoading, null, false));
釋放記憶體
JavaScript執行時無法啟動垃圾回收器。要確保一個物件能夠被回收,請刪除對該物件的所有引用。Sprite提供的destory會幫助設定內部引用為null。
例如,以下程式碼確保物件能夠被作為垃圾回收:
var sp = new Sprite(); sp.destroy();
當物件設定為null,不會立即將其從記憶體中刪除。只有系統認為記憶體足夠低時,垃圾回收器才會執行。記憶體分配(而不是物件刪除)會觸發垃圾回收。
垃圾回收期間可能佔用大量CPU並影響效能。通過重用物件,嘗試限制使用垃圾回收。此外,儘可能將引用設定為null,以便垃圾回收器用較少時間來查詢物件。有時(比如兩個物件相互引用),無法同時設定兩個引用為null,垃圾回收器將掃描無法被訪問到的物件,並將其清除,這會比引用計數更消耗效能。
資源解除安裝
遊戲執行時總會載入許多資源,這些資源在使用完成後應及時解除安裝,否則一直殘留在記憶體中。
下例演示載入資源後對比資源解除安裝前和解除安裝後的資源狀態:
var assets = []; assets.push("res/apes/monkey0.png"); assets.push("res/apes/monkey1.png"); assets.push("res/apes/monkey2.png"); assets.push("res/apes/monkey3.png"); Laya.loader.load(assets, Handler.create(this, onAssetsLoaded)); function onAssetsLoaded() { for(var i = 0, len = assets.length; i < len; ++i) { var asset = assets[i]; console.log(Laya.loader.getRes(asset)); Laya.loader.clearRes(asset); console.log(Laya.loader.getRes(asset)); } }
關於濾鏡、遮罩
嘗試儘量減少使用濾鏡效果。將濾鏡(BlurFilter和GlowFilter)應用於顯示物件時,執行時將在記憶體中建立兩張點陣圖。其中每個點陣圖的大小與顯示物件相同。將第一個點陣圖建立為顯示物件的柵格化版本,然後用於生成應用濾鏡的另一個點陣圖:
應用濾鏡時記憶體中的兩個點陣圖
當修改濾鏡的某個屬性或者顯示物件時,記憶體中的兩個點陣圖都將更新以建立生成的點陣圖,這兩個點陣圖可能會佔用大量記憶體。此外,此過程涉及CPU計算,動態更新時將會降低效能(參見“圖形渲染效能 – 關於cacheAs)。
ColorFiter在Canvas渲染下需要計算每個畫素點,而在WebGL下的GPU消耗可以忽略不計。
最佳的做法是,儘可能使用影像創作工具建立的點陣圖來模擬濾鏡。避免在執行時中建立動態點陣圖,可以幫助減少CPU或GPU負載。特別是一張應用了濾鏡並且不會在修改的影像。
第4節:圖形渲染效能
優化Sprite
1.儘量減少不必要的層次巢狀,減少Sprite數量。
2.非可見區域的物件儘量從顯示列表移除或者設定visible=false。
3.對於容器內有大量靜態內容或者不經常變化的內容(比如按鈕),可以對整個容器設定cacheAs屬性,能大量減少Sprite的數量,顯著提高效能。如果有動態內容,最好和靜態內容分開,以便只快取靜態內容。
4.Panel內,會針對panel區域外的直接子物件(子物件的子物件判斷不了)進行不渲染處理,超出panel區域的子物件是不產生消耗的。
優化DrawCall
1.對複雜靜態內容設定cacheAs,能大量減少DrawCall,使用好cacheAs是遊戲優化的關鍵。
2.儘量保證同圖集的圖片渲染順序是挨著的,如果不同圖集交叉渲染,會增加DrawCall數量。
3.儘量保證同一個皮膚中的所有資源用一個圖集,這樣能減少提交批次。
優化Canvas
在對Canvas優化時,我們需要注意,在以下場合不要使用cacheAs:
1.物件非常簡單,比如一個字或者一個圖片,設定cacheAs=bitmap不但不提高效能,反而會損失效能。
2.容器內有經常變化的內容,比如容器內有一個動畫或者倒數計時,如果再對這個容器設定cacheAs=bitmap,會損失效能。
可以通過檢視Canvas統計資訊的第一個值,判斷是否一直在重新整理Canvas快取。
關於cacheAs
設定cacheAs可將顯示物件快取為靜態影像,當cacheAs時,子物件發生變化,會自動重新快取,同時也可以手動呼叫reCache方法更新快取。 建議把不經常變化的複雜內容,快取為靜態影像,能極大提高渲染效能,cacheAs有”none”,”normal”和”bitmap”三個值可選。
- 預設為”none”,不做任何快取。
2.當值為”normal”時,canvas下進行畫布快取,webgl模式下進行命令快取。
3.當值為”bitmap”時,canvas下進行依然是畫布快取,webGL模式下使用renderTarget快取。這裡需要注意的是,webGL下renderTarget快取模式有2048大小限制,超出2048會額外增加記憶體開銷。另外,不斷重繪時開銷也比較大,但是會減少drawcall,渲染效能最高。 webGL下命令快取模式只會減少節點遍歷及命令組織,不會減少drawcall,效能中等。
設定cacheAs後,還可以設定staticCache=true以阻止自動更新快取,同時可以手動呼叫reCache方法更新快取。
cacheAs主要通過兩方面提升效能。一是減少節點遍歷和頂點計算;二是減少drawCall。善用cacheAs將是引擎優化效能的利器。
下例繪製10000個文字:
Laya.init(550, 400, Laya.WebGL); Laya.Stat.show(); var textBox = new Laya.Sprite(); var text; for (var i = 0; i < 10000; i++) { text = new Laya.Text(); text.text = (Math.random() * 100).toFixed(0); text.color = "#CCCCCC"; text.x = Math.random() * 550; text.y = Math.random() * 400; textBox.addChild(text); } Laya.stage.addChild(textBox);
下面是筆者電腦上的執行時截圖,FPS穩定於52上下。
當我們對文字所在的容器設定為cacheAs之後,如下面的例子所示,效能獲得較大的提升,FPS達到到了60幀。
// …省略其他程式碼… var textBox = new Laya.Sprite(); textBox.cacheAs = "bitmap"; // …省略其他程式碼…
文字描邊
在執行時,設定了描邊的文字比沒有描邊的文字多呼叫一次繪圖指令。此時,文字對CPU的使用量和文字的數量成正比。因此,儘量使用替代方案來完成同樣的需求。
對於幾乎不變動的文字內容,可以使用cacheAs降低效能消耗,參見“圖形渲染效能 – 關於cacheAs”。
對於內容經常變動,但是使用的字元數量較少的文字域,可以選擇使用點陣圖字型。
跳過文字排版,直接渲染
大多數情況下,很多文字都不需要複雜的排版,僅僅簡單地顯示一行字。為了迎合這一需求,Text提供的名為changeText的方法可以直接跳過排版。
var text = new Text(); text.text = "text"; Laya.stage.addChild(text); //後面只是更新文字內容,使用changeText能提高效能 text.changeText("text changed.");
Text.changeText會直接修改繪圖指令中該文字繪製的最後一條指令,這種前面的繪圖指令依舊存在的行為會導致changeText只使用於以下情況:
文字始終只有一行。
文字的樣式始終不變(顏色、粗細、斜體、對齊等等)。
即使如此,實際程式設計中依舊會經常使用到這樣的需要。
第5節:減少CPU使用量
減少動態屬性查詢
JavaScript中任何物件都是動態的,你可以任意地新增屬性。然而,在大量的屬性裡查詢某屬性可能很耗時。如果需要頻繁使用某個屬性值,可以使用區域性變數來儲存它:
function foo() { var prop = target.prop; // 使用prop process1(prop); process2(prop); process3(prop); }
計時器
LayaAir提供兩種計時器迴圈來執行程式碼塊。
- Laya.timer.frameLoop執行頻率依賴於幀頻率,可通過Stat.FPS檢視當前幀頻。
- Laya.timer.loop執行頻率依賴於引數指定時間。
當一個物件的生命週期結束時,記得清除其內部的Timer:
Laya.timer.frameLoop(1, this, animateFrameRateBased); Laya.stage.on("click", this, dispose); function dispose() { Laya.timer.clear(this, animateFrameRateBased); }
獲取顯示物件邊界的做法
在相對佈局中,很經常需要正確地獲取顯示物件的邊界。獲取顯示物件的邊界也有多種做法,而其間差異很有必要知道。
1.使用getBounds/ getGraphicBounds。、
var sp = new Sprite(); sp.graphics.drawRect(0, 0, 100, 100, "#FF0000"); var bounds = sp.getGraphicBounds(); Laya.stage.addChild(sp);
getBounds可以滿足多數多數需求,但由於其需要計算邊界,不適合頻繁呼叫。
2.設定容器的autoSize為true。
var sp = new Sprite(); sp.autoSize = true; sp.graphics.drawRect(0, 0, 100, 100, "#FF0000"); Laya.stage.addChild(sp);
上述程式碼可以在執行時正確獲取寬高。autoSize在獲取寬高並且顯示列表的狀態發生改變時會重新計算(autoSize通過getBoudns計算寬高)。所以對擁有大量子物件的容器應用autoSize是不可取的。如果設定了size,autoSize將不起效。
使用loadImage後獲取寬高:
var sp = new Sprite(); sp.loadImage("res/apes/monkey2.png", 0, 0, 0, 0, Handler.create(this, function() { console.log(sp.width, sp.height); })); Laya.stage.addChild(sp);
loadImage在載入完成的回撥函式觸發之後才可以正確獲取寬高。
3.直接呼叫size設定:
Laya.loader.load("res/apes/monkey2.png", Handler.create(this, function() { var texture = Laya.loader.getRes("res/apes/monkey2.png"); var sp = new Sprite(); sp.graphics.drawTexture(texture, 0, 0); sp.size(texture.width, texture.height); Laya.stage.addChild(sp); }));
使用Graphics.drawTexture並不會自動設定容器的寬高,但是可以使用Texture的寬高賦予容器。毋庸置疑,這是最高效的方式。
注:getGraphicsBounds用於獲取向量繪圖寬高。
根據活動狀態改變幀頻
幀頻有三種模式,Stage.FRAME_SLOW維持FPS在30;Stage.FRAME_FAST維持FPS在60;Stage.FRAME_MOUSE則選擇性維持FPS在30或60幀。
有時並不需要讓遊戲以60FPS的速率執行,因為30FPS已經能夠滿足多數情況下人類視覺的響應,但是滑鼠互動時,30FPS可能會造成畫面的不連貫,於是Stage.FRAME_MOUSE應運而生。
下例展示以Stage.FRAME_SLOW的幀率,在畫布上移動滑鼠,使圓球跟隨滑鼠移動:
Laya.init(Browser.width, Browser.height); Stat.show(); Laya.stage.frameRate = Stage.FRAME_SLOW; var sp = new Sprite(); sp.graphics.drawCircle(0, 0, 20, "#990000"); Laya.stage.addChild(sp); Laya.stage.on(Event.MOUSE_MOVE, this, function() { sp.pos(Laya.stage.mouseX, Laya.stage.mouseY); });
此時FPS顯示30,並且在滑鼠移動時,可以感覺到圓球位置的更新不連貫。設定Stage.frameRate為Stage.FRAME_MOUSE:
Laya.stage.frameRate = Stage.FRAME_MOUSE;
此時在滑鼠移動後FPS會顯示60,並且畫面流暢度提升。在滑鼠靜止2秒不動後,FPS又會恢復到30幀。
使用callLater
callLater使程式碼塊延遲至本幀渲染前執行。如果當前的操作頻繁改變某物件的狀態,此時可以考慮使用callLater,以減少重複計算。
考慮一個圖形,對它設定任何改變外觀的屬性都將導致圖形重繪:
var rotation = 0, scale = 1, position = 0; function setRotation(value) { this.rotation = value; update(); } function setScale(value) { this.scale = value; update(); } function setPosition(value) { this.position = value; update(); } function update() { console.log('rotation: ' + this.rotation + '\tscale: ' + this.scale + '\tposition: ' + position); }
呼叫以下程式碼更改狀態:
setRotation(90); setScale(2); setPosition(30);
控制檯的列印結果是
rotation: 90 scale: 1 position: 0
rotation: 90 scale: 2 position: 0
rotation: 90 scale: 2 position: 30
update被呼叫了三次,並且最後的結果是正確的,但是前面兩次呼叫都是不需要的。
嘗試將三處update改為:
Laya.timer.callLater(this, update);
此時,update只會呼叫一次,並且是我們想要的結果。
圖片/圖集載入
在完成圖片/圖集的載入之後,引擎就會開始處理圖片資源。如果載入的是一張圖集,會處理每張子圖片。如果一次性處理大量的圖片,這個過程可能會造成長時間的卡頓。
在遊戲的資源載入中,可以將資源按照關卡、場景等分類載入。在同一時間處理的圖片越少,當時的遊戲響應速度也會更快。在資源使用完成後,也可以予以解除安裝,釋放記憶體。
第6節:其他優化策略
1.減少粒子使用數量,在移動平臺Canvas模式下,儘量不用粒子;
2.在Canvas模式下,儘量減少旋轉,縮放,alpha等屬性的使用,這些屬性會對效能產生消耗。(在WebGL模式可以使用);
3.不要在timeloop裡面建立物件及複雜計算;
4.儘量減少對容器的autoSize的使用,減少getBounds()的使用,因為這些呼叫會產生較多計算;
5.儘量少用try catch的使用,被try catch的函式執行會變得非常慢;
相關文章
- 將ipad發揮到極致iPad
- 牢記將iPhone特色硬體優勢發揮到極致iPhone
- 將MySQL去重操作優化到極致之三彈連發MySql優化
- DapuStor聯合GRAID(圖睿科技)—驗證GPU RAID卡發揮NVMe SSD極致效能AIGPU
- React內部的效能優化沒有達到極致?React優化
- 將Objective-C語法簡化到極致,從此不再煩惱!!!Object
- 企業應當如何發揮社會化工具的效能
- React 同構與極致的效能優化React優化
- 如何充分發揮 SQL 能力?SQL
- 將定時測試任務玩到極致
- 3倍+提升,高德地圖極致效能優化之路地圖優化
- 9.2高分!浪潮資訊伺服器NF5180M6極致設計締造極致效能伺服器
- 3倍+提升,高德地圖極致效能最佳化之路地圖
- 極致效能最佳化:前端SSR渲染利器Qwik.js前端JS
- .NET8極致效能最佳化Non-GC HeapGC
- 作為一個產品運營,如何利用資料把產品打造到極致?
- JavaScript 極致效能追求:TC39 二進位制 AST 提案JavaScriptAST
- 計算機組成-無鎖程式設計追求極致效能計算機程式設計
- 提高遊戲留存,如何讓遊戲社群發揮效用?遊戲
- CRM如何讓客戶關係發揮作用
- 分庫分表如何進行極致的優化優化
- 「事件管理」如何讓使用者體驗更加極致?事件
- ToplingDB 的序列化框架:最佳化到極致框架
- go-zero微服務實戰系列(九、極致優化秒殺效能)Go微服務優化
- 新基建浪潮下,區塊鏈如何發揮作用?區塊鏈
- CRM如何讓客戶關係管理發揮作用?
- 專訪英特爾(中國)開源技術中心:HTML5要如何達到原生效能HTML
- 什麼是HTML5?HTML5發展前景如何?HTML
- 思邁特軟體Smartbi:專注BI,把產品打造到極致
- 做出Uber移動網頁版還不夠 極致效能打造才見真章網頁
- 穩中求進 浪潮將全力發揮數字技術優勢
- 如何讓敏捷中的每日站會發揮最大效果?敏捷
- 如何讓CAD技術發揮的更有價值
- 效能的極致,Rust的加持,Zed-Dev編輯器快速搭建Python3.10開發環境RustZeddevPython開發環境
- 將回合制遊戲做到極致《夢幻西遊》憑什麼火了十六年?遊戲
- 把前端監控做到極致前端
- mongodb網路傳輸處理原始碼實現及效能調優-體驗核心效能極致設計MongoDB原始碼
- 乾貨|效能提升金鑰,由程式碼細節帶來的極致體驗