7

I have some processing that is going to take a few seconds so I want to add a visual indicator while it is in progress.

.processing
{
  background-color: #ff0000;
}

<div id="mydiv">
  Processing
</div>

Script:

$("#mydiv").addClass("processing");
// Do some long running processing
$("#mydiv").removeClass("processing");

I naively thought that the class would be applied to the div and the UI would be updated. However, running this in the browser (in Firefox at least) the div is never highlighted. Can someone explain to me why my my div never gets highlighted? The class is added, the processing takes place and then the class is removed; the UI is not updated in between and the user never sees the red background.

I know JS is single-threaded but I'd always presumed the browser rendering would run synchronously as and when the DOM is updated. Is my assumption incorrect?

And more importantly, what is the recommended way to achieve this effect? Do I have to result to using setTimeout to make the slow processing asynchronous and work with a callback? There must be a better way as I really don't have any need for async behaviour here; I just want the UI to refresh.

EDIT:

JSFiddle: http://jsfiddle.net/SE8wD/5/

(Note, you may need to tweak the number of loop iterations to give you a reasonable delay on your own PC)

njr101
  • 9,499
  • 7
  • 39
  • 56
  • 1
    I believe such changes will make it directly to the DOM, but the browser will wait until the JavaScript execution is "idle" before doing any repainting. – pimvdb Mar 23 '12 at 10:58
  • "Do some long running processing" has some asynchronous call? (ajax, settimeout etc) – Luca Rainone Mar 23 '12 at 11:00
  • I can't really see where your code goes wrong, because it seems fine of what you have posted. Maybe there is some `!important` in your external css file. Are you sure the class get's added ( have you inspected it via firebug) and does the process really take that long ? maybe it happens too fast – mas-designs Mar 23 '12 at 11:00
  • @chumkiu havin an ajax or settimout in his code will not work because the add and remove will not wait for the asynchronous to finish ! – mas-designs Mar 23 '12 at 11:01
  • @EvilP exactly. His assumptions is correct, so there is something of wrong somewhere else – Luca Rainone Mar 23 '12 at 11:05
  • There is no asynchrnous processing. Will add JSFiddle – njr101 Mar 23 '12 at 11:49
  • Added JSFiddle. Thanks for any suggestions. – njr101 Mar 23 '12 at 11:58
  • Opera shows a red "refreshing" paragraph while executing the fiddle. – Bergi Mar 23 '12 at 13:53

4 Answers4

2

You probably should put the processing out in a seperate event, like this;

$("#mydiv").addClass("processing");
setTimeout(function(){
   // This runs as a seperate event after
   // Do some long running processing
   $("#mydiv").removeClass("processing");
},1);

That way the browser should redraw the screen with the processing, while the long running step will kick off as a seperate event, and remove the processing message when done.

Soren
  • 14,402
  • 4
  • 41
  • 67
  • Thanks, this is what I suspected. I really hoped there was a way to avoid this force the browser to re-render after changes to the DOM. – njr101 Mar 23 '12 at 14:20
  • warning - I find it starts working after 5ms timeout - anything shorter and chrome still waits. – user2486570 Dec 11 '16 at 21:22
  • @user2486570 -- I doubt it -- JS is single threaded event loop -- so it has only those race conditions you create yourself -- if you see a 5ms problem is is probably because you have something else running with a timer or a DOM ready event that is conflicting with your other code. – Soren Dec 11 '16 at 21:28
  • nope - browser doesn't trigger repaint unless the delay is long enough - I tried just now and actually, even 5 ms isn't enough sometimes. – user2486570 Dec 11 '16 at 22:25
  • my case is with prompt() instead of long running operation, but the result is the same. – user2486570 Dec 11 '16 at 22:25
1

I not recommend to freeze browser for a weight script. In this case I prefer to change the code for not freeze browser and DOM. If in your code there is some loop, you can call a function with a delay (using setInterval) and store some status variable somewhere. For example:

for(var i=0; i<1000000; i++) {
    // do something
}

Can be:

var i = 0;
var intervalID;
var _f = function() {
    // do something
    i++;
    if(i==1000000) clearInterval(intervalID);
}
intervalID  = setInterval(_f,1);

Or something of similar and more optimized. Your code will be a little bit more complex, and slower.. . but so you prevent browser freeze and you can make an advanced preload status bar.

Luca Rainone
  • 16,138
  • 2
  • 38
  • 52
  • Thanks, this was what I meant when I mentioned `setTimeout`. I realise I could do this but I was hoping to avoid the added complexity of async calls. That opens a whole class of other problems where state is indeterminate when the async callback occurs. – njr101 Mar 23 '12 at 12:50
  • I know. You can see also web worker http://www.html5rocks.com/en/tutorials/workers/basics/ – Luca Rainone Mar 23 '12 at 12:52
  • Thanks for the link. Interesting stuff, but unfortunately won't help me as I need cross-browser support. – njr101 Mar 23 '12 at 13:17
  • @njr the last suggest, is not a solution but a "stupid" trick, insert an alert('Please wait'); after addClass, that will force browser rendering :) – Luca Rainone Mar 23 '12 at 14:47
0

Force a redraw. Alternatively, use Soren's code because processing on the UI thread sounds bad...

Community
  • 1
  • 1
Tamara Wijsman
  • 12,198
  • 8
  • 53
  • 82
  • However, jQuery plugins seem to be down at the moment. Added an alternative. It could be that you don't need to defer `n.parentNode.removeChild(n)`; if so, feel free to edit my answer. – Tamara Wijsman Mar 23 '12 at 11:57
  • I tried this on my jsFiddle with no success. I'm obviously not using it properly. Can you add to Fiddle please? – njr101 Mar 23 '12 at 13:00
  • Sorry, but the Fiddle doesn't work. The class is applied and then script aborts with a script error. The "Finished" message is never displayed. – njr101 Mar 23 '12 at 14:17
  • Thanks for your help. I did mention the browser in the OP (it's Firefox, latest version). But is doesn't work for me in Chrome either. In both browsers the red highlighting is displayed and the script aborts. The "Finished" message never appears. I'm looking at this Fiddle http://jsfiddle.net/SE8wD/26/ (this one really works for you?) – njr101 Mar 23 '12 at 14:24
  • Thanks for all your efforts. This certainly works, although it is really just using hide() to provide the callback rather than setTimeout(). It's making an async call either way, which is what I really wanted to avoid. That async call will not fire until the rest of the inline code has been processed which then needs extra synchronisation. – njr101 Mar 23 '12 at 15:19
  • @njr: I think I understand why now, simply because you aren't supposed to be running processing code on the UI thread. Furthermore, a browser can choose when to redraw / reflow... – Tamara Wijsman Mar 23 '12 at 15:24
0

A browser is quite free about when to repaint and when to reflow. Different engines will show different behavior. For further reading, see When does reflow happen in a DOM environment?. In your example, the browser sees you adding a class and removing it directly after that, so he might not do anything visible.

To avoid that, you may apply a force-redraw-hack, or make your computation asynchronous with WebWorkers or setTimeout or something.

Community
  • 1
  • 1
Bergi
  • 630,263
  • 148
  • 957
  • 1,375