6

I have a simple React class showing this.state.progress (a number) and this state can be updated via updateProgress(progress) function.

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      progress: 0,
    }
  }

  updateProgress = (progress) => {this.setState({progress}); };
  render() {
    let {progress} = this.state;
    return <h1>{progress}</h1>;
  }
}

I have a compute intensive function myHeavyFunc, for which I need to show the progress bar. I call updateProgress function I mentioned above using the loop variable inside myHeavyFunc.

myHeavyFunc = async (updateProgress) => {
    let loopLength = 1000000;
    updateProgress(0);
    for(let i=0; i<loopLength; i++) {
      // some processing happens here
      updateProgress((i+1)/loopLength);
    }
}

What happens is that state gets updated, and I can confirm that by console logging progress in the setState callback, but the component doesn't re-render until the very end. However, if I include a small sleep of 1ms, then the re-render happens, progress updates (obviously with a huge loss in time, which I do not prefer).

JSFiddle here. Here I run myHeavyFunc on clicking the progress number. You can see that when await sleep(1) is commented, onClick finishes in a second, but does NOT show progress. It does not even change for any subsequent clicks. On the other hand, if it is not commented, I get the progress updates, but it just takes forever to complete!

I am aware that React shall batch the updates for performance reasons, but in my case, I can't even see one update happen till the whole loop finishes. Also, please note that I am NOT looking for a synchronous setState function, but I need re-rendering (atleast on the progress element alone) after the state is set. I am fine if it drops a few progress updates due to batching, but I do expect it to show progress.

Is there a way to run myHeavyFunc in a non-blocking manner while updating the progress in the UI? What is the right way to update progress of compute intensive functions in React?

  • Update [fiddle](https://jsfiddle.net/saravanabalagi/1pqt0sju/17/) – Saravanabalagi Ramachandran Oct 14 '19 at 14:50
  • just making a function `async` doesn't do anything magical. It makes it return a Promise, but if it does something that lasts a long time and is synchronous before returning, it will still run to completion before React has a chance to do anything. – Robin Zigmond Nov 09 '20 at 19:17
  • whereas when you add in the `await sleep(1)`, this "pauses execution" and allows the component to update – Robin Zigmond Nov 09 '20 at 19:18
  • 2
    so in short, your way with the `await sleep(1)` calls is the "right way" to do it - I appreciate that in this case it makes the process far too long, well in that case just only call that every 100 steps of the loop, or every 1000. I assume this is based on a more realistic example than a do-nothing long loop - in this case you might get better responses if you show your actual function, or at least a more realistic approximation to it. – Robin Zigmond Nov 09 '20 at 19:24
  • @RobinZigmond I have a very similar setup - a for loop where I simply build a very big array (~64k json entries), but since this question can be generalized to several other problems where we have a long running compute function, I left a do-nothing loop. – Saravanabalagi Ramachandran Nov 16 '20 at 13:04
  • Well if you're calling a synchronous function that takes a few seconds (or more) to complete, that's going to be a horrible user experience, for reasons that have nothing to do with React - and no matter what the function does. To avoid this, you basically have to "pause" the function at points you choose, and schedule the next part of the computation using `setTimeout` or similar. (In theory a web worker might be another approach, although not one I know much about.) And if you're doing that anyway then you can easily update some state each time, to update your progress bar. – Robin Zigmond Nov 16 '20 at 13:12
  • You're right, I see two ways we can do this, 1. Update progress every _n_ **steps**: [fiddle](https://jsfiddle.net/saravanabalagi/1pqt0sju/18/); 2. Update progress every _n_ **seconds**: [fiddle](https://jsfiddle.net/saravanabalagi/1pqt0sju/29/); – Saravanabalagi Ramachandran Nov 16 '20 at 15:17

2 Answers2

2

I recommend you notify the progress as few times as possible. The progress will go from 0 to 100, so I will notify the progress only 100 times, and also I will add a timeout, so the user can see the progress transition:

If loopLength = 1000000,

1000000 / 100 = 10000

Every 10000 iterations I will call updateProgress function.

let myHeavyFunc = async (updateProgress) => {
  let loopLength = 1000000;
  let current = 0;
  for(let i=0; i<loopLength; i++) {
    const result = Math.floor(((i+1)/loopLength)*100);
    if(current != result) {
       setTimeout(()=> {
           updateProgress(result);
       }, result *100);
    }
    current = result;
  }
}

See: https://jsfiddle.net/wbzx4j90/1/

lissettdm
  • 7,119
  • 1
  • 4
  • 23
  • This solution works and is quite similar to what's suggested in Robin Zigmond's comment. Why is the timeout linearly increasing with progress though? – Saravanabalagi Ramachandran Nov 16 '20 at 13:02
  • Yes, the idea is to call updateProgress(result) in different time, for result=1, time=100, result=2, time = 200, otherwise if you define the same time you will have result=1, time=100, result=2, time = 100, so the user won't see the progress transition. – lissettdm Nov 16 '20 at 13:10
  • You would only need the same amount of _timeout time_ all the time. The timeout allows the browser to render content. I'm using 1ms to reduce overhead, fiddle [here](https://jsfiddle.net/saravanabalagi/1pqt0sju/18/) – Saravanabalagi Ramachandran Nov 16 '20 at 14:56
  • I updated your [fiddle](https://jsfiddle.net/saravanabalagi/sd4bupaj/) to reflect the same number transition i.e. 0 to 1, leaving everything else untouched. But you can see it's taking way more time than it would without the timeouts. You can compare it against the fiddle I've posted above to perceive the time difference. – Saravanabalagi Ramachandran Nov 16 '20 at 14:59
  • Yes, that time was an example, you can use less. Instead of 100, you can try, 10 or 1 – lissettdm Nov 16 '20 at 15:27
  • Sure, I don't mind the 100 there, but why is it multiplied by result making the timeout increase with progress? – Saravanabalagi Ramachandran Nov 16 '20 at 17:35
  • If you let the same time setTimeout(()=> { updateProgress((i+1)/loopLength); }, 100); will go very fast, will notify all almost at the same time, of course this is not a problem, depends on the project requirements. – lissettdm Nov 16 '20 at 18:46
0

I guess the issue is in browser repainting. Usually, the browser repaints 60 frames per second. In your case, you ask the browser to perform more painting than it can. I suggest using requestAnimationFrame instead of for loop, to updating the DOM continuously. requestAnimationFrame will handle the function to be called before the browser performs the next repaint.

  let i = 0
  let loopLength = 1000;

  let myHeavyFunc = (updateProgress) => {
    updateProgress((i+1)/loopLength);
    i++;
  if (i < loopLength) {
    window.requestAnimationFrame(() => myHeavyFunc(updateProgress));
    
  }
}

Working demo https://jsfiddle.net/gdq5ouat/5/

Beso Kakulia
  • 556
  • 4
  • 12