Reducing JavaScript is a critical aspect of developing modern websites and a key aspect in terms of overall page efficiency.
As software engineering evolves, there is always a push for faster, more efficient sites with an overall smaller JavaScript payload. Unused JavaScript adds unnecessary bloat to your web applications and slows down overall performance.
In this article, you will learn about a series of patterns and tips that you can use to reduce this bloat by removing unused JavaScript, helping you save time, optimize performance, and improve efficiency.
In the simplest terms, unused JavaScript — often called dead code — is any code that your web app doesn’t use or need, but exists in the final JavaScript bundle that you ship to the browser. This dead code sits dormant and increases the overall size of your JavaScript bundle, which can impact performance.
There could be many reasons for unused code in your JavaScript bundle.
The most obvious reason is that you might have added code that you no longer need, but you forgot to remove it in the final bundle. These could be functions, classes, or variables that are no longer being executed, called, or used anywhere in your app.
Another reason could be unused dependencies. In other words, you might be using a third-party JavaScript dependency in your code that you are not using. Even worse, those dependencies could have their own unused JavaScript, further adding to the unnecessary bloat in your project.
There are a couple of ways you can remove unused JavaScript from your web apps. These tips and patterns will help you ship more robust and efficient JavaScript bundles to the web, whether you are using plain old JavaScript or any dedicated libraries or frameworks like React, SolidJS, or Vue.js.
Code splitting is a technique used to split JavaScript code into smaller, more manageable chunks. You can then load these chunks on demand or in parallel network requests, meaning you don’t have to load your entire JavaScript bundle every time — only what’s necessary.
Imagine you had a single JavaScript bundle like the following:
<script src="mainbundle.js"></script> // 60kb file
You can split it into smaller chunks that can be downloaded only when needed:
<script async defer src="chunk1.js"></script> // 30 kb file <script async defer src="chunk2.js"></script> // 30 kb file <script async defer src="chunk3.js"></script> // 30 kb file
This strategy reduces the overall network load on the main thread responsible for initializing and downloading JavaScript scripts:
If you’re using a JavaScript library or framework like Next.js or Vue, you don’t need to do this manually — most modern JavaScript frameworks support code splitting by default.
However, you can delegate some of the components used as server-side only. This helps the framework “smartly” split JavaScript into even smaller chunks that shouldn’t be bundled with client-side JavaScript chunks.
Many organizations use this strategy in production, which speaks to its efficiency. Here’s an example of code splitting in action on the Loom website:
Tree shaking refers to eliminating dead code — i.e., the unwanted JavaScript that is not being used by your app. Many popular bundlers, such as webpack, Rollup, or Vite, use a tree-shaking approach while building the JavaScript chunks that you ship to the browser.
To ensure tree shaking in your bundle, always use the modern ES6 syntax in your JavaScript components — that is, import
and export
syntax:
// default import import Navbar from 'Components' // named import import { Navbar } from 'Components' // default export export default Navbar // named export export { Navbar }
The modern ES6 syntax helps your bundler identify dead code, similar to how ESLint points out if there is an imported component but it’s not being consumed anywhere.
Less JavaScript in your bundle means less time needed for a browser to download the bundle. To ensure your JavaScript is as light as it can get, always minify it before shipping.
Minifying JavaScript will trim out whitespaces, syntax highlighting, comments, and other parts of your code that you don’t need in the final production build. These unnecessary snippets take up space in the bundle as dead code.
Even simple-looking JavaScript code can be compressed and modified. Here’s an example of simple JavaScript code before minification:
// add function const add = (a, b) => { return a + b } // call add function add(3, 4)
Here’s how this code looks after minification:
const add=(d,a)=>d+a;add(3,4);
Imagine the effect minifying JavaScript would have on large-scale bundles!
JavaScript minification is easier than you might think. You can pick from a number of JavaScript minification tools available online, such as Terser, Ugligy, babel-minify, and more.
A small tip that can save your network bandwidth significant amounts of time is always loading JavaScript asynchronously.
One way you can do this is by adding async
and defer
to your JavaScript scripts. This will automatically handle JavaScript downloading and won’t delay or block your HTML from parsing or rendering while JavaScript is being loaded.
async
and defer
attributes handle JavaScript script downloading and execution in a slightly different order. You can pick what is best for your project.
An async
JavaScript script works as shown in this image:
Meanwhile, a defer
JavaScript script works like so:
In many cases, adding both async and defer gets the job done correctly. Here’s an example of how you could write this:
<script async defer src="bundle.js"></script>
Dynamic imports are now possible in plain JavaScript thanks to ES6 modules. These are particularly helpful when you want to load JavaScipt modules or scripts conditionally. Here’s how you would write dynamic imports:
import('./utility.js') .then((module) => { // use utility code here }) .catch((error) => { // catch errors });
This strategy enables us to request a JavaScript bundle after certain conditions are met, as opposed to importing every JavaScript component at the top of the file. Consider this code snippet, where the module is loading only after attaching the event listener:
document.getElementById('dashboard').addEventListener('click', () => { import('./utility.js') .then((module) => { // Use utility module APIs module.callSomeFunction() }) // catch unexpected error here .catch((error) => { console.error("Oops! An error has occurred"); }); });
Lazy loading in JavaScript is a simple but quite useful pattern. When done correctly, lazy loading can help you conserve network bandwidth. The fundamental rule is to load only those JavaScript modules that are needed at the current time.
One pattern you can follow is to always load JavaScript on the viewport height. Suppose you had a very extensive list of users. You might not want to load the information of, say, the 300th user, as this information isn’t needed or even visible within the current viewport.
There are some really brilliant libraries out there for this particular use case, which delegate JavaScript modules to only load when a specified user — such as the 300th — reaches the current viewport.
Lazy loading is not just limited to lists. Assets such as images, videos, a large number of nodes, and more can be lazy loaded as well. For example, you can put up a placeholder before the actual image and its associated JavaScript gets downloaded by the browser.
These patterns not only help you with shipping less JavaScript code but improve user experience by a huge margin as well.
As the JavaScript ecosystem keeps improving, your code and the libraries you use should keep up with any changes. This will help make your code more robust and secure for future releases.
If you’re using any third-party libraries in your code, make sure to always use the latest versions and check that they have not been deprecated. Many libraries, especially smaller, open source libraries that are maintained on a volunteer basis, can become outdated or get deprecated unexpectedly.
In an npm or Node.js-based development environment, you can run the following code to check for library deprecations:
npx depcheck
Your choice of external dependencies or third-party libraries can affect how much JavaScript you are shipping. Always make sure any library you pick for your project is actively maintained, uses modern ECMAScript versions, and most importantly, is lightweight.
You can use tools like Bundlephobia to check for JavaScript size, overall versioning, and even how much time it takes for a particular library to download on a slow connection. You might find a better library out there that does a better job and is more lightweight compared to what you are using.
For example, if you check React using Bundlephobia, you will find out it is pretty fast — just 3ms on a 4G connection and 50ms on a slow 3g connection:
This is quite impressive for a JavaScript library with millions of downloads each week. Furthermore, it has just one dependency, making it quite lightweight at just over 6kB minified.
This qualifies React as quite a good pick for an external dependency to your project in terms of speed, JavaScript bundle size, and scalability.
In this article, we discussed eight methods you can use to ship less JavaScript and improve your project’s performance.
Every project is different and has different architecture and patterns being used. A lot of the tips mentioned here will work for you, but some might not.
Use your best judgment to determine what works best for you and your project. This will enable you to confidently make your sites and apps more robust.
Debugging code is always a tedious task. But the more you understand your errors, the easier it is to fix them.
LogRocket allows you to understand these errors in new and unique ways. Our frontend monitoring solution tracks user engagement with your JavaScript frontends to give you the ability to see exactly what the user did that led to an error.
LogRocket records console logs, page load times, stack traces, slow network requests/responses with headers + bodies, browser metadata, and custom logs. Understanding the impact of your JavaScript code will never be easier!
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowCreate a multi-lingual web application using Nuxt 3 and the Nuxt i18n and Nuxt i18n Micro modules.
Use CSS to style and manage disclosure widgets, which are the HTML `details` and `summary` elements.
React Native’s New Architecture offers significant performance advantages. In this article, you’ll explore synchronous and asynchronous rendering in React Native through practical use cases.
Build scalable admin dashboards with Filament and Laravel using Form Builder, Notifications, and Actions for clean, interactive panels.