4 JavaScript Mistakes Beginners Must Avoid

This is what I wish I understood when I first was adding JavaScript to a website.

Your JavaScript may “work”, but is it really as performant for your site as you think?

1. Failing to Batch Reads and Writes

We can write one or two simple lines of JavaScript for the web, and think it’s so concise and lightweight, right?

myElem.classList.toggle("some-class"); // NO-NO: writing before reading const someWidth = anotherElem.clientWidth;
Code language: JavaScript (javascript)

Well, yes…but as a beginner, I did not realize that when the browser executes JavaScript that writes to the DOM, and then reads from the DOM, the browser may have to recalculate the layout of the whole page before proceeding with the script.

So we can do our reading first, and then writing afterwards.

const someWidth = anotherElem.clientWidth; myElem.classList.toggle("some-class"); // BETTER: writing after reading
Code language: JavaScript (javascript)

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.

// Approach 1: BAD const listItems = document.querySelector('.some_items'); const myElem = document.querySelector('.my_element'); for(let i = 0; i < listItems.length - 1; i++) { if(myElem.clientWidth >= myElem.scrollWidth){ //NO-NO // Code } } // Approach 2: BETTER const listItems = document.querySelector('.some_items'); const myElem = document.querySelector('.my_element'); const elemWidth = myElem.clientWidth; const elemScrollWidth = myElem.scrollWidth; for(let i = 0; i < listItems.length - 1; i++) { if(elemWidth >= elemScrollWidth){ //BETTER: using static values instead of repeatedly measuring DOM // Code }
Code language: JavaScript (javascript)

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.

const someWidth = anotherElem.clientWidth; requestAnimationFrame(function(){ myElem.classList.toggle("some-class"); });
Code language: JavaScript (javascript)

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.

function myFunction() { // Do stuff } window.addEventListener('scroll', myFunction);
Code language: JavaScript (javascript)

Some events, like scroll or 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.

let throtFlag = false; function myFunction() { // Do stuff } window.addEventListener('scroll', function() { // only run if we're not throttled if (!throtFlag) { // actual callback action myFunction(); // we're throttled! throtFlag = true; // set a timeout to un-throttle setTimeout(function() { throtFlag = false; }, 400); } });
Code language: JavaScript (javascript)

Another approach is to debounce the code, to make the code execute once after a specified amount of time after the event is complete.

let timeoutD = false; // holder for timeout id function myFunction() { // Do stuff } window.addEventListener('resize', function() { clearTimeout(timeoutD); // clear the timeout // start timing for event "completion", where the second numerical param is the delay after event is "complete" to run callback timeoutD = setTimeout(myFunction, 100); });
Code language: JavaScript (javascript)

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.

window.addEventListener('resize', function() { // Code }, {passive: true});
Code language: JavaScript (javascript)

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 const and let instead of var.

function myFunction{ const var1 = 1; // scope limited to myFunction let var2 = 2; // scope limited to myFunction and can be reassigned within var var3 = 3; // global scope }
Code language: JavaScript (javascript)

The block-level const and 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.

function myFunction() { document.body.classList.toggle("some-class"); } requestAnimationFrame(myFunction);
Code language: JavaScript (javascript)

Regular named function used as callback

requestAnimationFrame(function () { document.body.classList.toggle("some-class"); });
Code language: JavaScript (javascript)

Anonymous function used as callback

Modern browsers can also make use of arrow functions, which are just less verbose anonymous functions.

requestAnimationFrame(() => document.body.classList.toggle("some-class") );
Code language: JavaScript (javascript)

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:

(function () { // Code })
Code language: JavaScript (javascript)
()=> { // Code }()
Code language: JavaScript (javascript)
() => ( // Code )
Code language: JavaScript (javascript)
{ // Code }
Code language: JavaScript (javascript)

Conclusion

It’s not only about what the JavaScript does, but how it’s delivered to the browser that determines a significant part of the performance impact.

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.

Leave a Comment