📜 ⬆️ ⬇️

Implements setImmediate: messages, mutations, or promises, which is faster?



Good day, %username% ! A small study on "what way to put a function / method for processing in a queue more efficiently" and, as a result, a comparative test, and the final implementation of a function similar to setImmediate . This method is necessary for those who want to break the execution of the script so that it does not “hang” the browser, which can be useful with a huge initialization script, parsing a large amount of data, building a complex structure without resorting to WebWorkers.

To understand: setImmediate is a method of a window object that should call a function passed to it asynchronously, such a setTimeout(fn, 0) , where 0 is actually 0, not at least 4. For nodejs programmers, this is process.nextTick . Since the method itself (setImmediate) has a clear standard with errors and additional parameters; consider the abstract task of asynchronous execution of the passed function / method as quickly as possible .
')
Studies are exclusively within the framework of browser scenarios, and the main ones, since in workers, it’s not quite clear why such a crushing, 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 by the presence of mutations ( MutationObserver ) in the list, because they are strongly recommended to avoid in product versions of software. Looking ahead: lie. Let's conduct research of four methods: setTimeout , postMessage , Promise , MutationObserver .

setTimeout


First, let's call our nextTick research method in honor of the nodejs version of the implementation, so as not to be confused with the original setImmediate, since error handling and analysis of additional parameters will be discarded to hell within the framework of the study. So, what is the easiest and nextTick way to implement nextTick ? Yes, through the same setTimeout(fn, 0) . (hehe, as done here , either from ignorance about process.nextTick or from the old version of node)

We formulate the method in the study:

  var nextTick, nextTickTO; nextTickTO = function() { var call //    , queue //  , i //      , fire //      , nextTick //        ; i = 0; queue = new Array(16); //     fire = false; call = function() { //   ? var len, s, track; //  ,    track = queue; //   len = i; // ..   queue.length   16 s = 0; //   queue = new Array(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 ( queue immediately 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 array length), a call method that will “go through” by array, indicator ( fire ), that the asynchronous start method is called by the producer himself in the nextTick , which is returned.
The key point: there is no error handling, checks, in the framework of tests, we "believe" 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 nextTick and considering the growth of the mobile device market, the need for optimization is enormous.
We formulate a test unit:
  nextTickPM = function() { var fire, i, nextTick, queue; i = 0; queue = new Array(16); fire = false; window.onmessage = function(message) { //  call var data, len, s, track; data = message.data; if (data === 'a') { //  - ?  ? track = queue; len = i; s = 0; queue = new Array(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 on domain names, checks on transmitted values, setting in separate message queues. The rest is a sharor beaver.

Promise way


Then my colleague took the path of using promises ( Promises ), deciding that it was possible and faster, and he was right.
Code:
  nextTickPR = function() { var call, fire, i, nextTick, p, queue, s; if (typeof Promise === "undefined" || Promise === null) { return nextTickMO(); } i = 0; r = 0; //   call queue = new Array(16); fire = false; p = Promise.resolve(); call = function() { var len, s, track; track = queue; len = i; s = 0; queue = new Array(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 (omitted) turned out to be twice as fast as the messages, bringing it under a single pattern from 3 times to 10 times faster (43 chrome)! And then there was a stumbling block: when testing for 1000 calls, the old firelis began to swear at the length of the recursion and had to add a call counter call ( r ) and select the new Promise.resolve() every 10 calls. Perhaps a different number will do, but later, as we shall see, you can throw these lines to the line (the evil one already has the setImmediate standard and the call counter).
It also left a better part to be desired: support by browsers, especially mobile (to which I, on duty, are especially anxious).

MutationObserver way


I went the way of thinking that we have mutations ( MutationObserver ), callbacks are called asynchronously in them, Node elements have (HTMLElement is an instance of Node) setAttribute method, which programmers, without hesitation, will associate the same with direct queuing, without unnecessary verification systems, like a messaging system. It is not recommended to use for full-fledged nodes that are already built into the DOM, but what if we don’t 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 = new Array(16); fire = false; a = document.createElement('a'); //   observer = new MutationObserver(function() { //  call var len, s, track; track = queue; len = i; s = 0; queue = new Array(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 (a) is created, a server that listens to only one node and one property (it may take longer, or maybe faster, I don’t know), and to be sure that the event will be triggered, the counter value is assigned to the existing attribute (I don’t know if Attribute selected (lang), like speed with it and with another attribute the same).
And the method turned out to be good, also faster than messages

Comparing paths


Mutations in browsers support are better, but there is a moment: when comparing speeds, promises and mutations are tightly coupled: the latest chrome versions on the desktop and tablet, as well as the opera from the last versions on the tablet showed that promises are twice as fast as mutations. Ognelis, the native browser of the nexus (chrome 33), mobile and tablet safaris showed that if they have any promises, they are two times slower. The most annoying thing is that there may well be no promises in existing and released models. This is how the browser is compiled ... postMessage (not to mention setTimeout) was far behind and only in the 12th firewall, where there are no mutations, it really came in handy.

Fortunately, I got chrome (39) in which the speed of fulfilling promises and mutations are approximately equal. For the opera, you can agree that if there is a webkit, then there will be promises if there are any. I hope habraeffekt will shed light on what version of the opera "transitional". Also, the UC browser is not at hand, in general, there is not enough data, if “curious” details are revealed, I will correct it.

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

Summary:


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


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

The final implementation of nextTick (the name of the prototype) in the form of tick (prod. Version):
github.com/NightMigera/tick

Source: https://habr.com/ru/post/275023/


All Articles