React updating progress from a compute intensive function using setState

Viewed 1449

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?

2 Answers

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/

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/

Related