Reputation: 7687
I have a very long loop in JS (2M + iterations).
I would like to show some progress to the user.
For some reason the code hangs, and only shows progress when the loop is done.
What am I doing wrong?
CAREFUL BROWSER MIGHT GET STUCK, SET MAX ACCORDINGLY
var MAX = 100000;
function doIt(){
alert('start count :'+ MAX);
for(i=0;i<MAX;i++){
setTimeout(updateBar, 600 , i);
console.log(i);
}
alert('count ended');
}
function updateBar(idx) {
var bar = document.getElementById('progress');
bar.style.width = Math.floor(100 * idx / MAX) + '%';
}
body {padding: 50px;}
<link href="http://getbootstrap.com/2.3.2/assets/css/bootstrap.css" rel="stylesheet"/>
<button onclick="doIt()">Start counting</button>
<div class="progress">
<div class="bar" id="progress"></div>
</div>
Upvotes: 3
Views: 830
Reputation: 20399
This is a classic issue that occurs as a result of a long-running function that does not halt periodically to pass control back to the browser's Event Loop, where the browser can do other tasks such as painting elements:
A downside of this model is that if a message takes too long to complete, the web application is unable to process user interactions like click or scroll. The browser mitigates this with the "a script is taking too long to run" dialog. A good practice to follow is to make message processing short and if possible cut down one message into several messages.
When the browser doesn't have any "breathing room" because your JS loop has control, the browser is not able to paint the progress bar.
To fix this, you need to run your loop in chunks, i.e. "cut down one message into several messages", as MDN recommends. To accomplish this you will need to use something asynchronous like a timeout to tell the browser to run the next iteration not "immediately", but "as soon as you're ready". This lets the browser do other important stuff before your function continues (like handle scrolls and clicks, and paint elements such as the progress bar).
The following solution breaks up the loop by returning control to the browser with setTimeout(fn, 0)
every x
iterations.
This is useful if the task you're doing involves the DOM, or for any other reason cannot be run in a web worker. Otherwise @blex's web worker solution is probably better, since any long-running tasks that can be run in background threads should be, for performance reasons.
const MAX = 500000;
let currentIteration = 0;
function doWork(){
while (currentIteration < MAX) {
// Do your actual work here, before the increment
currentIteration++;
if (currentIteration % 5000 === 0) {
console.log(currentIteration);
updateBar(currentIteration);
setTimeout(doWork, 0);
return;
}
}
alert('count ended');
}
function startWork() {
alert('start count :'+ MAX);
doWork();
}
function updateBar(idx) {
var bar = document.getElementById('progress');
bar.style.width = Math.floor(100 * idx / MAX) + '%';
}
body {padding: 50px;}
<link href="http://getbootstrap.com/2.3.2/assets/css/bootstrap.css" rel="stylesheet"/>
<button onclick="startWork()">Start counting</button>
<div class="progress">
<div class="bar" id="progress"></div>
</div>
Upvotes: 2
Reputation: 25648
Here is a possible solution, using a Web Worker:
worker-script.js (distinct context, distinct file)
// When this worker receives a message
self.addEventListener("message", onMessageReceive);
function onMessageReceive(e) {
// Get the max value
var max = e.data.value;
// Run the calculation
doIt(max);
}
function doIt(max) {
// Send messages to the parent from time to time
self.postMessage({ type: "start", value: max });
for (i = 0; i < max; i++) {
if (i % 5000 === 0) { // Every 5000 iterations - tweak this
self.postMessage({ type: "progress", value: i });
}
}
self.postMessage({ type: "end", value: max });
}
main.js (in your page)
var MAX = 20000000, // 20 millions, no problem
worker = null;
// If workers are supported, create one by passing it the script url
if (window.Worker) {
worker = new Worker("worker-script.js");
document
.getElementById("start-btn")
.addEventListener("click", startWorker);
} else {
alert("Workers are not supported in your browser.");
}
// To start the worker, send it the max value
function startWorker() {
worker.postMessage({ value: MAX });
}
// When the worker sends a message back
worker.addEventListener("message", onMessageReceive);
function onMessageReceive(e) {
var data = e.data;
switch (data.type) {
case "start":
console.log(`start count: ${data.value}`);
break;
case "progress":
updateBar(data.value);
break;
case "end":
updateBar(data.value);
console.log(`end count: ${data.value}`);
break;
}
}
function updateBar(idx) {
var bar = document.getElementById("progress");
bar.style.width = Math.floor((100 * idx) / MAX) + "%";
}
index.html
<button id="start-btn">Start counting</button>
<div class="progress">
<div class="bar" id="progress"></div>
</div>
<script src="main.js"></script>
Upvotes: 2