TLDR - 10 JavaScript Optimization Tips for Faster Web Apps
JavaScript performance optimization is crucial for creating fast, responsive web applications. This article highlights 10 key techniques to boost performance and improve user experience. Minimize DOM Manipulation : Reduce reflows and repaints for better performance.
Use Efficient Looping Patterns: Opt for classic for or for...of loops over methods like forEach. Optimize Object and Array Access : Cache data to avoid redundant lookups. Leverage Browser Caching : Store frequently accessed data locally to speed up load times.
Use Throttling and Debouncing : Control event handling to prevent unnecessary function calls. Leverage Asynchronous Code : Keep the UI responsive by offloading tasks to the background. Optimize Network Requests (Lazy Loading) : Load resources only when needed to reduce initial load time.
Defer or Delay Non-Critical JavaScript : Prioritize important scripts to speed up page rendering. Minify and Bundle JavaScript Files : Reduce file sizes and improve load times. Remove Unused Dependencies : Cut down on unnecessary libraries to streamline your app.
When it comes to optimizing JavaScript code, there are countless tools and techniques available, but many of them lack context for implementation. In this article, we’ll not only cover the most common techniques but also provide examples and explain how they can be applied in your applications. Here are the top 10 techniques we found for optimizing your JavaScript code.
If you’re unfamiliar with the phrase “ DOM manipulation ” or are still unsure of what it actually means, let’s break it down. The DOM (Document Object Model) represents a web page as a tree of objects. Each HTML element is an object (or node), and when we modify the content or behavior of these objects in the DOM, we are practicing DOM manipulation.
Manipulating the DOM too frequently can be costly because every time the DOM is changed, the browser may need to recalculate the styles (reflow) and redraw parts of the page (repaint) . By minimizing DOM manipulations or batching them together, you can reduce the number of reflows and repaints, resulting in smoother performance.
Below is an html file as represented in the DOM tree above. This document displays the text “Hello, World” and a button with the value “Change Text”
In our JavaScript file below, the “ getElementById ” method in our document object accesses the element in our HTML with the id of “greeting” (h1 element above) and assigns it to “greetingByElement”. We then do the same on the next line for “changeTextBtn” (our button element). We then add an “ event listener ” to our button so that when it’s clicked, our h1 element will display “Hello, JavaScript!”.
Document fragments can be used to hold DOM elements temporarily in memory before appending them to the document in a single operation. Think of it as saving up all the changes you’d like to make and applying them all at once.
Below, we use a “for loop” to iterate through some data and create 100 new div elements with some content in them. We then append each of those divs to a “fragment”. Once the loop is complete, the “fragment” containing all of our new divs is appended to our DOM. Now the DOM re-renders once with all of our new divs instead of 100 times.
Using methods like “ requestAnimationFrame ”, “ setTimeout ” ,“ setInterval ” and utilizing the “ innerHTML ” element may also help minimize the number of DOM reflows and repaints by batching updates. Libraries like React abstract away the DOM update process and optimize when and how updates are made using a "virtual DOM."
JavaScript offers several ways to iterate through arrays and other iterable objects. For example, the forEach method will iterate over an array and execute a function on each element within the array. While methods like forEach, map, or reduce can be useful, they are not always the most efficient option, especially when iterating through large amounts of data.
A classic for loop or for…of loop is often a better choice . This is because these loops allow for early exits (such as break or return), which is not possible with forEach.
Below, this “for loop” will iterate through “array” and log each element to the console. If the element is even, we exit this loop with “return”. Exiting a loop like this would not be possible using “forEach”.
Below, we see a “for…of” loop . This would be used when you want to iterate through an array but do not need access to each element by its index.
A “ for…in ” loop is designed for iteration over keys (property names) in objects. These loops can technically iterate over arrays but it’s generally not recommended because they will also iterate over any custom properties or methods that are added to an array as well as properties added to its prototypal chain.
While using loops, it’s essential to be mindful of nested loops, as they can become expensive when working with larger datasets. Higher-order functions like “map”, “filter”, and “reduce" might be the right choice for readability but may introduce unnecessary computational costs. Making informed decisions regarding array and object iteration ensures that your code performs well, even with large data sets.
Optimizing how you retrieve information from your data structures can prevent your JavaScript from doing extra work. If you need to access the same property or element multiple times in a loop, it’s more efficient to store it in a variable .
Similarly, accessing the array’s length property inside a loop may lead to redundant calculations on each iteration. By caching the data and array length, you can make your loops cleaner and faster.
In the image above, both items[i] and items.length are accessed repeatedly in the inefficient example. Caching these values into variables (here we used “item” and “len”) reduces repetitive lookups and speeds up the loop. This is especially helpful when working with large datasets.
Storing data in an object instead of an array can also be a great way to optimize your JavaScript performance. Iterating through an array (especially a large one) can take up a lot of extra time.
Flattening deeply nested objects also reduces the effort JavaScript has to put into accessing properties inside nested structures.
Deeply nested objects can be a lot like those Russian dolls where you have to open up each one, layer by layer to reach the doll at the core. Similarly, accessing properties within nested objects can require a lot of effort from your Javascript. To take some of that load off, we can flatten our structure.
The example above declares a recursive function to flatten our deeply nested object so that all of the properties are accessible as top-level keys. If this code looks a bit intimidating, don’t worry! All you need to know for now is that, once our object is flattened, we can access the properties directly as seen below.
Directly accessing a property–as shown above– can also simplify the debugging process, as the property names are stated explicitly rather than hidden in variables. If you’re looking for another way to optimize your Javascript in frequent look-up situations or unique collections:
Maps allow keys to be of any type (even objects or arrays) and maintain the order in which keys are inserted.
Sets store unique values of any type and ensure that there are no duplicates. The Set object has methods like “add” and “has”, which perform in constant time O(1).
By caching lookups, minimizing your redundant operations, simplifying structures, and choosing the right tools like Maps and Sets, you can handle even large datasets with ease.
HTTP caching stores frequently accessed data locally, reducing server requests and improving load times. When you load resources such as images, the browser caches them, so it doesn’t have to request them again during subsequent visits.
Set caching headers during server development to instruct the client on how to handle caching, using headers like cache-control, expires, ETag, and last modified. The cache-control header is most commonly used today to define caching policies and resource expiration times.
Today, “cache-control” is used more frequently than other headers. Here’s what you need to know: The “cache-control” header specifies “caching directives”. “caching directives” tell a browser, proxy server or cache-server which resources (files) are cached and how.
Common directives are: “max-age” - sets how long to cache a file “no-store” - instructs client not to cache the file “must-revalidate” - tells client that it must make a new request for the file from the server each time it wants to load it define a route for handling GET requests to /api/data and set our caching headers on the response within our middleware.
set the “Cache-Control” header to allow the resource to be cached publicly for one hour (‘public, max-age=3600’). use the “Last-Modified” header to mark the response with the current date include an ETag header with a unique identifier ('12345').
The use of http caching is a bit nuanced and may seem like an intimidating and complicated practice but, once you get the hang of it, it can be a fantastic way to speed up your application and optimize your JavaScript code . By caching resources effectively, you can improve the speed of your website and reduce the load on the server.
5. Use Throttling and Debouncing for Event Handling
Throttling ensures that an operation triggered by an event (e.g., scrolling) will only occur once during a specified time period, even if the event is triggered multiple times. This can prevent events from being fired too frequently, which would otherwise lead to performance issues.
Without throttling , events triggered by scrolling, resizing, clicking, etc. could be called hundreds or even thousands of times in a very short period which can lead to slow rendering or high volumes of requests.
Debouncing , on the other hand, ensures that a function is only executed after a certain amount of time has passed since the last event trigger. For example, in a search bar, debounce is used so that an API request is only made once the user stops typing for a moment.
Debouncing can be used in the following way: Each time the event (in this case the user typing) is triggered, a timer is started.
If the event keeps firing (the user keeps typing), the function call is delayed until the event stops for the specified amount of time.
Once the event stops (the user stops typing for a specified time), the function is executed. In this case, the function being executed after the specified time is a fetch request to an API to try and complete the user’s input. Instead of making a request after every keystroke, we wait until the user stops typing for a moment, then make a request.
Both techniques reduce unnecessary function calls, which can help optimize JavaScript performance .
We take a more in depth look into these two techniques in our article Your Guide to Debouncing in JavaScript so, as you’re expanding your understanding of them, be sure to check it out. Once you get a bit more comfortable with them, they can be used to optimize your app by controlling how many events are triggered and how often.
JavaScript is single-threaded , meaning it executes one line of code at a time. When long-running synchronous code executes, it blocks the main thread, making the entire UI unresponsive. Asynchronous code, however, allows your code to run without blocking the main thread, keeping your UI responsive.
In the example below, the thread of execution will not move on to the console.log until the “for loop” completes. In order to optimize your JavaScript code, it’s always a good idea to use asynchronous code for tasks like: Delaying execution (like in animations or transitions)
Common asynchronous methods include setTimeout , setInterval , and Promises . For example, using async/await syntax makes asynchronous code more readable and easier to manage, improving performance by offloading time-consuming tasks to the background. Let’s take a look at some common ways to handle asynchronous operations in your code.
setInterval will continue running a function at a given interval. Like setTimeout, it takes two parameters: the function to be run and the amount of time to let pass between invocations. Take a peek at Codesmith’s Asynchronous JavaScript unit in CSX if you want to learn more about these methods.
Leveraging the Promise object is essential in working with asynchronous code. A Promise object is used to store the eventual return from an asynchronous operation (like a fetch request). A Promise takes two parameters: one for a successful return (“resolve”) and one for an unsuccessful return (“reject”). These parameters are functions that either resolve the Promise with a value or reject it with an error.
The “fetchData” function is designed to do the following: fetch data from a specified URL and return a new Promise. Inside the Promise, the “fetch” function is called to make an asynchronous HTTP request to the passed in URL.
The first .then() method checks if the response is truthy or falsey. If falsey, it rejects the Promise with an error message. If the request is successful, the response is parsed as JSON Invoking response.json() returns a new Promise because parsing is asynchronous.
The second .then() method processes the parsed data and passes it to the resolve function which fulfills the Promise .
Finally, if any errors have occurred throughout the process, the catch() method will invoke the reject function and pass in the error.
We define an asynchronous function “fetchUserData” using the “async” keyword. This indicates that it contains asynchronous operations.
Inside the function, we use a try block to handle the functionality. The await keyword is used to pause the thread of execution until the fetch request completes.
Once the response is received, we use “await” again to pause the execution until the response is parsed as JSON (also asynchronous). After the data is successfully parsed, it is logged to the console. If any errors occur during the fetch request or JSON parsing, the catch block will handle the error by logging it to the console.
Asynchronous code can feel a bit tricky. But when it comes to JavaScript performance optimization, this technique is essential in offloading long running tasks from the main thread keeping your app responsive and performant. For more information on the inner workings of asynchronous code, check out Codesmith’s “Hard Parts” lecture on Async and Promises.
7. Optimize Network Requests (Lazy Loading)
Network requests are essential for web applications, but they can slow down performance if too many resources are loaded at once. Whenever you visit a website, the browser sends a network request to a server to ask for the files needed to display the webpage (scripts, images, stylesheets, etc.). The requests travel over the internet, and the server responds by sending the requested resources (or an error!) back to the browser.
For applications that only render small pages with limited data, the process can happen quickly. But for data heavy applications that need to load up lots of images or third-party scripts (ads, embedded video content, etc.) these requests can accumulate , slowing down load times.
Lazy loading helps optimize network requests by deferring the loading of resources until they are actually needed.
For example, images are commonly lazy-loaded, meaning they are only loaded when they scroll into view. By implementing lazy loading, you reduce the number of resources requested upfront , leading to faster page load times and better overall performance.
In this example HTML, each image is set with a lightweight placeholder image, and a “data-src” attribute to hold the real image source. We tell our “IntersectionObserver” to “observe” the images (“lazyImages”) with our data-src attributes and placeholders.
When those placeholder images come into the browser’s viewport, we take the real image file from our data-src attribute, and place it in a src attribute to render. Then we stop “observer” from observing the loaded image. Note: in Javascript, we use “dataset.src” to access the data-src HTML attribute.
Offscreen images will only load the lightweight placeholders, and as you scroll them into view, the real image is loaded!
If Javascript performance optimization is your priority, Lazy loading can be tremendously helpful . When you only load the resources you need, you improve your initial load times because fewer resources are required upfront. This improves overall user experience because your application will be more responsive and speedy, especially on slower networks.
By default, when a browser encounters a <script> tag in HTML, it stops parsing the HTML to download and execute the script. This can lead to delays in rendering, especially if the script is non-critical.
To improve load times, you can use the defer or async attributes on <script> tags. The defer attribute ensures the script is executed only after the HTML is fully parsed, while async executes the script as soon as it is ready, without waiting for the rest of the HTML.
Let's see how we can structure these tags: The browser pauses parsing to download and execute this script immediately. This makes sure that core functionality (like navigation, buttons, or critical interactions) is ready as soon as possible. This script starts downloading in the background while the browser continues parsing the HTML.
It executes only after the entire document has been fully parsed, making it ideal for non-urgent features like animations, widgets, or UI enhancements. This script downloads at the same time as the HTML parsing and executes immediately when it’s ready.
Because it’s independent and doesn’t rely on other scripts, it’s perfect for non-critical functionality like ads, analytics, or tracking tools. Given the above setup, the scripts will load and execute in the following order: essential.js: Blocks parsing and runs first.
thirdPartyScripts.js : Downloads in parallel and runs as soon as it’s ready. nonCritical.js : Executes last, after the HTML parsing is complete.
By controlling script loading, you ensure that critical functionality is prioritized, which reduces bottlenecks and speeds up page loading.
As your JavaScript application grows, the number of files and the size of your codebase increase, which can slow down page load times. Minifying JavaScript removes unnecessary spaces, characters, and comments, reducing the file size.
Minifying refers to the concept of eliminating any unnecessary spaces, characters, breaks, etc. without changing the code’s functionality. Below is a basic example of two versions of a function “sum”- the original version, followed by a minified version.
Often, a file will have hundreds or even thousands of lines of code. In those cases, minification can significantly reduce file sizes, improve load times, and ultimately provide users with the smoothest experience possible.
Bundling your code is the process of combining your JavaScript files into one or a few files. When your code is broken up into several files, the browser has to make multiple HTTP requests to fetch each one of them. As we know, HTTP requests are asynchronous meaning they take time. Having too many of them can significantly slow down your application.
Remember how we talked about HTTP caching for JavaScript performance optimization? Well, you can improve your code even further by caching a bundled file. Let’s say we have three files (index.js, app.js and api.js). We can bundle these files into one (bundle.js) and then have the browser cache that bundle. That way, after the initial load, users won’t need to download those files again- as long as the bundle remains unchanged.
With several files bundled into one and then minified, you can significantly reduce the size of your application resulting in faster load times and happy users. Fortunately there are tools for doing such.
Using module.exports we define and export the configuration object so that it can be used by Webpack. In Node.js, exporting a file makes it available to be used (required in) by other modules (files). This makes our object available to Webpack so that it can bundle and minify our project.
The output property is an object that defines how and where the bundled files will be saved:
filename : The filename property specifies the name of the output file. The bundled code will be saved in a file named “bundle.js”.
path : The path property specifies the directory where the output file will be placed. path.resolve(__dirname, 'dist') creates an absolute path to the dist directory within the current folder (__dirname). This makes sure that the output file “bundle.js” will be saved in the dist directory.
Finally, the mode property specifies the mode in which Webpack will run. Setting it to 'production' allows optimizations like minification (shortening code) and tree-shaking (getting rid of unused code), which reduces the size of the output bundle and improves performance. Production mode is typically used for deploying the application whereas setting mode to Development unlocks tools for debugging while developing an app.
