No surprises here, spamming your event loop with message events is not good.
Every time you postMessage
when the main thread is busy it has to keep the message event in a queue, and handling one message will keep the event loop busy, so posting 1 billion message will make the queue grow incredibly and eat all resources.
You have to redesign your code:
What you need is to have the Worker thread sends all the information it could generate thus far at some interval (e.g based on requestAnimationFrame frequency).
This way, your main thread only has to handle one message per rendering frame, and it can render all the data in one call.
The tricky part is to let the Worker thread know when it has to send its data.
For this, the best is to make it work by batches, and after each batch, wait just the next event loop iteration so our interval timer can slip through when required.
Here is a proof of concept rendering an infinite operation, which uses the MessageChannel API as a 0 timeout task scheduler engine used to let a requestAnimationFrame
callback fire when needed and performing 10000 operations per task (a lot more per frame if your computer is fast enough). This setup has the advantage of not slowing too much the overall processing that the Worker has to do.
const log = document.getElementById( "log" );
const script = document.querySelector( '[type="worker-script"]' );
const blob = new Blob( [ script.textContent ], { type: "text/javascript" } );
const worker = new Worker( URL.createObjectURL( blob ) );
worker.onmessage = ({ data }) => {
const [ PI, iterations ] = data;
log.textContent = `π = ${ PI }
after ${ iterations } iterations.`
};
<script type="worker-script">
// First some helpers
//
// Uses a MessageChannel to perform a 0 timeout scheduler
const postTask = (() => {
const channel = new MessageChannel();
channel.port1.start();
return (cb) => {
channel.port1.addEventListener( 'message', () => cb(), { once: true } );
channel.port2.postMessage( "" );
};
})();
// Promise wrapper around 'postTask'
const waitForNextEventLoopIteration = () =>
new Promise( res => postTask( res ) );
// WorkerScope.rAF is available in Chrome, not in FF
if( !self.requestAnimationFrame ) {
self.requestAnimationFrame = (cb) => setTimeout( cb, 16 );
}
// The actual processing
// borrowed from https://stackoverflow.com/a/50282537/3702797
// [addition]: made async so it can wait easily for next event loop
async function calculatePI( iterations = 10000 ) {
let pi = 0;
let iterator = sequence();
let i = 0;
// [addition]: start a new rAF loop
// which will report to main the current values
requestAnimationFrame( function reportToMain() {
postMessage( [ pi, i ] );
requestAnimationFrame( reportToMain );
} );
// [addition]: define a batch_size
const batch_size = 10000;
for( ; i < iterations; i++ ){
pi += 4 / iterator.next().value;
pi -= 4 / iterator.next().value;
// [addition]: In case we made one batch,
// we'll wait the next event loop iteration
// to let the rAF callback fire.
if( i % batch_size === 0 ) {
await waitForNextEventLoopIteration();
}
}
function* sequence() {
let i = 1;
while( true ){
yield i;
i += 2;
}
}
}
// Start the *big* job...
calculatePI( Infinity );
</script>
<pre id="log"></pre>