• Posted on: Feb 28, 2014
  • Tag:
  • Reactions: 5

> Performance tricks for (mobile) web development

During the many years of iScroll development I had to find all the possible ways to spare CPU cycles and speed optimization has become a mission. In this post I try to sum up the bare minimum for a snappy application.

First of all. Don’t believe “performance tricks” posts. Yes, including this one. Each scenario is different and what works for one installation might not work for another. The only Universal Truth about optimization is: testing. Do it a lot, do it often. Try different techniques, even those that theoretically shouldn’t perform better, even those discouraged by the evangelists at the last JS conf.

So trick number one is: test your code using various approaches and test it often because things change rapidly in this convoluted world.

To help me in this article I’ve baked a very simple and very wrong demo. The demo basically shows everything you should not do if you are looking for performance. We are going to apply a series of optimization techniques to this demo to reach what is hopefully the best possible frame rate. I’m not going to review it line by line, look at the source, they are few lines of code.

I’m using an iPad as a testing device so the benchmarks are consistent and not machine dependent. I’m emphasizing on mobile development but everything I’m saying here is also valid for desktop browser. Mobile development is making us better desktop developers, too.

This is the very wrong demo. The current FPS on an iPad with retina display is around 8fps (ouch!).

Keep it simple, keep it flat

Always start from a solid foundation. Keep your DOM simple, reduce your element tree to the bone, never use two elements if you can use one.

I know that your inner designer will shiver, but sometimes it is better to use an image instead a series of complicated CSS properties. Remember that shadows and opacity greatly impact on performance. By simply removing opacity from the demo we go from 8 to 13 frames per seconds, if we also remove box-shadow we spare another couple of FPS.

But how can we keep both shadow and transparency without kneeing the renderer? As I said: use an image. The same is true for many other scenarios, I know you don’t like to make a picture just for a box shadow, but the road to maximum performance is uphill and I never said you would have liked it.

In this updated demo I have replaced three elements with rich styling with one with just a background image. If you look at the demo on Chrome we already made the jump from 40 to 60 FPS, so the direction is right but on iPad performance is still very low.

This leads us to the next step.

Don’t touch the DOM!

When the element reaches the end of line it is destroyed and a new one created. This is really stressful for the browser.

Create all the elements you need in advance and reuse them as needed. Don’t alter the DOM especially during intensive rendering. In a recent experiment I’ve conducted I also discovered that touch events don’t like createElement. The internal event emitter gets really pissed off if you start toying with HTML structure.

So our demo now creates 50 elements and keeps reusing them. FPS count went from 8 to 14. Nice, but still very low.

Cache everything

Javascript really takes advantage of intense caching. Anything you can cache, you should… And actually everything can be cached in JS.

I’m doing a very nasty thing in the demo which is querySelectorAll inside a loop. Queries to the DOM should be always cached, so in the next iteration I’m going to store a reference to the elements in an array, but not only that. Since we are going to intensively use the style property we could also cache that. I added an array styles where I’m storing the element.style of all the targets.

You can be very aggressive with caching. You may have seen this:

for ( var i = 0, l = els.length; i < l; i++ ) {
...
}

Here we are caching the length of the array into a variable. Be creative here, for example if you need to traverse an object you can cache hasOwnProperty, if you have to use Math you could cache the function you need (sqrt/atan/random/...), and so on and so forth.

The bottom line is: cache everything you can, especially inside loops, even more if it's something related to the DOM.

The new demo goes at a spectacular 180FPS on desktop (if I remove the 60fps constraint), but it's still slow on our beloved iPad.

Use Hardware Accelerated CSS properties (with a catch)

At this point we have to unleash the power of the GPU and use hardware accelerated CSS properties. In this case transform:translateX() (and we add translateZ(0) to ensure that the element is really hardware accelerated).

This last change skyrocketed the demo to 210 FPS on desktop and an acceptable 30 FPS on iPad (remember it's an old gen device). This is pretty impressive but it comes to a price. Hardware acceleration is very demanding and should be handled with care.

You can't put the world on the hardware layer, you don't simply have enough resource for that; the browser will eventually and unexpectedly crash! Keep the browser light. Don't use the hardware layer unless you really need it and only if you need really smooth transitions. Do not throw top/left away just now, actually keep using them as much as you can knowing that when you really need it, there's transform.

requestAnimationFrame is (not) your master

So far we used setTimeout, you may have heard of the magic pill requestAnimationFrame. An article of its own should be dedicated to it, just let's say that requestAnimationFrame offers a more reliable loop than setTimeout/Interval does. The loop is also aligned with the screen refresh rate, resulting in tearing-free, smooth animations. And here you can hear the sound of a "but".

setTimeout is awful but it's best effort, requestAnimationFrame goes at a constant pace. The good of the latter is that the browser is more relaxed, the benefit of the former is that it is executed as fast as the CPU can do.

I'm not suggesting to drop requestAnimationFrame, just (again) make your own tests. With the polite rAF our demo goes at 25FPS instead of 30 for example. 5 FPS are well worth a relaxed renderer, but still there are situations where you might want to use the plain old timeout.

Use transitions

Tweening CSS properties is not usually a good idea. So far we are altering the position of the elements by a small amount at every cycle. We are asking the browser to repaint the screen 50 times every 16ms.

With transitions we change the style property just once per element instead of 3000 times per seconds. That's a huge difference.

The transition demo is probably the fastest we can go with the current installation. I removed the FPS count because we are not in a cycle anymore.

So basically avoid using tween libraries.

Going further

We've gove from 8FPS to 60 in just 6 steps, not bad but there are other things we could do to uber optimize our code; they are not directly applicable to this demo but are worth mentioning nonetheless.

Give it a rest

The most important thing you will learn about the mobile browser is that it is lazy. Changes to element styles take forever (in machine time) and you should always give the renderer a rest before executing multiple variations to the DOM.

A very common scenario is:

var x = 100;

setTimeout(function () {
    element.style.left = x + 'px';
}, 100);

If your code seems not to be working for any clear reason, try to give it a rest. You'll be surprised.

I also found out that sometimes it is better to wait at least 300ms from document load before firing complex code.

Keep event listener callbacks light

Events are bastard. Especially touchmove and devicemotion. They are fired like crazy and they are very sensible to DOM changes. Basically don't do anything in the event callback, just save a variable and use the value later, maybe in a requestAnimationFrame loop.

Really fake pseudo code:

var x;

element.addEventListener('touchmove', function (e) {
    x = e.changedTouches[0].pageX;
}

function loop () {
    requestAnimationFrame(loop);
	
    element.style.left = x + 'px';
}

Also remember: don't use createElement if you are listening to touch events. Ever.

Event loading/unloading

A thing that I often see doing is event loading/unloading based on user action. Very common is loading the touchMove event on touchStart and unloading it on touchEnd.

It makes sense, but if you follow the above rule (Keep event listener callbacks light), I feel it is better to just leave the events registered. This is especially true on older Android.

Don't use unnecessary add-ons

Lurking a website code I found this not long ago:

width = $(window).get(0).innerWidth;

That's all jQuery was loaded for. Really?!

We are so accustomed to jQuery (and many other tools) that we do not even stop thinking for a second. It must be something related to the $, everybody loves money, but you don't get wealthier by using $ indiscriminately.

I'm all for using super sassy libraries, just pick the ones you really need, FGS!

I just scratched the surface, if there's interest I could go even deeper, what are your optimization tricks?

/Share the joy

/Reactions

    • Author: Brandon
    • Posted on: 2014/03/08
    • At: 23:34

    This is really, really useful. I’m always looking for new tricks to speed up our JS…we heavily use Extjs 4 so we need all the help we can get. :) Thanks!

    Reply
    • Author: matt
    • Posted on: 2014/03/14
    • At: 06:16

    Very nice article!

    Reply
    • Author: rick
    • Posted on: 2014/03/17
    • At: 08:07

    Something I learned – hiding a wrapping element while its contents are being manipulated. I.e., nested wrappers being changed for whatever reason. Once everything is in place then show them (display:block). That translates into a single repaint instead of several.

    Of course, I’m sure you knew that ;)

    Also, I am happy to see your mention of jQuery – so many people “learn” JavaScript with it yet do not know how to function without it.

    Thanks for the article!

    Reply
    • Author: Val
    • Posted on: 2014/03/20
    • At: 22:27

    Great addition to known best practices in high performance JS.

    Thanks for sharing!

    Reply
  • Hey Matteo,

    great set of tips. I was wondering whether you’ve got any posts planned for animated transitions? Looking forward to working with those in the future.

    - Shawn

    Reply

/Leave a reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>