Back to Home

Implementations of setImmediate: messages, mutation or promises, which is faster?

Good day · % username%! A small study on the topic “what is the best way to put a function / method for processing in a queue” and · as a result · a comparative test · and the final ...

Implementations of setImmediate: messages, mutation or promises, which is faster?



Good day %username%! A small study on the topic “what is the best way to queue a function / method for processing” and, as a result, a comparative test, and the final implementation is similar to a setImmediatefunction. This method is needed for those who want to break the execution of the script so that it does not “hang up” the browser, which is useful with a huge initialization script, parsing a large data array, building a complex structure without resorting to WebWorkers.

To understand: setImmediatethis is the method of the object window, which should call the function passed to it, asynchronously, such setTimeout(fn, 0)where 0 is really 0, and not at least 4. For nodejs programmers, this process.nextTick. Because the method itself (setImmediate) has a clear standard with errors and additional parameters, consider an abstract problemasynchronously execute the passed function / method as quickly as possible .

Research is exclusively within the framework of browser scripts, and the main ones, as in workers, it’s not entirely clear why such fragmentation is necessary, although if necessary, you can try promises and messages.

So, let's find out what works best: postMessage, MutationObserver or Promise?

Study


Someone may be surprised at the presence of mutations ( MutationObserver) in the list, because they are strongly recommended to be avoided in grocery versions of software. Looking ahead: they lie. Draw a study of four methods: setTimeout, postMessage, Promise, MutationObserver.

setTimeout


To begin with, we will name our research method nextTickin honor of the nodejs version of the implementation, so as not to be confused with the original setImmediate, because we will throw error handling and analysis of additional parameters to hell as part of the study. So, what is the easiest and most concise way to implement nextTick? Yes, through the same setTimeout(fn, 0). (hehe, as done here , either from ignorance about process.nextTickwhether from the old version of node)

Let us formulate the method in the study:

var nextTick, nextTickTO;
    nextTickTO = function() {
      var call // метод обхода очереди
        , queue // очередь
        , i // указатель на последний непустой элемент
        , fire // индикатор что запущен асинхронный метод
        , nextTick // метод постановки в очередь и пуска механизма
        ;
      i = 0;
      queue = newArray(16); // массив строго заданной длины
      fire = false;
      call = function() { // пройдёмся по очереди?var len, s, track; // выделяем длину, указатель и дубликат
        track = queue; // дублируем очередь
        len = i; // т.к. массив предвыделен queue.length вернёт всегда 16
        s = 0; // стартовая позиция
        queue = newArray(16); // сразу выделяем новую очередь
        i = 0; // смещаем указатель
        fire = false;
        while (s < len) {
          track[s++](); // опасно: сломайся внутри что и порушим всё
        }
      };
      nextTick = function(fn) {
        queue[i++] = fn;
        if (!fire) {
          fire = true;
          setTimeout(call, 0);
        }
      };
      return nextTick;
    };
    nextTick = nextTickTO();

We created a small closure: it allocates memory for a ready-made array of methods ( queueimmediately of a certain size so that the speed of allocating memory for arrays on different engines does not affect research), a pointer ( i, at the same time, an indicator of the length of the array), a method callthat will “walk” through the array, indicator ( fire) that the asynchronous start method is called and the director himself in the queue nextTick, which returns.
The key point: there is no error handling, checks, as part of the tests we believe in ourselves. Next, we will rely on this template, creating implementations based on other asynchronous techniques.

postMessage way


Then you can find a way through postMessage( here or for example here ), and it is much faster and you could calm down, but everything changes when you really need a very fast nextTickand taking into account the growth of the mobile device market, the need for optimization is enormous.
Let's formulate a test block:
    nextTickPM = function() {
      var fire, i, nextTick, queue;
      i = 0;
      queue = newArray(16);
      fire = false;
      window.onmessage = function(message) { // вместо callvar data, len, s, track;
        data = message.data;
        if (data === 'a') { // может что-то посложнее? А зачем?
          track = queue;
          len = i;
          s = 0;
          queue = newArray(16);
          i = 0;
          fire = false;
          while (s < len) {
            track[s++]();
          }
        }
      };
      nextTick = function(fn) {
        queue[i++] = fn;
        if (!fire) {
          fire = true;
          postMessage('a', '*');
        }
      };
      return nextTick;
    };


What is bad postMessage? It affects a huge message transfer system, checks for domain names, checks for transmitted values, and placing it in separate message queues. Otherwise, the beaver is a beaver.

Promise way


Further, my colleague went along the path of using promises ( Promises), deciding what could be done faster, and he turned out to be right.
Code:
    nextTickPR = function() {
      var call, fire, i, nextTick, p, queue, s;
      if (typeofPromise === "undefined" || Promise === null) {
        return nextTickMO();
      }
      i = 0;
      r = 0; // счётчик вызова call
      queue = newArray(16);
      fire = false;
      p = Promise.resolve();
      call = function() {
        var len, s, track;
        track = queue;
        len = i;
        s = 0;
        queue = newArray(16);
        i = 0;
        fire = false;
        while (s < len) {
          track[s++]();
        }
        if ((r++) % 10 === 0) { // вот тут момент внимания
          p = Promise.resolve();
        }
      };
      nextTick = function(fn) {
        queue[i++] = fn;
        if (!fire) {
          fire = true;
          p = p.then(call);
        }
      };
      return nextTick;
    };


And the method in its implementation (I omit it) turned out to be twice as fast as the messages, bringing it under a single template it turned out to be faster from three times to 10 times (chrome 43)! And then there was a stumble: when testing for 1000 calls, the old firefox started cursing at the length of the recursion and we had to add a call counter call( r) and select a new one Promise.resolve()every 10 calls. Perhaps a different number will do, but in the future, as we will see, you can throw these lines to the goblin (evil spirits already have the setImmediate standard and the call counter).
The detail also left much to be desired: support by browsers, especially mobile ones (to which I, owing to my work, am especially anxious).

MutationObserver way


I took the path of reasoning that we have mutations ( MutationObserver), callbacks are called in them asynchronously, there Nodeis a method (HTMLElement is instance of Node) setAttributethat programmers without hesitation will associate directly with queuing directly, without unnecessary systems checks as a message system. They do not recommend using it for full-fledged nodes that are already built into the DOM, but what if we do not embed in the DOM and neatly live the node in closures? As it turned out so.

Code:
    nextTickMO = function() {
      var a, fire, i, nextTick, observer, queue, s;
      i = 0;
      r = 0; // счётчик для надёжности
      queue = newArray(16);
      fire = false;
      a = document.createElement('a'); // сам узел
      observer = new MutationObserver(function() { // вместо callvar len, s, track;
        track = queue;
        len = i;
        s = 0;
        queue = newArray(16);
        i = 0;
        fire = false;
        while (s < len) {
          track[s++]();
        }
      });
      observer.observe(a, { // слушаем и ничего лишнего
        attributes: true,
        attributeFilter: ['lang']
      });
      nextTick = function(fn) {
        queue[i++] = fn;
        if (!fire) {
          fire = true;
          a.setAttribute('lang', (r++).toString());
        }
      };
      return nextTick;
    };


A node is created (a), a server that listens only to one node and one property (it can take so long, or maybe faster, I don’t know), and to be sure that the event is called, the counter value is assigned to the existing attribute (I don’t know if it’s good the (lang) attribute is selected, like the speed is the same with it and with another attribute).
And the method turned out to be good, also faster than messages.

Path comparison


Mutations support browsers better, but there is a point: when comparing speeds, promises and mutations are tightly coupled: the latest versions of Chrome on the desktop and tablet, as well as the latest opera on the tablet, showed that promises are twice as fast as mutations. Ognelis, the nexus’s native browser (chrome 33), safari on mobile and tablet showed that if they have promises, then they work twice as slow. The most unpleasant thing is that in existing and produced models there may well be no promises. It's like a browser built ... postMessage (not to mention setTimeout) was far behind and only in the 12th firefox, where there were no mutations, it really came in handy.

Fortunately, chrome fell into my hands (39), in which the speed of fulfilling promises and mutations is approximately equal. For the opera, you can agree that if there is a webkit, then let there be promises if they exist. I hope the habraeffect will shed light on what version of the opera is "transitional". Also, the UC browser is not at hand, in general, there is little data, if “curious” details are revealed, I’ll fix it.

There are no IE 10 and 11 at hand, so the native non-standard setImmediate is simply omitted in the study.

Summary:


  1. If the chrome is 39 and younger (the larger version), or if the opera is 15 and younger, then the promises.
  2. Otherwise, a mutation, if any.
  3. If there are no mutations, then messages, and if there are no messages, a timer to zero.


Tests (there are minor discrepancies with the published code, they do not affect performance, but fixing them would have to kill statistics):
jsperf.com/tick

Final implementation of nextTick (prototype name) in the form of tick (cont. Version):
github.com/NightMigera/tick

Read Next