Optimizing JavaScript for Mobile

http://goo.gl/zuuEIU

Who am I?

Anzor Bashkhaz - @anzor_b

Developer Relations at BlackBerry

Memory



var greeting = "hello";
var greeting2 = greeting;
greeting = null;

console.log(greeting2);
> "hello"

Memory  

In JavaScript, all variables are pointers to memory addresses.

var greeting = "hello";
//greeting points at a memory address with string "hello"

var greeting2 = greeting2;
//greeting2 points at the same memory address with string "hello"

var greeting = null;
//removes (dereferences) "greeting" pointer to "hello"

Memory


Memory types in JavaScript

  • boolean
  • true or false
  • number
  • any double precision number (IEEE 754)
  • string
  • UTF-16 (UCS-2) string
  • object
  • key value map/ dictionary

Memory


7: "hello"
5: greeting
6: greeting2

Memory


So how does the browser reclaim back its memory?

Garbage Collection

Garbage refers to nodes that no longer have pointers referencing them.

greeting = null;
greeting2 = null;
//memory occupied by string "hello" can now be reclaimed (GC'd).

Memory


Nodes 9 and 10 are no longer referenced from the root, and can therefore be cleaned up.

More aggressive on mobile, due to memory constraints.

Tools


Chrome Task Manager (Tools > Task Manager)

Chrome Console> performance.memory

Heap Profiler


Shows memory contents and compares memory between runs.

DOM memory




var el = document.createElement("div") 
 
DOM nodes take up memory even when not attached to DOM tree (detached DOM nodes)

DOM Memory


DOM Memory


Tool: Chrome Developer Tools Timeline Panel


DOM Node count:

Total number of attached and detached DOM nodes at any given time 


Event Listener count:

Total number of event listeners at any given time

DOM Memory




  • Common memory leak pattern in modern HTML5 apps

  • create / destroy cycles (eg. View Management)

  • Everything created needs to be destroyed




Example : custom Dialog

Dialog.create = function() {
    
    var newDialog = {
        open : function() {},
        close : function() {},
        update : function() {},
        init : function() {
            this.el = document.createElement("div");
            return this;
        }
    };

return newDialog.init();
    
};

var dialog = Dialog.create(); //returns a new dialog

Create / Destroy

	
    //create 1000 dialogs
    var dialogs = [];
    for (var i=0; i<1000; i++){
	    var dialog = Dialog.create();
	    dialogs.push(dialog);
    };
	
    //wait for 5 seconds
    setTimeout(function(){
        //set the dialogs to null
	    for (var i=0; i<1000; i++){
	        dialogs[i] = null;
	    };
    },5000);

DOM graph


Unhealthy DOM memory

Dialog.create = function() {
var update = function(){
    newDialog.update();
};

var newDialog = {
    open : function() {},
    close : function() {},
    update : function() {},
    init : function() {
        this.el = document.createElement("div");
        window.addEventListener("orientationchange", update);
        return this;
    }
};

return newDialog.init();
    
};

DOM Graph

Unhealthy DOM memory


window.addEventListener("orientationchange", update);
    

  • window holds a reference to the update function 

 
  • this prevents objects from being GC'd, leaving lingering DOM nodes

 
aka. Zombie Nodes

Another memory leak pattern

Nodes are not being cleaned up

Preventing Zombie Nodes


a good "destroy" function:

  • dereference all DOM nodes
  • remove all Event listeners


    newDialog.destroy = function(){
        this.el = null;
        window.removeEventListener("orientationchange", update);
    };
    
var dialog = Dialog.create();
dialog.destroy();
dialog = null; //destroy ensures browser gets all the memory back

Manual GC


Desktop browsers are less aggressive than mobile


This forces garbage collection.

DOM operations


(eg: appendChild, removeChild, replaceChild)



DOM operations are expensive .


But, inevitable.

Reflows


A reflow occurs when:

  1. A DOM element is modified (class, height, width, etc...)
  2. Browser needs to traverse to the DOM tree
  3. And recalculate potential changes to all elements in the tree



Reflows are expensive (eg. cause jitters in animations)

Reflows

Chrome devTools' Timeline panel shows reflows

Reflows


How to minimize reflows?

  1. Batch DOM operations (documentFragment)
  2. Use "off-flow" animations
  3. Minimize CSS rules and remove unused CSS rules

Document Fragments



Task: create 100 DOM nodes and append to body

    
    for (var i=0; i< 100;i++){
        var el = document.createElement("div");
        document.body.appendChild(el);
    };



100 live DOM appendChild operations = very expensive

Document Fragments


document.createDocumentFragment();


Allows developers to create a set of DOM nodes off live DOM and append once to live DOM.

Document Fragments



    var fragment = document.createDocumentFragment();
    for (var i=0; i< 100;i++){
        var el = document.createElement("div");
        fragment.appendChild(el);
    };
    document.body.appendChild(fragment);
    

single live DOM operation = much more efficient
also, a single reflow, versus 100

Animations



  • Use hardware accelerated animations.

- transform: translate3d(x,y,z);

  • If possible, animate elements with position: absolute

- this has a smaller effect on sibling elements.

Event Delegation


In lists and grids, it is not uncommon to bind each item to an Event Listener.

var listEl = document.createElement("div");
for (var i = 0; i < 1000; i++) {
    var el = document.createElement("div");
    el.setAttribute("data-index", i);
    el.addEventListener("click", function() {
        console.log(this.getAttribute("data-index"));
    });
    listEl.appendChild(el);
};

Event Delegation

Event Delegation


Instead, why not attach a single listener to the parent element and use Event Bubbling?

    var listEl = document.createElement("div");
    for (var i = 0; i < 1000; i++) {
	    var el = document.createElement("div");
	    el.setAttribute("data-index", i);
	    listEl.appendChild(el);
	};
    listEl.addEventListener("click", function(e) {
	    console.log(e.target.getAttribute("data-index"));
    });

Event Delegation


Event Delegation




That's one event listener versus 1000.

Event bubbling (e) provides the target (e.target) element that first captured the event.




Additional tips

Avoid




What's wrong with the following? 
                   
for ( i = 0; i < 100; i++){ 
         //do something 
 }; 

Avoid


  • var start = function(){
        //do something
    };



var start = function start(){
    //do something
};
  • setTimeout(function(){
     
    //do something
    },100);
     


setTimeout(function doSomething(){
//do something
},100);

Avoid


Unnecessary CSS selectors:

div#header { } - instead, just use #header

body * { } - '*' is very expensive

Smooth scrolling


overflow: scroll;
-webkit-overflow-scrolling: touch;

Provides hardware accelerated, inertia scrolling on BB10 + iOS :)

What about the following?
-webkit-overflow-scrolling: -blackberry-touch;

References

JavaScript Memory Profiling (developer.google.com)
http://goo.gl/OOtOLQ

JavaScript Profiling (Smashing Magazine)
http://goo.gl/vS3tuO

Finding and debugging memory leaks in JavaScript with Chrome DevTools (Gonzalo Ruiz de Villa)
http://slid.es/gruizdevilla/memory

Memory Profiling with Chrome DevTools (YouTube)
http://goo.gl/UvQdrq
Made with Slides.com