DOM refresh on long running function

27,298

Solution 1

SOLVED IT!! No setTimeout()!!!

Tested in Chrome 27.0.1453, Firefox 21.0, Internet 9.0.8112

$("#btn").on("mousedown",function(){
$('#btn').html('working');}).on('mouseup', longFunc);

function longFunc(){
  //Do your long running work here...
   for (i = 1; i<1003332300; i++) {}
  //And on finish....  
   $('#btn').html('done');
}

DEMO HERE!

Solution 2

Webpages are updated based on a single thread controller, and half the browsers don't update the DOM or styling until your JS execution halts, giving computational control back to the browser. That means if you set some element.style.[...] = ... it won't kick in until your code finishes running (either completely, or because the browser sees you're doing something that lets it intercept processing for a few ms).

You have two problems: 1) your button has a <span> in it. Remove that, just set .innerHTML on the button itself. But this isn't the real problem of course. 2) you're running very long operations, and you should think very hard about why, and after answering the why, how:

If you're running a long computational job, cut it up into timeout callbacks (or, in 2019, await/async - see note at the end of this anser). Your examples don't show what your "long job" actually is (a spin loop doesn't count) but you have several options depending on the browsers you take, with one GIANT booknote: don't run long jobs in JavaScript, period. JavaScript is a single threaded environment by specification, so any operation you want to do should be able to complete in milliseconds. If it can't, you're literally doing something wrong.

If you need to calculate difficult things, offload it to the server with an AJAX operation (universal across browsers, often giving you a) faster processing for that operation and b) a good 30 seconds of time that you can asynchronously not-wait for the result to be returned) or use a webworker background thread (very much NOT universal).

If your calculation takes long but not absurdly so, refactor your code so that you perform parts, with timeout breathing space:

function doLongCalculation(callbackFunction) {
  var partialResult = {};
  // part of the work, filling partialResult
  setTimeout(function(){ doSecondBit(partialResult, callbackFunction); }, 10);
}

function doSecondBit(partialResult, callbackFunction) {
  // more 'part of the work', filling partialResult
  setTimeout(function(){ finishUp(partialResult, callbackFunction); }, 10);
}

function finishUp(partialResult, callbackFunction) {
  var result;
  // do last bits, forming final result
  callbackFunction(result);
}

A long calculation can almost always be refactored into several steps, either because you're performing several steps, or because you're running the same computation a million times, and can cut it up into batches. If you have (exaggerated) this:

var resuls = [];
for(var i=0; i<1000000; i++) {
  // computation is performed here
  if(...) results.push(...);
}

then you can trivially cut this up into a timeout-relaxed function with a callback

function runBatch(start, end, terminal, results, callback) {
  var i;
  for(var i=start; i<end; i++) {
    // computation is performed here
    if(...) results.push(...);      } 
  if(i>=terminal) {
    callback(results);
  } else {
    var inc = end-start;
    setTimeout(function() {
      runBatch(start+inc, end+inc, terminal, results, callback);
    },10);
  }
}

function dealWithResults(results) {
  ...
}

function doLongComputation() {
  runBatch(0,1000,1000000,[],dealWithResults);
}

TL;DR: don't run long computations, but if you have to, make the server do the work for you and just use an asynchronous AJAX call. The server can do the work faster, and your page won't block.

The JS examples of how to deal with long computations in JS at the client are only here to explain how you might deal with this problem if you don't have the option to do AJAX calls, which 99.99% of the time will not be the case.

edit

also note that your bounty description is a classic case of The XY problem

2019 edit

In modern JS the await/async concept vastly improves upon timeout callbacks, so use those instead. Any await lets the browser know that it can safely run scheduled updates, so you write your code in a "structured as if it's synchronous" way, but you mark your functions as async, and then you await their output them whenever you call them:

async doLongCalculation() {
  let firstbit = await doFirstBit();
  let secondbit = await doSecondBit(firstbit);
  let result = await finishUp(secondbit);
  return result;
}

async doFirstBit() {
  //...
}

async doSecondBit...

...

Solution 3

As of 2019 one uses double requesAnimationFrame to skip a frame instead of creating a race condition using setTimeout.

function doRun() {
    document.getElementById('app').innerHTML = 'Processing JS...';
    requestAnimationFrame(() =>
    requestAnimationFrame(function(){
         //blocks render
         confirm('Heavy load done') 
         document.getElementById('app').innerHTML = 'Processing JS... done';
    }))
}
doRun()
<div id="app"></div>

As an usage example think of calculating pi using Monte Carlo in an endless loop:

using for loop to mock while(true) - as this breaks the page

function* piMonteCarlo(r = 5, yield_cycle = 10000){

  let total = 0, hits = 0, x=0, y=0, rsqrd = Math.pow(r, 2);
  
  while(true){
  
     total++;
     if(total === Number.MAX_SAFE_INTEGER){
      break;
     }
     x = Math.random() * r * 2 - r;
     y = Math.random() * r * 2 - r;
     
     (Math.pow(x,2) + Math.pow(y,2) < rsqrd) && hits++;
     
     if(total % yield_cycle === 0){
      yield 4 * hits / total
     }
  }
}

let pi_gen = piMonteCarlo(5, 1000), pi = 3;

for(let i = 0; i < 1000; i++){
// mocks
// while(true){
// basically last value will be rendered only
  pi = pi_gen.next().value
  console.log(pi)
  document.getElementById('app').innerHTML = "PI: " + pi
}
<div id="app"></div>

And now think about using requestAnimationFrame for updates in between ;)

function* piMonteCarlo(r = 5, yield_cycle = 10000){

  let total = 0, hits = 0, x=0, y=0, rsqrd = Math.pow(r, 2);
  
  while(true){
  
     total++;
     if(total === Number.MAX_SAFE_INTEGER){
      break;
     }
     x = Math.random() * r * 2 - r;
     y = Math.random() * r * 2 - r;
     
     (Math.pow(x,2) + Math.pow(y,2) < rsqrd) && hits++;
     
     if(total % yield_cycle === 0){
      yield 4 * hits / total
     }
  }
}

let pi_gen = piMonteCarlo(5, 1000), pi = 3;


function rAFLoop(calculate){

  return new Promise(resolve => {
    requestAnimationFrame( () => {
    requestAnimationFrame(() => {
      typeof calculate === "function" && calculate()
      resolve()
    })
    })
  })
}
let stopped = false
async function piDOM(){
  while(stopped==false){
    await rAFLoop(() => {
      pi = pi_gen.next().value
      console.log(pi)
      document.getElementById('app').innerHTML = "PI: " + pi
    })
  }
}
function stop(){
  stopped = true;
}
function start(){
  if(stopped){
    stopped = false
    piDOM()
  }
}
piDOM()
<div id="app"></div>
<button onclick="stop()">Stop</button>
<button onclick="start()">start</button>

Solution 4

As described in the "Script taking too long and heavy jobs" section of Events and timing in-depth (an interesting reading, by the way):

[...] split the job into parts which get scheduled after each other. [...] Then there is a “free time” for the browser to respond between parts. It is can render and react on other events. Both the visitor and the browser are happy.

I am sure that there are many times in which a task cannot be splitted into smaller tasks, or fragments. But I am sure that there will be many other times in which this is possible too! :-)

Some refactoring is needed in the example provided. You could create a function to do a piece of the work you have to do. It could begin like this:

function doHeavyWork(start) {
    var total = 1000000000;
    var fragment = 1000000;
    var end = start + fragment;

    // Do heavy work
    for (var i = start; i < end; i++) {
        //
    }

Once the work is finished, function should determine if next work piece must be done, or if execution has finished:

    if (end == total) {
        // If we reached the end, stop and change status
        document.getElementById("btn").innerHTML = "done!";
    } else {
        // Otherwise, process next fragment
        setTimeout(function() {
            doHeavyWork(end);
        }, 0);
    }            
}

Your main dowork() function would be like this:

function dowork() {
    // Set "working" status
    document.getElementById("btn").innerHTML = "working";

    // Start heavy process
    doHeavyWork(0);
}

Full working code at http://jsfiddle.net/WsmUh/19/ (seems to behave gently on Firefox).

Solution 5

If you don't want to use setTimeout then you are left with WebWorker - this will require HTML5 enabled browsers however.

This is one way you can use them -

Define your HTML and an inline script (you don't have to use inline script, you can just as well give an url to an existing separate JS file):

<input id="start" type="button" value="Start" />
<div id="status">Preparing worker...</div>

<script type="javascript/worker">
    postMessage('Worker is ready...');

    onmessage = function(e) {
        if (e.data === 'start') {
            //simulate heavy work..
            var max = 500000000;
            for (var i = 0; i < max; i++) {
                if ((i % 100000) === 0) postMessage('Progress: ' + (i / max * 100).toFixed(0) + '%');
            }
            postMessage('Done!');
        }
    };
</script>

For the inline script we mark it with type javascript/worker.

In the regular Javascript file -

The function that converts the inline script to a Blob-url that can be passed to a WebWorker. Note that this might note work in IE and you will have to use a regular file:

function getInlineJS() {
    var js = document.querySelector('[type="javascript/worker"]').textContent;
    var blob = new Blob([js], {
        "type": "text\/plain"
    });
    return URL.createObjectURL(blob);
}

Setup worker:

var ww = new Worker(getInlineJS());

Receive messages (or commands) from the WebWorker:

ww.onmessage = function (e) {
    var msg = e.data;

    document.getElementById('status').innerHTML = msg;

    if (msg === 'Done!') {
        alert('Next');    
    }
};

We kick off with a button-click in this demo:

document.getElementById('start').addEventListener('click', start, false);

function start() {
    ww.postMessage('start');
}

Working example here:
http://jsfiddle.net/AbdiasSoftware/Ls4XJ/

As you can see the user-interface is updated (with progress in this example) even if we're using a busy-loop on the worker. This was tested with an Atom based (slow) computer.

If you don't want or can't use WebWorker you have to use setTimeout.

This is because this is the only way (beside from setInterval) that allow you to queue up an event. As you noticed you will need to give it a few milliseconds as this will give the UI engine "room to breeth" so-to-speak. As JS is single-threaded you cannot queue up events other ways (requestAnimationFrame will not work well in this context).

Hope this helps.

Share:
27,298

Related videos on Youtube

MirrorMirror
Author by

MirrorMirror

Updated on July 09, 2022

Comments

  • MirrorMirror
    MirrorMirror almost 2 years

    I have a button which runs a long running function when it's clicked. Now, while the function is running, I want to change the button text, but I'm having problems in some browsers like Firefox, IE.

    html:

    <button id="mybutt" class="buttonEnabled" onclick="longrunningfunction();"><span id="myspan">do some work</span></button>
    

    javascript:

    function longrunningfunction() {
        document.getElementById("myspan").innerHTML = "doing some work";
        document.getElementById("mybutt").disabled = true;
        document.getElementById("mybutt").className = "buttonDisabled";
    
        //long running task here
    
        document.getElementById("myspan").innerHTML = "done";
    }
    

    Now this has problems in firefox and IE, ( in chrome it works ok )

    So I thought to put it into a settimeout:

    function longrunningfunction() {
        document.getElementById("myspan").innerHTML = "doing some work";
        document.getElementById("mybutt").disabled = true;
        document.getElementById("mybutt").className = "buttonDisabled";
    
        setTimeout(function() {
            //long running task here
            document.getElementById("myspan").innerHTML = "done";
        }, 0);
    }
    

    but this doesn't work either for firefox! the button gets disabled, changes colour ( due to the application of the new css ) but the text does not change.

    I have to set the time to 50ms instead of just 0ms, in order to make it work ( change the button text ). Now I find this stupid at least. I can understand if it would work with just a 0ms delay, but what would happen in a slower computer? maybe firefox would need 100ms there in the settimeout? it sounds rather stupid. I tried many times, 1ms, 10ms, 20ms...no it won't refresh it. only with 50ms.

    So I followed the advice in this topic:

    Forcing a DOM refresh in Internet explorer after javascript dom manipulation

    so I tried:

    function longrunningfunction() {
        document.getElementById("myspan").innerHTML = "doing some work";
        var a = document.getElementById("mybutt").offsetTop; //force refresh
    
        //long running task here
    
        document.getElementById("myspan").innerHTML = "done";
    }
    

    but it doesn't work ( FIREFOX 21). Then i tried:

    function longrunningfunction() {
        document.getElementById("myspan").innerHTML = "doing some work";
        document.getElementById("mybutt").disabled = true;
        document.getElementById("mybutt").className = "buttonDisabled";
        var a = document.getElementById("mybutt").offsetTop; //force refresh
        var b = document.getElementById("myspan").offsetTop; //force refresh
        var c = document.getElementById("mybutt").clientHeight; //force refresh
        var d = document.getElementById("myspan").clientHeight; //force refresh
    
        setTimeout(function() {
            //long running task here
            document.getElementById("myspan").innerHTML = "done";
        }, 0);
    }
    

    I even tried clientHeight instead of offsetTop but nothing. the DOM does not get refreshed.

    Can someone offer a reliable solution preferrably non-hacky ?

    thanks in advance!

    as suggested here i also tried

    $('#parentOfElementToBeRedrawn').hide().show();
    

    to no avail

    Force DOM redraw/refresh on Chrome/Mac

    TL;DR:

    looking for a RELIABLE cross-browser method to have a forced DOM refresh WITHOUT the use of setTimeout (preferred solution due to different time intervals needed depending on the type of long running code, browser, computer speed and setTimeout requires anywhere from 50 to 100ms depending on situation)

    jsfiddle: http://jsfiddle.net/WsmUh/5/

    • Mike 'Pomax' Kamermans
      Mike 'Pomax' Kamermans almost 11 years
      can you turn this into a jsfiddle please?
    • MirrorMirror
      MirrorMirror almost 11 years
      @Mike'Pomax'Kamermans yes i just updated my post with a jsfiddle.
    • Josh
      Josh almost 11 years
      With regard to your edit, does this mean jQuery can also be part of the solution?
    • MirrorMirror
      MirrorMirror almost 11 years
      Josh, yes I don't mind if jquery is part of the solution
    • Šime Vidas
      Šime Vidas almost 11 years
      Why aren't you caching your references? You're performing the same DOM queries multiple times.
    • Šime Vidas
      Šime Vidas almost 11 years
      You don't want to have heavy processing in your main browser thread as it freezes the page. Use Web Workers to perform the task.
    • Ezequiel Gorbatik
      Ezequiel Gorbatik almost 11 years
  • Jay Patel
    Jay Patel almost 11 years
    I thought the problem was use of innerHTML that's why I recommended use of value
  • MirrorMirror
    MirrorMirror almost 11 years
    yes the complete html button code is : <button id="mybutt" class="buttonEnabled" onclick="longrunningfunction();"><span id="myspan">run long running function</span></button>. also updated in the initial post
  • Jay Patel
    Jay Patel almost 11 years
    Why is there a span inside the button?
  • Jay Patel
    Jay Patel almost 11 years
    It seems so. Try removing it as it serves no purpose.
  • Jay Patel
    Jay Patel almost 11 years
    As you're updating the value of myspan instead of mybutton, DOM might not see the update. You need to update value of mybutton and get rid of the span.
  • MirrorMirror
    MirrorMirror almost 11 years
    I tried what you suggested. It didn't work. the problem does persist
  • Jay Patel
    Jay Patel almost 11 years
    as I don't have access to full code and specifically in the environment your setup has, I can't give you more suggestions.
  • MirrorMirror
    MirrorMirror almost 11 years
    unfortunately no, doesn't work. your updated jsfiddle: jsfiddle.net/BZWbH/3
  • MirrorMirror
    MirrorMirror almost 11 years
    there is no option to do the work in the server, because it cannot technically be done in the server.
  • MirrorMirror
    MirrorMirror almost 11 years
    i'll probably split the long running function in small settimeouts
  • Mike 'Pomax' Kamermans
    Mike 'Pomax' Kamermans almost 11 years
    That might be wise; if it cannot be done at the server, then cutting it up into a callback chain with timeouts will at least unblock the browser.
  • German Latorre
    German Latorre almost 11 years
    Devilishly clever solution, +1, :-) Is there any documentation that supports it? I mean, the setTimeout(..., 0); trick should work (and it's widely documented), but did not work for @MirrorMirror in FireFox
  • David Mulder
    David Mulder almost 11 years
    Btw, did you look into webworkers, because this does sound like the kind of thing that should either not be done at all in javascript or in a webworker which is meant to solve this limitation of JS.
  • Matt Coughlin
    Matt Coughlin almost 11 years
    This breaks standard browser behavior. Click events don't fire until after a mousedown and mouseup fire for the same element. If either occurs while the mouse is outside of the element, the click event doesn't fire. Problematic use cases: 1) Press the mouse button while within the element, move the mouse outside, then release the button. The animation runs indefinitely, but the processing never starts. 2) Press the mouse button while outside the element, move the mouse inside, then release the button. The processing runs with no visible indication that it's started.
  • Kylie
    Kylie almost 11 years
    Well........unfortunately for the OP, he wants something without setTimeout().......this is the ONLY solution, I tried 34 different iterations, and this is the one that passed the test. Unless he's willing to use setTimeout(), or betteryet, use a WebWorker..which is definitely how I would do it.......this is the one for him. If moving the mouse outside of the button is an issue, I suggest he makes the button extra large. :)
  • MirrorMirror
    MirrorMirror almost 11 years
    @KyleK thanks for your answer, I will try to see if I can avoid the problematic cases as pointed out by Matt. I don't mind a setTimeout solution per se, I'm fine with a solution having setTimeout(..., 0) or setTimeout(...,1), what i don't want is setTimeout(..., 50/100) where i would have to put large value to take care for all browsers
  • traditional
    traditional over 9 years
    Click the mouse on the button, drag it out side the button. Now release it. Woop, there is a problem. :(
  • Sebas
    Sebas almost 8 years
    but internally jquery uses setTimeout all over -.- you're just using a framework instead, I don't see what's smart in this
  • twobob
    twobob about 7 years
    spot on, this is easily refactored too to most long running tasks.
  • Jon Fagence
    Jon Fagence over 4 years
    I'm presuming this works because the first frame (where we want to show the messaging) occurs when the outer rAF runs, then the resource hog runs at the second frame coming from the inner rAF. Neat :).
  • Estradiaz
    Estradiaz over 4 years
    @JonFagence your comment made me add an example ^^
  • mathias999us
    mathias999us over 3 years
    This is the BEST answer on this page for my situation. I have a heavy javascript load that blocks the UI thread for several seconds. It cannot be split up, and I would like to place a loading mask / spinner over the page while this occurs. Your solution is the only way I can accomplish this in a deterministic fashion without introducing artificial delays with guessed timeout values. Thanks.