1. Failing to Batch Reads and Writes
So we can do our reading first, and then writing afterwards.
And if we take a step back and look at the principle here, we really want to reduce our interactions with the DOM as much as we can.
For example, we can read from the DOM and store our measurement in a variable to reference later instead of measuring the same thing repeatedly.
We can also temporarily remove elements from the DOM with removeChild, perform actions on the removed elements, and then add them back with appendChild. I may go into more detail with another post about this. We can do something similar with cloneNode and replaceChild (although cloning the node does not copy event listeners added to it). This is especially helpful for avoiding loops that directly manipulate the DOM, which would be expensive.
So let’s think about both the order and the frequency in which we read and write to the DOM.
2. Failing to Utilize requestAnimationFrame()
The above is all fine, but realistically, we likely have other scripts in our HTML document, and the next one may need to read again after we just wrote to the DOM.
We can more wisely allocate the work the browser has to do by utilizing requestAnimationFrame().
When we read from the DOM and then want to later write to the DOM, we can write to the DOM in the callback of requestAnimationFrame. This is not just for animations.
Placing code that writes to the DOM in a callback of requestAnimationFrame increases chances that it will execute before the browser paints the next frame, and not at some random time to interrupt the painting. Devices these days typically have a refresh rate of 60Hz, with 16ms available for each frame in order for them to appear smooth. If the browser is too busy to meet this deadline, or if the timing is not right, dropped frames happen.
3. Allowing Event Listeners to Consume Too Much
We can easily add an event listener to execute a function whenever an event occurs.
Some events, like
resize, can execute hundreds or thousands of times in a short amount of time. Often, we don’t need to the browser to do so much work.
As a beginner, I failed to think about reducing the frequency at which my event listener callbacks respond to events.
We can throttle the callback or debounce the callback, or both.
We can throttle the callback to make it execute our desired code at a fixed interval of time, and no more frequently than that.
The code below ensures that
myFunction executes no more than once every 400ms when scrolling.
Another approach is to debounce the code, to make the code execute once after a specified amount of time after the event is complete.
We can even combine throttling and debouncing for more precision, but I plan to make that for another post.
Another thing we can do is make our event listener passive to improve scrolling performance by simply setting the flag.
4. Failing to Utilize Anonymous Functions and Reduce Scope
We can reduce the scope of our variables, and therefore the chance for bugs and unintended reassignment, by using
let instead of
let are available only in the section of brackets where they are defined. Defining variables with
var can clutter up the global namespace. The more variables in the global scope, the more chances there are for conflicts, such as variables with the same name.
By the same principle, anonymous functions also can be used to avoid the global scope. They have their pros and cons, but it’s helpful to understand how and why to use them.
Regular named function used as callback
Anonymous function used as callback
Modern browsers can also make use of arrow functions, which are just less verbose anonymous functions.
An IIFE (immediately invoked function expression) is a type of anonymous function which basically just executes immediately when encountered. It can be used so that all declared variables and functions within are not included in the global scope.
I understand one should be careful using
this in such a function.
These are all ways to implement an IIFE:
5. Ignoring Delivery Method
There are already great illustrations and posts out there on this, so I will just point to one here.
One advanced trick is to use the
One caveat to the
type = "module" is that there are certain limitations in the available scope of global variables and functions.
Still another approach is delaying scripts until user interaction.
It is critical to understand how each of these implementations affects the script. Sometimes you want your script to execute as soon as possible, or you have dependencies, etc. So these can either harm you or help you depending on how you use them.
Aside from affecting performance, these attributes can also cause scripts to flat out fail to execute due to errors if you make certain scripts that are needed for others to execute in the wrong order.
Here I just want to mention this as something to do on your list; details of this can be further researched. In general it is good to start out with your script tags in the <head>.
For example, one thing to watch out for is if you try to do something like modify the DOM before a certain element being targeted by your script has been parsed in the HTML, then your script might fail due to the script executing too soon, so you might want to move it to the footer, where you can be confident that the DOM has completed. Alternatively, you can make your script to execute on the
onload event or whathaveyou.
One of the minify tools use is this one. Many WordPress optimization or caching plugins offer this too.
Minification can only help you, as long as you still have the original somewhere for readability and future maintenance, and assuming the minification tool does not have a bug in it that breaks your code, which is unlikely but worth making sure your your webpage still has no console errors after minifying.
The order and frequency with which we read, write, and interact with the DOM in general needs to be given careful consideration. We can minimize these interactions by storing DOM measurements in variables for later reference instead of repeatedly measuring the DOM. We can also temporarily remove DOM nodes while doing work with them and then add them back when finished.
We can make use of requestAnimationFrame to improve performance of things changing the DOM and updating the screen.
We can also make sure our event listeners are not executing expensive code more than necessary by throttling and/or debouncing. We can also make them passive to improve scrolling performance
Finally, and this one is probably the least impactful on performance, but helpful to understand early on, we can make use of anonymous functions to simplify our code and reduce complexity and chances of name conflicts and a cleaner global scope.
I hope you found this helpful.