这篇文章告诉你一些简单的技巧来优化 JavaScript 编译器工作,从而让你的 JavaScript 代码运行的更快。尤其是在你游戏中发现帧率下降或是当垃圾回收器有大量的工作要完成的时候。
单一同态:
当你定义了一个两个参数的函数,编译器会接受你的定义,如果函数参数的类型、个数或者返回值的类型改变编译器的工作会变得艰难。通常情况下,单一同态的数据结构和个数相同的参数会让你的程序会更好的工作。
- function example(a, b) {
- // 期望a,b都为数值类型
- console.log(++a * ++b);
- };
- example(); // 不佳
- example(1); // 仍然不佳
- example("1", 2); // 尤其不佳
- example(1, 2); // 很好
展开:
编译器会在编译的时候求出变量的值并且展开它(最佳实践),因此在程序执行前可以尽量多的表达信息。常量和变量一样可以被展开,只要它们没有用任何的与运行期有关的运算。
- const a = 42; // 很容易展开
- const b = 1337 * 2; // 可以求值
- const c = a + b; // 也可以求值
- const d = Math.random() * c; // 只能展开'c'
- const e = "Hello " + "Medium"; // 其他类型的值也可以
- // 展开前
- a;
- b;
- c;
- d;
- e;
- // 展开后
- // 会在编译的时候做好这些!
- 42
- 2674
- 2716
- Math.random() * 2716
- "Hello Medium"
函数内联:
JIT 编译器会找出你的代码中哪些部分是经常执行的。在编译的时候通过将函数分成小块来将代码块内联并且热追踪函数之后代码会执行的更快。
- // 以下这些会内联
- // [✓] 单一的返回语句
- // [✓] 返回总是一样的
- // [✓] 返回时单一同态的
- // [✓] 参数是单一同态的
- // [✓] 函数体是一个单一的语句
- // [✓] 不是包裹在另一个函数体内
- // ...
- function isNumeric(n) {
- return (
- n >= 48 && n <= 57
- );
- };
- let cc = "8".charCodeAt(0);
- // 内联前
- if (isNumeric(cc)) {
- }
- // 内联之后
- if (cc >= 48 && cc <= 57) {
- }
Declarations:
避免在频繁调用的函数里声明函数 / 闭包或对象。对象(也包括函数,对象)会被压到堆里,垃圾回收器会影响这个堆,那里有很多 需要确定下一步(像释放与否)
相反,声明一个变量会很快,因为它们是被压到栈里。比如,一个函数会有自己的栈,与函数相关的变量都会压到这个栈里,无论何时这个函数退出,栈也随着释放。
- // 欠佳
- function a() {
- // 决不再函数里面申明函数
- // 会在每次调用函数分配资源
- let doSomething = function() {
- return (1);
- };
- return (doSomething());
- };
- let doSomething = function() {
- return (1);
- };
- // 很好
- // 在函数外申明 'doSomething'
- // 因此可以只调用它,而不是
- // 在每次调用'b'去申明和调用
- function b() {
- return (doSomething());
- };
参数:
函数调用的代价是昂贵的(如果编译器不能内联它们)。尝试去使用尽可能少的参数并且不在函数体内修改参数。
- function mul(a, b) {
- return (arguments[0]*arguments[1]); // 欠佳, 很慢
- return (a*b); // 很好
- };
- function test(a, b) {
- a = 5; // 欠佳, 不修改参数标识
- let tmp = a; // 很好
- tmp *= 2; // 可以修改伪造的 'a'
- };
数据类型:
尝试尽可能多的取用数值和布尔类型,它们在比较操作中比其他基本类型要快很多。比如,声明一个字符串类型会偷偷的造成一大堆垃圾数据,因为字符串是一个复杂的有很多预设属性的对象。
同时,避免操作负数和多位小数的双精度浮点数。
- const ROBOT = 0;
- const HUMAN = 1;
- const SPIDER = 2;
- let E_TYPE = {
- Robot: ROBOT,
- Human: HUMAN,
- Spider: SPIDER
- };
- // 欠佳
- // 避免在大任务中缓存字符串(最好在一般中场景也不)
- if (entity.type === "Robot") {
- }
- // 很好
- // 编译器会算出数值表达式
- // 没有深度计算会更快
- if (entity.type === E_TYPE.Robot) {
- }
- // 完美
- // 右侧的二元表达式也是可以被展开的
- if (entity.type === ROBOT) {
- }
严格和抽象运算符:
尝试使用三等号操作如 "==="(严格的)而不是 "==" (宽松的, 抽象的)。执行严格的运算符保证编译器去预设一个明确的值,而不用以多种情况去比较语句。(比如 n>0=^true),这样会导致更好的性能方案。
严格条件:
JavaScript 提供了很棒的语法糖来允许你比如 "if (a) then bla"这样的代码。这种情况下,编译器必须去以多种类型去比较"a" 来确定是否为真,因为它不知道是那种类型的结果。当然,你应该使用这些很棒的语法糖,但是在快速执行的复杂的有多个返回语句的函数(如 null 或者 Object)中,你应该考虑以下建议。
有毒:
以下列表里的语言特性,会较少或阻止代码优化过程。
对象:
对象实例通常会尝试共享一个隐藏的类,谨慎的给一个实例化的对象添加一个成员变量,因为这样会创建一个新的隐藏类并且对编译器来说这会复杂很多(对你也会一样)
- // 隐藏类'hc_0'
- class Vector {
- constructor(x, y) {
- // 编译器找到并且期望的成员声明在这
- this.x = x;
- this.y = y;
- }
- };
- // 两个vector对象共享隐藏类'hc_0'
- let vec1 = new Vector(0, 0);
- let vec2 = new Vector(2, 2);
- // 欠佳,vec2这样会创建新的隐藏类'hc_1'
- vec2.z = 0;
- // 很好,编译器知道这个成员
- vec2.x = 1;
循环:
缓存你的数组长度属性,并且使用数组作为一个单一同态的类型。通常避免使用 "for..in" 或者在数组上循环,因为这会很慢。
循环语句中 continue 和 break 语句会比 if 语句体更快。保持你的循环干净,把所有的东西打包成一个子函数,这样编辑器感觉会更舒服。同时,使用预增操作符(_++ ++_)会有一点点的性能提升。
- let badarray = [1, true, 0]; // 欠佳, 不要混合类型
- let array = [1, 0, 1]; // 好的用法
- // 不好的选择
- for (let key in array) {
- };
- // 更好的
- // 但是总是缓存数组大小
- let i = 0;
- for (; i < array.length; ++i) {
- key = array[i];
- };
- // 很好
- let i = 0;
- let key = null;
- let length = array.length;
- for (; i < length; ++i) {
- key = array[i];
- };
来源: