That is to be expected, "committing" to the main thread is not done synchronously, but when the browser dims it appropriate (i.e often at next painting frame) so when you call worker.terminate()
, the actual painting may not have occurred yet, and won't ever.
Here is an live repro for curious:
const worker_script = `
self.addEventListener('message', (evt) => {
const canvas = evt.data;
const ctx = canvas.getContext( "2d" );
// draw a simple grid of balck squares
for( let y = 0; y<canvas.height; y+= 60 ) {
for( let x = 0; x<canvas.width; x+= 60 ) {
ctx.fillRect(x+10,y+10,40,40);
}
}
self.postMessage( "" );
});`;
const worker_blob = new Blob( [ worker_script ], { type: "text/javascript" } );
const worker_url = URL.createObjectURL( worker_blob );
const worker = new Worker( worker_url );
worker.onmessage = (evt) => worker.terminate();
const canvas_el = document.querySelector( "canvas" );
const off_canvas = canvas_el.transferControlToOffscreen();
worker.postMessage( off_canvas, [ off_canvas ] );
<h3>Run this snippet a few times (in Chrome), sometimes it will work, sometimes it won't.</h3>
<canvas width="500" height="500"></canvas>
To circumvent this, there is an OffscreenCanvasRendering2DContext.commit() method that you could call before you terminate your worker, but it's currently hidden under chrome://flags/#enable-experimental-web-platform-features
.
if( !( 'commit' in OffscreenCanvasRenderingContext2D.prototype ) ) {
throw new Error( "Your browser doesn't support the .commit() method," +
"please enable it from chrome://flags" );
}
const worker_script = `
self.addEventListener('message', (evt) => {
const canvas = evt.data;
const ctx = canvas.getContext( "2d" );
// draw a simple grid of balck squares
for( let y = 0; y<canvas.height; y+= 60 ) {
for( let x = 0; x<canvas.width; x+= 60 ) {
ctx.fillRect(x+10,y+10,40,40);
}
}
// force drawing to element
ctx.commit();
self.postMessage( "" );
});`;
const worker_blob = new Blob( [ worker_script ], { type: "text/javascript" } );
const worker_url = URL.createObjectURL( worker_blob );
const worker = new Worker( worker_url );
worker.onmessage = (evt) => worker.terminate();
const canvas_el = document.querySelector( "canvas" );
const off_canvas = canvas_el.transferControlToOffscreen();
worker.postMessage( off_canvas, [ off_canvas ] );
<h3>Run this snippet a few times (in Chrome), it will always work ;-)</h3>
<canvas width="500" height="500"></canvas>
So a workaround, without this method is to wait before you terminate your Worker. Though there doesn't seem to be a precise amount of time nor a special event we can wait for, by tests-and-error I came up to wait three painting frames, but that may not do it on all devices, so you may want to be safest and wait a few plain seconds, or even just to let the GarbageCollector take care of it:
const worker_script = `
self.addEventListener('message', (evt) => {
const canvas = evt.data;
const ctx = canvas.getContext( "2d" );
// draw a simple grid of balck squares
for( let y = 0; y<canvas.height; y+= 60 ) {
for( let x = 0; x<canvas.width; x+= 60 ) {
ctx.fillRect(x+10,y+10,40,40);
}
}
self.postMessage( "" );
});`;
const worker_blob = new Blob( [ worker_script ], { type: "text/javascript" } );
const worker_url = URL.createObjectURL( worker_blob );
const worker = new Worker( worker_url );
worker.onmessage = (evt) =>
// trying a minimal timeout
// to be safe better do setTimeout( () => worker.terminate(), 2000 );
// or even just let GC collect it when needed
requestAnimationFrame( () => // before next frame
requestAnimationFrame( () => // end of next frame
requestAnimationFrame( () => // end of second frame
worker.terminate()
)
)
);
const canvas_el = document.querySelector( "canvas" );
const off_canvas = canvas_el.transferControlToOffscreen();
worker.postMessage( off_canvas, [ off_canvas ] );
<h3>Run this snippet a few times (in Chrome), it should always work.</h3>
<canvas width="500" height="500"></canvas>
Now, I should note that creating a new Worker for a single job is generally a very bad design. Starting a new js context is a really heavy operation, and making the Worker thread's link with the main thread's GPU instructions is an other one, I do'nt know much about what you're doing, but you should really consider if you won't need to reuse both the Worker and the OffscreenCanvas, in which case you should rather keep them alive.