將 HTML5 性能發揮到極致

HTML5做爲新興領域愈來愈熱。然而在移動設備硬件性能弱於PC的背景下,對性能的需求顯得更爲重要,而HTML5性能優化前與優化後有着極大的差異,如何優化才能提升性能,對此熟知的人不多。本文以LayaAir引擎爲例,經過代碼示例詳細闡述如何利用引擎對HTML5做出性能的極致優化。前端

主題包括:web

  • 代碼執行基本原理
  • 基準測試
  • 內存優化
  • 圖形渲染性能
  • 減小CPU使用量
  • 其餘優化策略

第1節:代碼執行基本原理

LayaAir引擎支持AS三、TypeScript、JavaScript三種語言開發,然而不管是採用哪一種開發語言,最終執行的都是JavaScript代碼。全部看到的畫面都是經過引擎繪製出來的,更新頻率取決於開發者指定的FPS,例如指定幀頻率爲60FPS,則運行時每一個幀的執行時間爲六十分之一秒,因此幀速越高,視覺上感受越流暢,60幀是滿幀。編程

因爲實際運行環境是在瀏覽器中,所以性能還取決於JavaScript解釋器的效率,指定的FPS幀速在低性能解釋器中可能不會達到,因此這部分不是開發者可以決定的,開發者能做的是儘量經過優化,在低端設備或低性能瀏覽器中,提高FPS幀速。canvas

LayaAir引擎在每幀都會重繪,在性能優化時,除了關注每幀執行邏輯代碼帶來的CPU消耗,還須要注意每幀調用繪圖指令的數量以及GPU的紋理提交次數。瀏覽器

第2節:基準測試

LayaAir引擎內置的性能統計工具可用於基準測試,實時檢測當前性能。開發者可使用laya.utils.Stat類,經過Stat.show() 顯示統計面板。具體編寫代碼以下例所示:緩存

Stat.show(0,0); //AS3的面板調用寫法       
Laya.Stat.show(0,0); //TS與JS的面板調用寫法

Canvas渲染的統計信息:性能優化

WebGL渲染的統計信息:dom

統計參數的意義:異步

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」三個值可選。

  1. 默認爲」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);
web前端開發學習Q-q-u-n: 784783012 ,分享學習的方法和須要注意的小細節,不停更新最新的教程和學習方法(詳細的前端項目實戰教學視頻)

下面是筆者電腦上的運行時截圖,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提供兩種計時器循環來執行代碼塊。

  1. Laya.timer.frameLoop執行頻率依賴於幀頻率,可經過Stat.FPS查看當前幀頻。
  2. 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);
web前端開發學習Q-q-u-n: 784783012 ,分享學習的方法和須要注意的小細節,不停更新最新的教程和學習方法(詳細的前端項目實戰教學視頻)

此時,update只會調用一次,而且是咱們想要的結果。

圖片/圖集加載

在完成圖片/圖集的加載以後,引擎就會開始處理圖片資源。若是加載的是一張圖集,會處理每張子圖片。若是一次性處理大量的圖片,這個過程可能會形成長時間的卡頓。

在遊戲的資源加載中,能夠將資源按照關卡、場景等分類加載。在同一時間處理的圖片越少,當時的遊戲響應速度也會更快。在資源使用完成後,也能夠予以卸載,釋放內存。

第6節:其餘優化策略

1.減小粒子使用數量,在移動平臺Canvas模式下,儘可能不用粒子;

2.在Canvas模式下,儘可能減小旋轉,縮放,alpha等屬性的使用,這些屬性會對性能產生消耗。(在WebGL模式可使用);

3.不要在timeloop裏面建立對象及複雜計算;

4.儘可能減小對容器的autoSize的使用,減小getBounds()的使用,由於這些調用會產生較多計算;

5.儘可能少用try catch的使用,被try catch的函數執行會變得很是慢;

相關文章
相關標籤/搜索