Asynchronous JS in Firefox (37.0.2) is broken. I mean it.
It started during routine debugging: something went wrong with form validation and I've stopped at breakpoint in Firefox debugger. And then I saw something that amazed me. The financial quotes on the page (taken from WebSocket live stream) were updating. While I was standing on a break-point and main event loop should have been stopped something updated the DOM.
It doesn't look like plain old "one thread with event loop" JavaScript. So I had to investigate that.
I made a test. Various asynchronous stuff is started: XHR
, WebWorker
, postMessage
, WebSocket
and plain old setTimeout
,
each logging a text line into a <pre>
upon completion. And finally an ordinary synchronous expression.
Here's the code:
function _log(message){
// Helper function, appends text into a page
document.querySelector('pre').textContent += message + '\n';
}
function doAsyncStuff(){
// XHR
var xhr = new XMLHttpRequest();
xhr.open('GET', 'samplexhr.txt', true);
xhr.onreadystatechange = function(){
if (xhr.readyState === 4){
_log('Async: XHR');
}
};
xhr.send();
// WebWorker
var worker = new Worker('sampleworker.js');
worker.onmessage = function(){
_log('Async: WebWorker');
};
// PostMessage
window.addEventListener('message', function(e){
_log('Async: postMessage');
}, false);
window.postMessage('Oh, hi!', '*');
// WebSocket
var ws = new WebSocket("ws://echo.websocket.org/");
ws.onopen = function() {
_log('Async: Websocket');
ws.close();
};
// setTimeout
setTimeout(function(){
_log('Async: setTimeout');
}, 0);
}
doAsyncStuff();
// Just write into console
_log('Synchronous');
If you're an experienced JS developer, you probably can tell what should be printed into <pre>
.
First line should be"Synchronous"
and all the others should be printed below because they're…
ahem… asynchronous.
Note that "async" lines can be printed in any particular order: as soon as browser unpredictably decides the process
is done.
So we create all the files (sampleworker.js
and samplexhr.txt
) and then run the code above in Firefox and get the result.
All the files mentioned are right here in this gist, you can take it and try it yourself. These files should be served via HTTP, some tests, likepostMessage
, won't work if index.html
is opened directly from disk.
Synchronous
Async: postMessage
Async: setTimeout
Async: XHR
Async: WebWorker
Async: Websocket
Just as expected! But now we'll set a breakpoint just before the synchronous log call, like this:
doAsyncStuff();
debugger;
// Just write into console
_log('Synchronous');
The contents of doAsyncStuff
function wasn't changed
Script execution almost immediately stops at the breakpoint. And we get the following on the screen:
Async: postMessage
Async: XHR
Async: Websocket
Then we resume the script execution and following lines are appended:
Synchronous
Async: WebWorker
Async: setTimeout
The heck is that? postMessage
, XHR
and Websocket
just jumped the queue leaving unfinished synchronous code behind.
The scripts runs quite the opposite way it should, just because debugger is enabled.
This is not how debuggers work, right, Mozilla?
Maybe it's not a debugger issue, but the way Mozilla erroneously optimize the event loop?
Let's try a busy loop, an artificial delay big enough for async stuff to finish:
doAsyncStuff();
var targetTimestamp = Date.now() + 5000;
while (Date.now() < targetTimestamp){
// Do nothing
}
_log('Synchronous');
Works as charm. Firefox freezes for 5 seconds, then spitting out lines in a correct order.
Synchronous
Async: postMessage
Async: setTimeout
Async: XHR
Async: WebWorker
Async: Websocket
Now let's try another event loop blocker, a synchronous XHR. 100 times, just to be sure it's enough:
doAsyncStuff();
for (var i=0; i<100; i++){
var xhr = new XMLHttpRequest();
xhr.open('GET', 'samplexhr.txt', false); // Synchronous
xhr.send();
}
_log('Synchronous');
The result:
Async: postMessage
Async: XHR
Async: Websocket
Synchronous
Async: WebWorker
Async: setTimeout
Yay! Same incorrect behavior again. And no debugger is involved at this time.
And the last, the most old and infamous method, an alert()
. Here's the code:
doAsyncStuff();
alert('alert() is synchronous!');
_log('Synchronous');
Here's a result. Alert dialog is shown and the text on a page reads:
Async: postMessage
Async: XHR
Async: WebWorker
Async: Websocket
Then after OK button is pressed two lines appears:
Async: setTimeout
Synchronous
This time synchronous code ran the last, even setTimeout
somehow managed to outrun it.
Previous results were incorrect, this one is absolutely wrong.
Of course, it is quite unlikely one can encounter alert()
or synchronous XHR in the wild these days.
But it would be great if this stuff (although deprecated) didn't ruin the program flow.
But even if you don't want to use these outdated anti-patterns (and you shouldn't), it would be great... no, it's absolutely necessary for debugger to work in an expected way.
I've got something about async execution for Chrome, too.
setTimeout(function(){
console.log(1);
}, 10);
setTimeout(function(){
console.log(2);
}, 5);
How should this code work?
Second setTimeout
has lower timeout value, so it should run first, even if main loop is blocked for more than 10 seconds.
So the result should be:
2
1
It works everywhere, with one exception. In Chrome when page is not focused, it is called in the order timeouts were defined:
1
2