編寫高性能的 JavaScript
本文的初衷是想介紹如何利用些簡單的代碼小技巧就能促進JavaScript編譯器的優化進程從而提升代碼運行效率。特別是在游戲這種對于垃圾回收速度要求較高,你性能稍微差點用戶就能見到白屏的地方。
Monomorphism:單態性
JavaScript中允許函數調用時候傳入動態參數,不過就以簡單的2參數函數為例,當你的參數類型、參數數目與返回類型動態調用時才能決定,編譯器需要更多的時間來解析。編譯器自然地希望能夠處理那些單態可預測的數據結構、參數統計等。
function example(a, b) {
// we expect a, b to be numeric
console.log(++a * ++b);
};
example(); // bad
example(1); // still bad
example("1", 2); // dammit meg
example(1, 2); // good
Constants:常量
使用常量能夠讓編譯器在編譯時即完成變量的值替換:
const a = 42; // we can easily unfold this
const b = 1337 * 2; // we can resolve this expression
const c = a + b; // still can be resolved
const d = Math.random() * c; // we can only unfold 'c'
// before unfolding
a;
b;
c;
d;
// after unfolding
// we can do this at compile time!
42;
2674;
2716;
Math.random() * 2716;
Inlining:內聯
JIT編譯器能夠找出你的代碼中被執行次數最多的部分,將你的代碼分割成多個小的代碼塊能夠有助于編譯器在編譯時將這些代碼塊轉化為內聯格式然后增加執行速度。
Data Types:數據類型
盡可能地多用Numbers與Booleans類型,因為他們與其他類似于字符串等原始類型相比性能表現更好。使用字符串類型可能會帶來額外的垃圾回收消耗。
const ROBOT = 0;
const HUMAN = 1;
const SPIDER = 2;
let E_TYPE = {
Robot: ROBOT,
Human: HUMAN,
Spider: SPIDER
};
// bad
// avoid uncached strings in heavy tasks (or better in general)
if (entity.type === "Robot") {
}
// good
// the compiler can resolve member expressions
// without much deepness pretty fast
if (entity.type === E_TYPE.Robot) {
}
// perfect
// right side of binary expression can even get unfold
if (entity.type === ROBOT) {
}
Strict & Abstract Operators
盡可能使用 === 這個嚴格比較操作符而不是 == 操作符。使用嚴格比較操作符能夠避免編譯器進行類型推導與轉換,從而提高一定的性能。
Strict Conditions
JavaScript中的if語句也非常靈活,你可以直接在 if(a) then bla 這個類型的條件選擇語句中傳入隨意類似的a值。不過這種情況下,就像上文提及的嚴格比較操作符與寬松比較操作符一樣,編譯器需要將其轉化為多個數據類型進行比較,而不能立刻得出結果。當然,這并不是一味的反對使用簡寫方式,而是在非常強調性能的場景,還是建議做好每一個細節的優化:
let a = 2;
// bad
// abstracts to check in the worst case:
// - is value equal to true
// - is value greater than zero
// - is value not null
// - is value not NaN
// ..
if (a) {
// if a is true, do something
}
// good
if (a === 2) {
// do sth
}
// same goes for functions
function b() {
return (!false);
};
if (b()) {
// get in here slow
}
if (b() === true) {
// get in here fast
// the compiler knows a specific value to compare with
}
Arguments
盡可能避免使用arguments[index]方式進行參數獲取,并且盡量避免修改傳入的參數變量:
function mul(a, b) {
return (arguments[0]*arguments[1]); // bad, very slow
return (a*b); // good
};
function test(a, b) {
a = 5; // bad, dont modify argument identifiers
let tmp = a; // good
tmp *= 2; // we can now modify our fake 'a'
};
Toxicity:這些關鍵字有毒
Toxicity
如下列舉的幾個語法特性會影響優化進程:
-
eval
-
with
-
try/catch
同時盡量避免在函數內聲明函數或者閉包,可能在大量的運算中導致過多的垃圾回收操作。
Objecs
Object實例通常會共享隱類,因此當我們訪問或者設置某個實例的未預定義變量值的時候會創建一個隱類。
// our hidden class 'hc_0'
class Vector {
constructor(x, y) {
// compiler finds and expects member declarations here
this.x = x;
this.y = y;
}
};
// both vector objects share hidden class 'hc_0'
let vec1 = new Vector(0, 0);
let vec2 = new Vector(2, 2);
// bad, vec2 got hidden class 'hc_1' now
vec2.z = 0;
// good, compiler knows this member
vec2.x = 1;
Loops
盡可能的緩存數組長度的計算值,并且盡可能在同一個數組中存放單個類型。避免使用 for-in 語法來遍歷某個數組,因為它真的很慢。另外,continue與break語句在循環中的性能也是不錯的,這一點使用的時候不用擔心。另外,盡可能將短小的邏輯部分拆分到獨立的函數中,這樣更有利于編譯器進行優化。另外,使用前綴自增表達式,也能帶來小小的性能提升。(++i代替i++)
let badarray = [1, true, 0]; // bad, dont mix types
let array = [1, 0, 1]; // happy compiler
// bad choice
for (let key in array) {
};
// better
// but always try to cache the array size
let i = 0;
for (; i < array.length; ++i) {
key = array[i];
};
// good
let i = 0;
let key = null;
let length = array.length;
for (; i < length; ++i) {
key = array[i];
};
drawImage
draeImage函數算是最快的2D Canvas API之一了,不過我們需要注意的是如果為了圖方便省略了全參數傳入,也會增加性能損耗:
// bad
ctx.drawImage(
img,
x, y
);
// good
ctx.drawImage(
img,
// clipping
sx, sy,
sw, sh,
// actual stuff
x, y,
w, h
);
// much hax
// no subpixel rendering by passing integers
ctx.drawImage(
img,
sx|0, sy|0,
sw|0, sh|0,
x|0, y|0,
w|0, h|0
);
來自:https://segmentfault.com/a/1190000007604645