Hello, fellow web perf enthusiast! Contribute to the 2024 edition happening this December. Click.

Web Performance Calendar

The speed geek's favorite time of year
2013 Edition
ABOUT THE AUTHOR
Andrea Giammarchi

Andrea Giammarchi (@WebReflection) is a mobile web engineer at Twitter, previously working at Facebook and also at Nokia where he was mainly focused on the Mobile HTML5 Here map engine. Active contributor and web-standards promoter ), Andrea often talked about performance optimizations specially for the mobile world, with a concrete experience on such field since Android 1.5 and other smartphones in the market. These days, Andrea is probably writing some code which ideal aim is to work from latest iOS Safari Mobile, down to IE9 Mobile, passing through some Android 2.3 or greater (… and Firefox OS, and Blackberry 10, and Asha, and UCBrowser … and … )

The task that Nathan LeClaire bombed during his interview is only one of many common situations that could be improved on the internet we surf these days.

Unfortunately, those situations are rarely described or resolved in daily tutorials and even most common books, so here one approach with multiple possible purposes.

More Than Just Asynchronous

Generally speaking, every time we set an event listener, either on the server or the client, we already believe we are doing a good job placing some non-blocking bit that will be executed in the future.

//# the worst assumption ever!
field.addEventListener('input', function(e){
  // will be executed asynchronously
  // we can do everything we want/need to
  // it's asynchronous, hoooorayyy!!!
});

Back to the Nathan’s case, the action performed per each character inserted in the field should have fired an asynchronous Ajax call to the server, resulting indeed in a very problematic and pointless stress for both the client, in charge of creating N network requests, and the server, which might do very complex queries and normalizations before replying with any meaningful data.

This Is Not Only About The Amount Of Requests

Moreover, what libraries and frameworks rarely do or give us, is the ability to xhr.abort() the ongoing network requests so that last one will have unconditional precedence over the previous one, in case it’s still processing.

In fact, it is possible and quite common that 2 asynchronous requests will come back in the future with a different order than expected. Maybe the first request was more difficult to digest or the connection in that very specific moment wasn’t so good, while the second request was straight forward.

Accordingly, the user will see hints and results for the second request, but once the first one will be resolved, the user might get confused by new results, in terms of time, and for an elder search, in term of input.

How To Deal With Delayed Tasks

Solving one problem per time, when Nathan was looking for a solution, he couldn’t think about any magic plugin or library because probably there’s not much out there … so here a very simple one, it’s called Delayed and it works like this:

field.addEventListener('input', Delayed(function(e){
  // will be executed asynchronously
  // only after 300 milliseconds the user
  // is not adding text
  alert(this.value);
}, 300));

You can test above example directly and read what you wrote not while you are writing, but only once you stopped.

Not Only Ajax

Input validation might be much more complicated. There could be some quality check, some computation to do or some spell check.

As example, if your browser marks with red wrong words and you write a very big chunk of text inside a textarea with a different language than expected, you might realize that every time you type something or you copy and paste that text inside the area the browser will slow down a little bit or even freeze for an instant the moment it parses the whole content and marks it as red.

Try it yourself using Latin as possible language your browser will not recognize and you’ll notice that when there is too much text in there, the browser will probably even stop correcting it since that would be too expensive.

Similar operations are performed in every textarea with implemented parsing, color changing, Markdown recognizing, any sort of feature. We cannot do all those changes while the user is typing, it does not make sense and it will slow down the user too.

Things might get even worst if we think about excel like HTML grids where a little change in a small cell, might result in a whole scene/graph re-generation, pie charts drawing, etc etc.

In all these cases we should still think about delaying the execution of whatever we’d like to do which will result in a perceived faster interactions from the user perspective.

Gaming And More

In case we are tracking movements and doing some costly operation meanwhile, it’s very possible to ruin the (in)famous 60FPS achievement. Considering that events are usually fired at such rate, everything that will be higher than 17ms as interval will be already good to be considered as executed “after”.
In this mouse tracking example, you can see some simple animation performed only when your mouse stops moving. In all those cases where this is the desired behavior, the Delayed approach can be recycled as well.

document.documentElement
  .addEventListener('mousemove', Delayed(function(e){
  var p = document.querySelector('p');
  p.style.cssText = 'top:' + (e.pageY - p.offsetHeight) + 'px;' +
                    'left:' + Math.round(
                      e.pageX - p.offsetWidth / 2
                    ) + 'px;';
}, 30));

Ultra Combos

Another common scenario where delayed computation is desired or needed, is the one with combination of multiple inputs.
This could be the famous Konami Code or some Street Fighter combo.

In these cases the usage of a delayed function can be used to both mark the combo time expired and/or execute it.

A simple Konami Code example could then be like the following:

document.documentElement.addEventListener(
  'keyup',
  // a better handler than a callback
  {
    handleEvent: function (e) {
      // store the key, the only "expensive" operation
      this._keys.push(e.keyCode || e.charCode || e.key);
      // invoke the delayed method
      this._comboMaster();
    },
    // one (or more) combo key/name pair
    _combo: {
      '38:38:40:40:37:39:37:39:66:65': 'konamicode'
    },
    // the list of combo-keys
    _keys: [],
    // the delayed method
    _comboMaster: Delayed(function(){
      // grab the combo
      var combo = this._keys.join(':');
      // clear up the list of _keys before doing anything else
      this._keys.length = 0;
      // check if the combo is present
      if (this._combo.hasOwnProperty(combo)) {
        // trigger it!
        window.dispatchEvent(
          new CustomEvent('combo:' + this._combo[combo])
        );
      }
    })  // using the default 500ms but
        // real combos are usually in less than 250
  },
  true  // capturing before all other events
);

The same logic could be applied through touch, mouse, or pointer events where during the change/move we can store the list of points and/or the list of buttons and only after a delay we can decide if that was a combo or not.

The advantage in using delayed functions is that the logic behind is transparent and everything looks like we are directly invoking or checking inline all we need to do in order to react.

The Cost Of A Timeout

If you are worried about how costly could be this setting and dropping timers at high rates, I’ve created a jsperf case to monitor your target device. Your device should be able to handle thousands of timers per seconds so I believe benefits over such tiny slow-down operation are a win so … go and implement All The Combos without regrets!

Conclusion

As summary, every time there is an expensive operation there’s always a way to split its cost in smaller pieces and using delayed tasks, with or without trapping useful data around, is already one part of such optimization.

There’s also much more to consider so it’s good to have all cons on the plate too:

  • beware of never-executed tasks. As it is for the mousemove example if you never stop moving the action will never happen.
  • if there is asynchronous logic involved within the delayed method, be sure to enrich the control of the dl.clear() operation so that internal tasks can be canceled too
  • thousands of timers aren’t a good idea neither, we might consider to put all delayed executions inside a requestAnimationFrame so that the browser could be able to extra-optimize on top of our delayed optimization

One solution to these problems would be simply to improve my tiny library.
However, as abstract concept, and generic article, it is good to know there are possible gotchas to take care of 😉