<script id="workerScript" type="text/javascript">
// This script is written in such a way that it can be used
// inline or converted to a url blob and used as a web worker.
const msPerFrame = 1000 / 60;
const squareSize = 50;
const margin = 1;
let canvas;
let ctx;
let shouldSlow = false;
function animate() {
const totalSize = squareSize + (margin) * 2;
const frameNo = Math.floor(performance.now() / msPerFrame);
const moveBy = frameNo % totalSize;
const across = Math.ceil(canvas.width / squareSize);
const down = Math.ceil(canvas.height / squareSize);
ctx.clearRect(0, 0, canvas.width, canvas.height);
for(let a = -3; a <= across; a++) {
for(let d = -3; d <= down; d++) {
const x = a * totalSize + moveBy;
const y = d * totalSize;
ctx.fillStyle = d % 2 === 0 ? '#fa8520' : '#2fbee9';
ctx.fillRect(x, y, squareSize, squareSize);
}
}
// Do a big loop to slow down the thread
if(shouldSlow) {
for(let i=0; i < 500000000; i++) {
// slow ...
}
}
self.canvasRafId = self.requestAnimationFrame(animate);
}
function slow(slow) {
shouldSlow = slow;
}
self.onmessage = function(ev) {
if(ev.data.msg === 'init') {
canvas = ev.data.canvas || window.canvas;
ctx = canvas.getContext('2d');
animate();
} else if(ev.data.msg === 'slow') {
slow(ev.data.shouldSlow);
}
}
</script>
<div id="content">
<div id="unsupported-warning" style="display: none">Your browser does not support Offscreen Canvas. Please try this demo in one that does, such as Chrome 69+.</div>
<div id="explanation">
<h1>Main Thread Canvas vs Web Worker (Offscreen) Canvas</h1>
<p>This simple example shows how running a canvas on a web worker (that is, an <em>offscreen canvas</em>) can prevent jank and other performance issues.</p>
<p>First, run the simple animation by pressing the <strong>Start Animation</strong> button. This runs on the main thread. If the animation takes a long time, then the whole page will become unresponsive. Note the timestamp, next to the buttons, which updates with every frame.</p>
<p>Now, press the <strong>Slow Animation</strong> button. This includes a very large loop in each animation frame, causing it to take a long time. The animation will become jumpy, compared to the smooth motion before. Notice that the timestamp is also slow to update, compared to previously. This is because the whole page cannot update until the animation has completed, as it is blocking the single thread. Try typing in the box below, and you will see that it feels unresponsive.</p>
<p><input type="text" placeholder="Try typing here when slowed..." /></p>
<p>Press the <strong>Transfer Canvas to Worker</strong> button. This uses the new <em>canvas.transferControlToOffscreen</em> function to take a canvas which is in the DOM and give control of it to a web worker. Notice that the animation is smooth, as it resets to normal speed. Now, press the <strong>Slow Animation</strong> button. The animation slows down, but the timer continues at a normal speed. Additionally, you can type like normal in the text box.</p>
</div>
<div id="controls">
<button id="toggle-animation" onclick="toggleAnimation();"><span class="action">Start</span> Animation</button>
<button id="toggle-slow" onclick="toggleSlow();"><span class="action">Slow</span> Animation</button>
<button id="toggle-thread" onclick="toggleThread();" class="if-">Transfer Canvas to <span class="target">Worker</span></button>
<span id="time"></span>
</div>
</div>
<div id="canvas-container"></div>
<div id="canvas-info">
<span class="slow"></span>
<span class="thread"></span>
</div>
* {
box-sizing: border-box;
}
html, body {
height: 100%;
overflow: hidden;
}
body {
margin: 0;
padding: 0;
display: flex;
background-color: #fafafa;
font-family: Roboto, -apple-system, BlinkMacSystemFont, "Helvetica Neue", "Segoe UI", "Oxygen", "Ubuntu", "Cantarell", "Open Sans", sans-serif
}
h1 {
margin: 0;
font-size: 150%;
}
#content {
display: flex;
flex-direction: column;
flex: 1;
}
#explanation {
flex: 1;
padding: 10px;
overflow-y: auto;
}
#unsupported-warning {
background-color: red;
text-align: center;
color: #fff;
padding: 10px;
}
#controls {
padding: 10px;
text-align: center;
border-top: 1px solid #ccc;
}
#canvas-container {
width: 35%;
height: 100%;
background-color: #fff;
}
#canvas {
width: 100%;
height: 100%;
}
#canvas-info {
position: fixed;
right: 0;
bottom: 0;
background-color: #555;
color: #fff;
padding: 5px 10px;
font-size: 80%;
}
button, input {
padding: 10px;
}
button {
display: inline-block;
}
input {
width: 100%;
}
@media (max-width: 700px) {
body {
flex-direction: column;
}
#content {
max-height: 75%;
}
#canvas-container {
width: 100%;
height: 35%;
}
}
let currentThread = 'main';
let isAnimating = false; // start with animation stopped
let isSlow = false;
let worker;
// Animate the canvas using the worker, and stop the main thread if it is running there
function animateOnWorker() {
init();
// Prepare this canvas to be transferred to the worker
const offscreenCanvas = canvas.transferControlToOffscreen();
// Get the worker script from this page, and create an object url from it
// so it can be loaded without being a separate file.
const workerScript = document.getElementById('workerScript').textContent;
const blob = new Blob([workerScript], {type: 'text/javascript'});
const url = URL.createObjectURL(blob);
worker = new Worker(url);
// Pass our init message to the worker, along with the transferred canvas
worker.postMessage({msg: 'init', canvas: offscreenCanvas}, [offscreenCanvas]);
// We're done with the object url, get rid of it to save memory
URL.revokeObjectURL(url);
}
// Animate the canvas using the main thread
function animateOnMain() {
init();
window.postMessage({msg: 'init'}, '*');
}
/*****
*
* Below here are the things that make the demo run better, but
* the important stuff is above here!
*
*****/
function init() {
stopAnimation();
// We need to make a new canvas every time we switch between the main thread
// and the worker. This is because a canvas cannot be transferred once it has
// a rendering context.
document.getElementById('canvas-container').innerHTML = '<canvas id="canvas"></canvas>';
canvas = document.getElementById('canvas');
canvas.width = canvas.clientWidth;
canvas.height = canvas.clientHeight;
// when running on the main thread, we can't pass the canvas in a postMessage,
// so we just set it globally
window.canvas = canvas;
tickTimer();
}
function setThread(thread) {
currentThread = thread;
if(currentThread === 'main') {
animateOnMain();
} else if(currentThread === 'worker') {
animateOnWorker();
}
// Getting slow to turn on when we switch requires listening
// for the worker to be ready, which is a bit complicated for now.
// So, we just turn off slow.
toggleSlow(false);
document.querySelector('#toggle-thread .target').innerText = currentThread === 'main' ? 'Worker' : 'Main';
document.querySelector('#canvas-info .thread').innerText = currentThread;
}
function toggleThread() {
if(typeof currentThread !== 'undefined' && currentThread === 'main') {
setThread('worker');
} else {
setThread('main');
}
}
function toggleAnimation() {
isAnimating = !isAnimating;
if(isAnimating) {
setThread(currentThread);
} else {
stopAnimation();
}
document.querySelector('#toggle-animation .action').innerText = isAnimating ? 'Stop' : 'Start';
}
function stopAnimation() {
if(worker) {
worker.terminate();
worker = null;
}
[window.canvasRafId, window.timerRafId].map(rafId => {
if(rafId) {
window.cancelAnimationFrame(rafId);
}
});
}
function toggleSlow(shouldSlow) {
isSlow = typeof shouldSlow === 'boolean' ? shouldSlow : !isSlow;
if(worker) {
worker.postMessage({msg: 'slow', shouldSlow: isSlow});
} else {
slow(isSlow);
}
document.querySelector('#toggle-slow .action').innerText = isSlow ? 'Unslow' : 'Slow';
document.querySelector('#canvas-info .slow').innerText = isSlow ? '(slow)' : '';
}
const timeEl = document.getElementById('time');
function tickTimer() {
timeEl.innerText = Date.now();
window.timerRafId = window.requestAnimationFrame(tickTimer);
}
if(typeof window.OffscreenCanvas !== 'function') {
document.getElementById('unsupported-warning').style.display = '';
document.getElementById('toggle-thread').setAttribute('disabled', 'disabled');
}
This Pen doesn't use any external CSS resources.
This Pen doesn't use any external JavaScript resources.