JavaScript programs run on a single thread in the browser and in runtimes such as Node.js. When code is executing in a browser tab, everything else stops: menu commands, downloads, rendering, DOM updates and even GIF animations.
This is rarely evident to the user because processing occurs quickly in small chunks. For example: a button is clicked which raises an event that runs a function which makes a calculation and updates the DOM. Once complete, the browser is free to handle the next item on the processing queue.
JavaScript code can’t wait for something to occur; imagine the frustration if an app froze every time it made an Ajax request. JavaScript code therefore operates using events and callbacks: a browser or OS-level process is instructed to call a specific function when an operation has completed and the result is ready.
In the following example, a handler function is executed when a button click event occurs which animates an element by applying a CSS class. When that animation completes, an anonymous callback removes the class:
// raise an event when a button is clicked
document.getElementById('clickme').addEventListener('click', handleClick);
// handle button click event
function handleClick(e) {
// get element to animate
let sprite = document.getElementById('sprite');
if (!sprite) return;
// remove 'animate' class when animation ends
sprite.addEventListener('animationend', () => {
sprite.classList.remove('animate');
});
// add 'animate' class
sprite.classList.add('animate');
}
ES2015 provided Promises
and ES2017 introduced async
/await
to make coding easier, but callbacks are still used below the surface. For more information, refer to “Flow Control in Modern JS”.
Blocking Bandits
Unfortunately, some JavaScript operations will always be synchronous, including:
- running calculations
- updating the DOM
- using localStorage or IndexedDB to store and retrieve data.
The following pen shows an invader which uses a combination of CSS animation to move and JavaScript to wave the limbs. The image on the right is a basic animated GIF. Hit the write button with the default 100,000 sessionStorage operations
:
See the Pen
DOM-blocking animation by SitePoint (@SitePoint)
on CodePen.
DOM updates are blocked during this operation. The invader will halt or stutter in most browsers. The animated GIF animation will pause in some. Slower devices may show a “script unresponsive” warning.
This is a convoluted example, but it demonstrates how front-end performance can be affected by basic operations.
Web Workers
One solution to long-running processes is web workers. These allow the main browser application to launch a background script and communicate using message events. For example:
// main.js
// are web workers supported?
if (!window.Worker) return;
// start web worker script
let myWorker = new Worker('myworker.js');
// message received from myWorker
myWorker.onmessage = e => {
console.log('myworker sent:', e.data);
}
// send message to myWorker
myWorker.postMessage('hello');
The web worker script:
// myworker.js
// start when a message is received
onmessage = e => {
console.log('myworker received:', e.data);
// ... long-running process ...
// post message back
postMessage('result');
};
A worker can even spawn other workers to emulate complex, thread-like operations. However, workers are intentionally limited and a worker cannot directly access the DOM or localStorage
(doing so would effectively make JavaScript multi-threaded and break browser stability.) All messages are therefore sent as strings, which permits JSON-encoded objects to be passed but not DOM nodes.
Workers can access some window
properties, web sockets, and IndexDB — but they wouldn’t improve the example shown above. In most cases, workers are used for long-running calculations — such as ray tracing, image processing, bitcoin mining and so on.
(Node.js offers child processes which are similar to web workers but have options to run executables written in other languages.)
Hardware-accelerated Animations
Most modern browsers don’t block hardware-accelerated CSS animations which run within their own layer.
By default, the example above moves the invader by changing the left-margin
. This and similar properties such as left
and width
cause the browser to reflow and repaint the whole document at every animation step.
Animation is more efficient when using the transform
and/or opacity
properties. These effectively place the element into a separate compositing layer so it can be animated in isolation by the GPU.
Click the hardware acceleration checkbox and the animation will immediately become smoother. Now attempt another sessionStorage
write; the invader will continue to move even if the animated GIF stops. Note that the limb movement will still pause because that’s controlled by JavaScript.
In-memory Storage
Updating an object in memory is considerably faster than using a storage mechanism which writes to disk. Select the object storage type in the pen above and hit write. Results will vary, but it should be around 10x faster than an equivalent sessionStorage
operation.
Memory is volatile: closing the tab or navigating away causes all data to be lost. A good compromise is to use in-memory objects for improved performance, then store data permanently at convenient moments — such as when the page is unloaded:
// get previously-saved data
var store = JSON.parse(localStorage.getItem('store'));
// initialise an empty store
if (!store || !store.initialized) {
store = {
initialized: true,
username: 'anonymous'
score: 0,
best: { score: 1000, username: 'Alice' }
}
};
// save to localStorage on page unload
window.addEventListener('unload', () => {
localStorage.setItem('store', JSON.stringify(store));
});
Games or single-page applications may require more complex options. For example, data is saved when:
- there’s no user activity (mouse, touch or keyboard events) for a number of seconds
- a game is paused or the application tab is in the background (see the Page Visibility API)
- there’s a natural pause — such as when the player dies, completes a level, moves between primary screens, and so on.
Web Performance
Web performance is a hot topic. Developers are less constrained by browser limits and users expect fast, OS-like application performance.
Do as little processing as infrequently as possible and the DOM will never be noticeably blocked. Fortunately, there are options in situations where long-running tasks can’t be avoided.
Users and clients may never notice your speed optimizations, but they’ll always complain when the application becomes slower!