9

Apparently, using window.postMessage is a preferred way to queue an async javascript callback over window.setTimeout(fn, 0) across all modern browsers. I could not find a similar comparison between window.postMessage and MessagePort.postMessage (using the same MessageChannel for sending and receiving messages asynchronously). Has anyone seen or done any timing? Does MessagePort.postMessage work for this purpose at all (where available)?

[EDITED] MessagePort.postMessage does work for this, but window.postMessage remains a preffered way, IMO (see my answer).

noseratio
  • 59,932
  • 34
  • 208
  • 486
  • As this still gets views... check out [`queueMicrotask`](https://developer.mozilla.org/en-US/docs/Web/API/queueMicrotask). – noseratio Oct 20 '21 at 00:13

1 Answers1

8

[UPDATE] Added a test for setImmediate and a JSFiddle. Related, there's a cross-browser implementation of setImmediate and ASAP library used by Q for a promise resolution/rejection.

I went ahead and did some timing, using a modified version of David Baron's code, the results are below:

setTimeoutMC - using MessageChannel
setTimeoutPM - using window.postMessage
setTimeout(0) - using setTimer

IE10:

2000 iterations of setTimeoutMC took 126 milliseconds.
2000 iterations of setTimeoutPM took 190 milliseconds.
2000 iterations of setTimeout(0) took 7986 milliseconds.

Chrome v29.0.1547.66:

2000 iterations of setTimeoutMC took 144 milliseconds.
2000 iterations of setTimeoutPM took 81 milliseconds.
2000 iterations of setTimeout(0) took 10589 milliseconds.

Clearly, window.postMessage is the winner here (considering the level of existing cross-browser support for it). The looser is window.setTimeout(fn, 0) and should be avoided wherever possible.

Code:

<!DOCTYPE html>
<html>
<head>
    <!-- http://stackoverflow.com/q/18826570/1768303 -->
    <!-- based on http://dbaron.org/log/20100309-faster-timeouts -->
    <!-- requires IE10 or Chrome. Firefox doesn't support MessageChannel yet -->
    <title></title>
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <script type="text/javascript">

        // setTimeoutMC via MessageChannel

        (function () {
            "use strict";
            var i = 0;
            var timeouts = {};
            var setApiName = "setTimeoutMC";
            var clearApiName = "clearTimeoutMC";

            var channel = new MessageChannel();

            function post(fn) {
                if (i === 0x100000000) // max queue size
                    i = 0;
                if (++i in timeouts)
                    throw new Error(setApiName + " queue overflow.");
                timeouts[i] = fn;
                channel.port2.postMessage(i);
                return i;
            }

            channel.port1.onmessage = function (ev) {
                var id = ev.data;
                var fn = timeouts[id];
                if (fn) {
                    delete timeouts[id];
                    fn();
                }
            }

            function clear(id) {
                delete timeouts[id];
            }

            channel.port1.start();
            channel.port2.start();

            window[setApiName] = post;
            window[clearApiName] = clear;
        })();

        // setTimeoutPM via window.postMessage

        (function () {
            "use strict";
            var i = 0;
            var timeouts = {};
            var setApiName = "setTimeoutPM";
            var clearApiName = "clearTimeoutPM";
            var messageName = setApiName + new Date().getTime();

            function post(fn) {
                if (i === 0x100000000) // max queue size
                    i = 0;
                if (++i in timeouts)
                    throw new Error(setApiName + " queue overflow.");
                timeouts[i] = fn;
                window.postMessage({ type: messageName, id: i }, "*");
                return i;
            }

            function receive(ev) {
                if (ev.source !== window)
                    return;
                var data = ev.data;
                if (data && data instanceof Object && data.type === messageName) {
                    ev.stopPropagation();
                    var id = ev.data.id;
                    var fn = timeouts[id];
                    if (fn) {
                        delete timeouts[id];
                        fn();
                    }
                }
            }

            function clear(id) {
                delete timeouts[id];
            }

            window.addEventListener("message", receive, true);
            window[setApiName] = post;
            window[clearApiName] = clear;
        })();

        // timing

        function runtest() {
            var output = document.getElementById("output");
            var outputText = document.createTextNode("");
            output.appendChild(outputText);
            function printOutput(line) {
                outputText.data += line + "\n";
            }

            var n = 2000;
            var i = 0;
            var startTime = Date.now();
            setTimeoutMC(testMC);

            function testMC() {
                if (++i === n) {
                    var endTime = Date.now();
                    printOutput(n + " iterations of setTimeoutMC took " + (endTime - startTime) + " milliseconds.");
                    i = 0;
                    startTime = Date.now();
                    setTimeoutPM(testPM, 0);
                } else {
                    setTimeoutMC(testMC);
                }
            }

            function testPM() {
                if (++i === n) {
                    var endTime = Date.now();
                    printOutput(n + " iterations of setTimeoutPM took " + (endTime - startTime) + " milliseconds.");
                    i = 0;
                    startTime = Date.now();
                    setTimeout(test, 0);
                } else {
                    setTimeoutPM(testPM);
                }
            }

            function test() {
                if (++i === n) {
                    var endTime = Date.now();
                    printOutput(n + " iterations of setTimeout(0) took " + (endTime - startTime) + " milliseconds.");
                }
                else {
                    setTimeout(test, 0);
                }
            }
        }
    </script>
</head>

<body onload="runtest()">
    <pre id="output"></pre>
</body>
</html>
noseratio
  • 59,932
  • 34
  • 208
  • 486
  • 3
    On browsers that support Promise (which is most of the major ones, according to [MDN Promise docs](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)), Promise.resolve may be even faster. See my [modified Fiddle](http://jsfiddle.net/wPCN4/7/), where it's 5-6x faster in the latest Firefox nightly build in my testing. – Myk Melez Jan 24 '15 at 20:57
  • @MykMelez Wow. Good for native Promises! Why can't setTimeout be as fast? Is there a reason? setTimeout can also be replaced with a Promise.resolve implementation, but would that cause problems? – trusktr Mar 06 '16 at 05:19
  • 2
    @trusktr https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/setTimeout#Nested_timeouts_forced_to_%3E4ms has the details, but the summary is that successive `setTimeout(fn, 0)` calls are "clamped" to a minimum timeout of 4ms, so it'll never take less than that amount of time to resolve them. – Myk Melez Mar 07 '16 at 21:51
  • My question is it measuring speed of execution to make those calls AND waiting until the are completed or just making the calls. Because of the huge difference I am just wondering if it is an apples to apples comparison. – Joey Garcia Nov 04 '16 at 17:23
  • Does window.postMessage event factor in tasks that are already in the event task queue? If not, then I can understand the time difference and is not an apple to apples comparison. – Joey Garcia Nov 04 '16 at 17:31
  • @MykMelez Wow, I knew so little back then. Of course replacing a setTimeout implementation with promise.resolve can cause problems: tasks would no longer be macrotasks that leave room for the browser to operate (to handle user input, paint, etc), but microtasks that will block the browser from performing any native tasks between the scheduling call and the task execution. – trusktr Oct 19 '21 at 15:06
  • @trusktr and @JoeyGarcia yeah it's been a while :) E.g., we now have [`queueMicrotask`](https://developer.mozilla.org/en-US/docs/Web/API/queueMicrotask) in Node and browsers. Some great reads: https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules, https://v8.dev/blog/fast-async Oh, and IE is nearly gone :) – noseratio Oct 19 '21 at 21:19
  • 1
    @noseratio Nice links! "Oh, and IE is nearly gone :)" Yaaaaay. – trusktr Oct 20 '21 at 06:50