Tuesday, 21 January 2014

12 javascript optimizations tips

History

Urban Galaxy Online's client is written in pure javascript utilising modern API's such as WebGL, websockets, HTML audio and CSS3.

The client is written on thousand lines of javascript, most of them executing on each frame to update the game's logic and handle the continuous communication with the server. Every single line of code may have implications on the browser performance, affecting the game's framerate and the user experience.

The initial implementation of the client was... slow, especially on javascript engines other than Google's V8. The performance on Google Chrome was adequate at the time the client was built, but.. it suffered alot on Firefox and Safari. This changed when Firefox 22 was released, which featured a HUGE speed boost making the performance to be on par or even better compared to Google Chrome.

By applying a couple of optimization tips we managed to double or triple the performance of the javascript execution engine making the game play smooth on the most popular WebGL-enabled browsers.

This blog post is about revealing a couple of hints applied to the javascript code to improve framerate.

1. For loop optimizations

Replace the classic for loops with while loops.

Before

for (var i = 0; i < list.length; ++i) {
  list[i].foo();
}

After

var i = list.length;
while(i--) {
  list[i].foo();
}

2. Loop unrolling

There are special cases where loops can be unrolled to reduce for-loop overhead when the list length is fixed and known.

Before

for (var i = 0; i < 3; ++i) {
  list[i].foo();
}

After

list[0].foo();
list[1].foo();
list[2].foo();

3. Caching properties or arrays

Objects can contain objects which might contains objects and so on. It is very important to cache a property when it is accessed multiple times within a loop. Same goes with multidimensional arrays as illustrated in the example below.

Before

var grid = [][];
....
for (var x = grid.length - 1; x >= 0; --x) {
  for (var y = grid[x].length - 1; y >= 0; --y) {
    grid[x][y].foo();
  }
}

After

var grid = [][];
....
var x = grid.length;
while(x--) {
  var column = grid[x];
  var y = column.length;
  while(y--) {
    column[y].foo();
  }
}

4. Cache globals

When a variable is declared outside the scope of a function then, when it is accessed, it is looked up until the root window object, in order to be found. You'd better cache it locally.

Before

var global = 5;
function foo() {
  var i = 100;
  while(i) global += 10;
}

After

var global = 5;
function foo() {
  var i = 100, local = global;
  while(i) local += 10;
  global = local;
}

5. Use prototypes

Use prototypes for frequently allocated objects.

Before

function myObject(data) {
  this.foo = data.foo;
  this.bar = data.bar;
  this.doSomething = function() {
    ...
  } 
}

After

function myObject(data) {
  this.init(data);
}
myObject.prototype.init = function(data) {
  this.foo = data.foo;
  this.bar = data.bar;
}
myObject.prototype.doSomething = function() {
  ...
}


6. Optimize Math

Sometimes you may get a performance boost by replacing the Math.* functions with inline operations, such as:

Math.round(X)  ->  ~~(0.5 + X)
Math.floor(X)  ->  X|0

Also you may cache some numbers such as:

Math.PI * 2    -> var UG_PI = Math.PI * 2
Math.PI * 0.5  -> var UG_PI_HALF = Math.PI * 0.5

or some math functions

var UG_PI_DIV_180 = Math.PI / 180.0;

var UG_180_DIV_PI = 180.0 / Math.PI;



function deg2rad(degrees) {
  return degrees * UG_PI_DIV_180;

}

function rad2deg(radians) {

  return radians * UG_180_DIV_PI;
}

7. Reuse objects

Garbage collection may have a critical hit on your application's performance. Cache frequently used objects as proxies that can be reused. For example:

Before

function swap(vector1, vector2) {
  var temp = new Vector3();
  temp.copy(vector1);
  vector1.copy(vector2);
  vector2.copy(temp);

After

var proxy = new Vector3();
function swap(vector1, vector2) {
  //avoid memory allocation and garbage collection
  var temp = proxy;
  temp.copy(vector1);
  vector1.copy(vector2);
  vector2.copy(temp);

8. Inline function calls

Instead of calling a function inside big loops, try to inline the function within the loop. This way, you have a performance gain by avoiding the overhead of the function call.

Before

function copyVector(source, target) {
  target.x = source.x;
  target.y = source.y;
  target.z = source.z;
var i = ships.length;
while(i--) {
  var ship = ships[i];
  copy(ship.previous, ship.next);

After

var i = ships.length;
while(i--) {
  var previous = ship[i].previous;
  var next = ship[i].next;
  next.x = previous.x;
  next.y = previous.y;
  next.z = previous.z;
}

9. Minimize closures

Closures are one the most powerful and awesome features of the javascript language but making overuse of it can decrease performance, messing alot with your garbage collector.

Before

setTimeout(function() {
  doSomething();
}, 1000); 

After

function timoutCallback() {
  doSomething();
}
setTimeout(timeoutCallback, 1000);

10. Touch the DOM as less as possible

One of the most important performance hogs is altering the DOM, either by changing the DOM structure or by modifying the CSS. For in-game HUDs that need to change frequently, try to cache their status in javascript variables and then change it.  On the example below, each ship (or character) has an HTML element attached to display its name on top of the ship object in 2D space. When the ship is outside the camera viewport we need to hide it and display it each time it gets inside.

Before

if ( ship.isInside(viewport) ) {
  $(ship.el_name).show();
}
else {
  $(ship.el_name).hide();
}

After

if ( ship.isInside(viewport) ) {
  if (!ship.visible) {
    $(ship.el_name).show();
    ship.visible = true;
  }
}
else if (ship.visible) {
  $(ship.el_name).hide();
  ship.visible = false;
}


11. Set CSS style directly

Continuing the previous example, lets say that need to set the position of the ship's name element on each frame. jQuery is neat for changing the CSS of HTML elements, but when we need to touch the DOM many times per frame, the jQuery overhead may be important. So... for frequent changes, use raw javascript.

Before

var i = ships.length;
while(i--) {
  var el = ships[i].el_name;
  el.css({
    top: Y + 'px',
    left: X + 'px
  });
}

After

var i = ships.length;
while(i--) {
  var style = ships[i].el_name.style;
  style.top = Y + 'px';
  style.left = X + 'px';
}

12. Do not blindly use external libraries

Urban Galaxy is making use of some really neat frameworks, such as underscore.js. Underscore provides the _.each function to iterate through objects and lists. Although convenient, it introduces a perfomance hit due to the function callback. Use a while loop instead.  

Before

_.each(list, function(item) {
  item.foo();
}, this);

After

var i = list.length;
while(i--) {
  list[i].foo();
}

4 comments:

  1. I am expecting more interesting topics from you. And this was nice content and definitely it will be useful for many people.
    iOS Training in Chennai
    Android Training in Chennai
    php Training in Chennai

    ReplyDelete
  2. من المعروف ان نظافة المنازل من بين اهم الخدمات الضرورية في التنظيف والترقية بمنازلنا الى مصاف المنازل العصرية والتي عرفت تنظيفا عصريا من شانه ان يوفر لساكنته ظروفا حياتية راقية ولابد من توفر هيئة تختص في المجال مع تقديم ضمانات متكاملة من شانه ان تجعل الافراد يضمنون نجاح خدمة التنظيف ولا حاجة للمزيد من ضياع الاموال عبر طلبات خدمة نظافة المنازل التي لا تلبي حاجياتهم ولا تتماشى مع رغباتهم لأن العديد من شركات تنظيف المنازل تسعى فقط الى عرض خدماتها عبر طرق ترويجية فحسب من اجل كسب المال فقط دون مراعاة الوازع الاخلاقي والضمير المهني الذي يحتم على مدراء مثل هاته الشركات ان يسعوا خلف ارضاء العملاء ليس اكثر من هذا عبر اتقان تنظيف المنازل شركة تنظيف خزانات بالرياض
    شركة تنظيف مجالس بالرياض
    شركة نظافة عامة بالرياض
    شركة نقل عفش بالرياض
    شركة مكافحة حشرات بالرياض
    شركة تنظيف شقق بالرياض
    شركة تنظيف منازل بالرياض
    شركة رش مبيد بالرياض

    ReplyDelete