I use web-workers that calculate position of an element when user clicked a button. Each click spawns a new worker. Once a worker finished with it's task its terminated.
I've noticed that sometimes response from a newly spawned worker received by main process with a delay (a whole second sometimes, when browser was idle for a while), so I've tried leave workers in memory and re-use them, only spawning new if all are busy. This seems to fix the issue with delay (on average the first response received 4-6 times faster this way too), but now I'm wondering, is it a good idea in a long run leaving workers in memory, especially since in this example each one consumes 1.5mb of RAM?
for(let i = 0; i < 100; i++)
table.appendChild(document.createElement("div"));
const workerURL = URL.createObjectURL(new Blob(["(" + (() =>
{
const timeNow = performance.now();
// worker code
const pref = {};
this.onmessage = e => {
Object.assign(pref, e.data);
if (pref.status)
this.postMessage(performance.now() - pref.time);
}; //set preferences
(function loop(timestamp = performance.now())
{
setTimeout(loop);
if (!pref.status || timestamp - pref.time < pref.speed)
return;
pref.time = timestamp;
pref.position++;
this.postMessage(pref); //send new position
})();
}).toString() + ")()"]));
const animations = (() =>
{
const workers = new Map();
const showWorkers = (i=0) =>
{
while(workersList.children.length > workers.size)
workersList.removeChild(workersList.lastChild);
let busy = 0
for(let [_worker, pref] of workers)
{
const node = workersList.children[i++] || document.createElement("span");
node.textContent = pref.id;
node.classList.toggle("active", pref.status);
node.style.setProperty("--bg", "#" + pref.bg);
node.style.setProperty("--color", pref.color);
if (pref.status)
busy++;
if (!node.parentNode)
workersList.appendChild(node);
}
stats.textContent = "Workers: " + workers.size + " Busy: " + busy;
};
const initPref = pref =>
{
const defaultPref = {
bg: (~~(Math.random() * 16777215)).toString(16).padStart(6, 0),
speed: ~~(Math.random() * 50 + 10),
};
pref = Object.assign({},
defaultPref, //set default parameters
pref, //restore parameters from existing worker
persist.checked ? {} : defaultPref, //reset to default
{position: -1, status: 1, time: 0});//reset some of the parameters
pref.color = ["black","white"][~~([0,2,4].map(p=>parseInt(pref.bg.substr(p,2),16)).reduce((r,v,i)=>[.299,.587,.114][i]*v+r,0)<128)]; //luminiacity
return pref;
}
showWorkers();
let id = 1;
return {
start: () =>
{
const timeStart = performance.now();
let worker, pref = {}, prevNode;
//find idle worker
for(let [_worker, _pref] of workers)
{
if (!_pref.status)
{
worker = _worker;
pref = _pref;
break;
}
}
pref = initPref(pref);
if (!worker)
{
worker = new Worker(workerURL);
pref.id = id++
}
worker.onmessage = e =>
{
if (typeof e.data == "number")
{
return response.textContent = ("Worker " + pref.id + " responded in " + (performance.now() - timeStart).toFixed(1) + "ms\n" + response.textContent).split("\n").slice(0, 100).join("\n");
}
Object.assign(pref, e.data); //update pref
if (prevNode)
{
prevNode.removeAttribute("style");
delete prevNode.dataset.id;
}
if (pref.position >= table.children.length) //finish worker
{
if (reuseWorkers.checked)
{
pref.status = 0;
worker.postMessage(pref); //stop worker
}
else
{
worker.terminate();
workers.delete(worker);
}
showWorkers(); //update stats
return;
}
const node = table.children[pref.position];
node.style.setProperty("--bg", "#" + pref.bg);
node.style.setProperty("--color", pref.color);
node.dataset.id = pref.id;
prevNode = node;
};
worker.postMessage(pref);
workers.set(worker, pref);
showWorkers();
},
}
})();
start.onclick = () => animations.start();
#table
{
display: inline-grid;
grid-template-columns: repeat(10, 10fr);
}
#table > *
{
outline: 1px solid black;
background-color: white;
width: 1em;
height: 1em;
}
#table > *::before
{
content: "";
}
#table > [style]::before
{
content: attr(data-id);
width: 125%;
height: 125%;
font-size: 0.7em;
line-height: 1.75em;
display: inline-flex;
justify-content: center;
position: relative;
background-color: var(--bg);
color: var(--color);
border: 1px solid;
top: -25%;
left: -25%;
border-radius: 1em;
}
.nowrap
{
white-space: nowrap;
display: flex;
align-items: flex-start;
}
#workersList
{
display: inline-flex;
flex-wrap: wrap;
align-content: flex-start;
margin-left: 0.5em;
}
#workersList > *
{
padding: 0.2em;
border: 1px solid black;
margin: -1px 0 0 -1px;
background-color: white;
min-width: 1em;
height: 1em;
text-align: center;
}
#workersList > .active
{
background-color: var(--bg);
color: var(--color);
border-radius: 100%;
}
#response
{
white-space: pre;
max-height: 10em;
overflow: auto;
vertical-align: top;
display: inline-block;
margin: 0 0.5em;
padding: 0 0.5em;
flex-shrink: 0;
}
#stats
{
margin-left: 1em;
}
<div>
<button id="start">Start</button>
<label><input type="checkbox" id="reuseWorkers">Re-use idle workers</label>
<label><input type="checkbox" id="persist">Re-use color/speed</label>
<span id="stats"></span>
</div>
<div class="nowrap">
<div id="table"></div>
<div id="response"></div>
<div id="workersList"></div>
</div>
P.S. I probably could add self termination if it was idle for a while.
Yes should definitely reuse your Workers.
Starting a new Worker means that a new process, a new event loop, and a new JS context have to be spawned. This is an heavy operation, as the specs say:
Workers (as these background scripts are called herein) are relatively heavy-weight, and are not intended to be used in large numbers. For example, it would be inappropriate to launch one worker for each pixel of a four megapixel image. [...]
Generally, workers are expected to be long-lived, have a high start-up performance cost, and a high per-instance memory cost.
There are active discussions (to which I took part), to improve the Worker
interface to prevent web-authors doing exactly what you did. Currently it's too tempting to have one-off Workers since it's relatively complicated to set up multi-tasks Workers. Hopefully this discussion will lead to something that makes Workers easy to reuse.
Actually, as a rule of thumb, you should even never start more Workers than navigator.hardwareConcurrency - 1
. Failing to do so, your Workers's concurrency would be done through task-switching instead of true parallelism, ant that would incur a significant performance penalty.
Regarding the memory consumption of your Workers, if you correctly get rid of the references to the objects you are using in each task, the Garbage Collector will do its job, normally even if the pressure comes from another thread. And as hinted by the specs quote, even an empty Worker comes with its own memory footprint, so by starting a lot of Workers you are actually adding more pressure.