5 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 readingCode 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)

5. Ignoring Delivery Method

The way you deliver JavaScript to users can make all the difference.

Both the location and timing of the Javascript have a huge impact on your performance.

Timing

Use attributes on the <script> tag to control the timing of your Javascript to download, parse, and/or execute.

By default, your Javascript will download, parse, and execute immediately when it is encountered. But if you specify the attributes async or defer, some or all of the expensive Javascript work can be saved for later, allowing more important tasks to finish first or in the background, allowing the main thread to continue uninterrupted.

There are already great illustrations and posts out there on this, so I will just point to one here.

The attributes async and defer can be used only on external JavaScript files that are linked.

One advanced trick is to use the type = "module" attribute on an inline JavaScript tag. The benefit of this is that the JavaScript is treated as if you had applied the defer attribute, which, as mentioned, you cannot do on an inline <script> tag. Inline JavaScript does not require being downloaded from a server, although it cannot be cached like external files, which is why it is great for smaller scripts.

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.

Location

Javascript can be placed in a webpage document’s <head> tag, or later in the <footer> tag.

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.

Minification

When your script is ready for production, you can minify it to save on file size, which is of great help, as JavaScript is one of the most expensive file types by type. A reduction of 5kb in JS is not the same as a reduction of 5kb in HTML for example.

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.

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