Running long async scripts prevents paint of the page. So you need to convert your tasks into "batches" and let the browser process other tasks after running each batch.
Look at this example from https://javascript.info/event-loop :
let i = 0;
function count() {
    // do a piece of the heavy job (*)
    do {
      i++;
      progress.innerHTML = i;
    } while (i % 1e3 != 0);
    if (i < 1e7) {
      setTimeout(count);
    }
}
count();
Now the heavy task is done in batches of 1000 iterations instead of one batch of 1e7 iterations.
In the code we are saying that:
- Increase iif it's not a multiplication of 1000. But the codeprogress.innerHTML = idoesn't update the interface before the loop ends.
- As soon as iis a multiplication of 1000, the loop breaks, anotherbatchis "postponed" usingsetTimeoutand the UI is updated.
You can also move your code into a web worker which runs on a background thread and doesn't interfere with the user interface.
UPDATE: The setTimeout method shown above works but it's not a perfect solution because:
The main script of JS is run on a single thread and async actions aren't run on another thread, they're just postponed to be run "later". So they "always" block the main script when they're running, even if we do the mentioned hack (the paint is blocked when we're in the do-while loop).
If we want to "really" separate that heavy task from our main script, we should use Web Workers which are really run on another OS-level thread.
So your best choice is to use a Web Worker and communicate the results using postMessage system.
Note: Web Workers do not have access to the DOM