Streamline npm Packages: Optimize and Boost Performance
Bloated node_modules folders and slow build times stem from unmanaged npm dependencies, leading to inefficiencies and potential risks.
Join the DZone community and get the full member experience.
Join For FreeSluggish build times and bloated node_modules
folders are issues that many developers encounter but often overlook. Why does this happen? The answer lies in the intricate web of npm dependencies. With every npm install, your project inherits not only the packages you need but also their dependencies, leading to exponential growth in your codebase. As a result, it can slow down your daily workflow, making it ineffective and introducing security vulnerabilities.
In this piece, we’ll examine practical methods for auditing and refining your npm packages. By the end, you’ll have a clearer understanding of how to keep your project efficient and secure.
Understand Dependencies vs. DevDependencies
In the context of a project, "dependencies" refer to third-party libraries and other utilities necessary for your project to run in a production or testing environment. Including libraries in the right section is important to optimize your production build. For this, let's dig deeper into dependencies and devDependencies.
Dependencies: The Core of Your Project
Dependencies are the packages or external libraries that your project relies on to operate successfully in a production environment. When you install a package/library as a dependency, you're saying that your project requires this package to function effectively, not only during development but also when it's deployed and used by others.
For instance, if you're working on a React project, you would include React and ReactDOM as your core dependencies because they are essential for your React components to render in the browser. Every library you specify in your 'dependencies' section must and will be included in the build generated for the production environment. They affect everything from the size of your application to its performance and security.
"dependencies": {
"react": "^18.3.1",
"react-dom": "^18.3.1",
},
DevDependencies: Tools for Development, But Not for Production
Unlike regular dependencies required to run your application in production, devDependencies are used for tasks like testing, code analysis, local development, and building. They are not included when your project is deployed to a production environment. The development workflow often involves compiling source code, running tests, and linting to ensure code quality.
For example, if you're developing a React application, you might use Babel to transpile JSX into browser-readable JavaScript, and often, to build unit tests around your code, you will use Jest or Storybook. These tools are vital for your development process but are unnecessary when your application is in production.
"devDependencies": {
"storybook": "^8.3.5",
"eslint-plugin-storybook": "^0.9.0",
"webpack": "^5.94.0"
}
Reduce Production Load by Isolating DevDependencies
By isolating devDependencies, you can reduce the load on your production environment. When you install packages for production using the npm install --production
command, npm will not install packages listed under devDependencies. This results in a smaller footprint for your application, which can lead to faster deployment times and reduced bandwidth usage, both of which are important in a production setting.
Optimizing Third-Party Library Inclusions
One of the simplest ways to reduce build times is to optimize third-party libraries and include only what you need.
Install smaller Node modules over larger ones where possible. For instance, in a recent project, I used date-fns to format dates in a specific style. While the library offers over 200 date-related utility functions and includes around 5,000 files totaling 22MB, I avoided importing the entire library. Instead, I included only the specific file required for my date formatting utility, significantly reducing unnecessary bloat.
Similarly, lodash is a widely-used utility library offering hundreds of functionalities. In one project, I utilized the _compact function from lodash. Rather than importing the entire library, I imported just the specific library like this: "import compact from 'lodash/compact''' to use the necessary function. This approach helped to streamline the build and keep the dependency footprint minimal.
As projects grow, keeping a close eye on third-party dependencies and their impact on the build is essential. Whenever feasible, favor native JavaScript operations over third-party libraries to further minimize overhead and maintain a leaner codebase.
Tree Shaking
Tree shaking is an aptly named technique that works much like shaking a tree to remove dead leaves — it identifies and eliminates unused code by leveraging the static structure of ES2015 module syntax.
Enabling tree shaking is straightforward if you use Webpack, as it can be activated by setting the build mode to 'production.' However, before deploying, it's crucial to test it thoroughly in the development environment. To do this, set the mode to 'development' and enable optimization.usedExports
by setting it to true
in your Webpack configuration. When you run the build, Webpack will generate files with comments highlighting unused code (e.g., “/* unused harmony export square */").
Once you’ve reviewed the unused exports, switch the mode back to 'production' in your Webpack configuration. This enables usedExports: true
and minimize: true
, effectively marking and removing unused code from the production bundle. Structuring your code into modular units with clear export and import statements allows bundlers like Webpack to analyze the dependency graph efficiently and exclude unnecessary exports, resulting in a leaner and faster build.
Removing Unused Dependencies
Unused dependencies are equivalent to unused code, and this presents another easy opportunity to reduce the npm build size for larger projects. As the project evolves, many libraries become obsolete or vulnerable. Unless flagged by a security tool or when the build starts failing, these dependencies often go unnoticed. As the saying goes, "If it ain't broke, don’t fix it."
Depcheck is a great tool for analyzing the dependencies in a Node project and finding unused dependencies. It’s simple to run once you have npm installed using npx, which is a package runner bundled in npm: npx depcheck
.
ESLint is another commonly used linter that comes with built-in plugins and rules for catching unused imports. Both @typescript-eslint
and eslint-plugin-unused-imports
will highlight unused variables in your codebase. ESLint can be integrated into your CI/CD pipeline to ensure that every new commit is tested. This reduces the long-term overhead of managing unused dependencies and helps maintain a clean and efficient project structure.
Minification and Compression
Minification and compression are key techniques for reducing the size of your build, and Webpack offers excellent tools to achieve this. Minification and compression work together to shrink the size of text-based assets by up to 70%. Minification removes unnecessary characters from code files without altering their functionality. In Webpack's production mode, minification is applied automatically using the TerserPlugin for JavaScript.
For CSS, you can use plugins like css-minimizer-webpack-plugin
to achieve similar results. These practices can drastically reduce bundle sizes, especially when combined with thoughtful coding strategies. I added a separate section in this article to highlight some coding practices to help you further reduce your build size when minified.
Analyzing Module Size With Cost-of-Modules
Understanding the size impact of your installed modules is critical to keeping your project efficient. The CLI tool cost-of-modules provides a straightforward way to analyze the size of the libraries listed in your package.json
. By installing and running this tool within your project, you can identify large dependencies that might be contributing to unnecessary bloat.
To use cost-of-modules
, simply run the following command:
npx cost-of-modules
This will generate a report that breaks down the size of each module, helping you pinpoint oversized packages. With this information, you can make informed decisions about replacing heavy dependencies with lighter alternatives or refactoring parts of your code.
Coding Practices
Optimizing using the following coding practices can significantly impact the size of your npm build package. Incorporating the following techniques into your daily workflow can help ensure a lean and efficient codebase:
Remove Any Code Repetition
This is a general practice that should be followed for any programming language. If you notice repeated functionality, extract it into a separate reusable function. This not only reduces the overall size of your code but also improves readability and maintainability.
Example Before Optimization
export const greetUser1 = () => {
console.log("Hello, Jack!");
}
export const greetUser2 = () => {
console.log("Hello, Jill!");
}
export const greetUser3 = () => {
console.log("Hello, Bob!");
}
greetUser1();
greetUser2();
greetUser3();
Minified (Output: 256 bytes):
export const greetUser1=()=>{console.log("Hello, Jack!")};export const greetUser2=()=>{console.log("Hello, Jill!")};export const greetUser3=()=>{console.log("Hello, Bob!")};console.log("Hello, Jack!"),console.log("Hello, Jill!"),console.log("Hello, Bob!");
Example After Optimization
export const greetUser = (name) => {
console.log("Hello, " + name + "!");
}
greetUser("Jack");
greetUser("Jill");
greetUser("Bob");
Minified (Output: 110 bytes):
export const greetUser=e=>{console.log("Hello, "+e+"!")};greetUser("Jack"),greetUser("Jill"),greetUser("Bob");
Optimize Object Properties
There are some interesting things with objects. When frequently accessing the same object field, destructure the object and assign it to a variable. This reduces redundancy and helps minifiers compress the code more effectively.
Example Before Optimization
import { Acme } from 'acme';
export const func = () => {
const obj = new Acme();
console.log(obj.subObject.field1);
console.log(obj.subObject.field2);
console.log(obj.subObject.field3);
};
Minified (Output: 160 bytes):
import{Acme}from"acme";export const func=()=>{const e=new Acme;console.log(e.subObject.field1),console.log(e.subObject.field2),console.log(e.subObject.field3)};
Example After Optimization
import {Acme} from 'acme-lib';
export const func = () => {
const obj = new Acme();
const subObj = obj.subObject;
console.log(subObj.field1);
console.log(subObj.field2);
console.log(subObj.field3);
};
Minified (Output: 146 bytes):
import{Acme}from"acme-lib";export const func=()=>{const o=(new Acme).subObject;console.log(o.field1),console.log(o.field2),console.log(o.field3)};
Leverage Arrow Functions With ES6
Arrow functions allow for concise syntax and can help reduce the overall code size when minified. When declaring arrow functions in a row via const or let, all subsequent const or let except the first one are shortened. Next, arrow functions can return values without using the return keyword.
Example Before Optimization
export function function1() {
return 1;
}
export function function2() {
console.log(2);
return 2;
}
Minified (Output: 89 bytes):
export function function1(){return 1}export function function2(){return console.log(2),2}
Example After Optimization
export const function1 = () => 1;
export const function2 = () => {
console.log(2);
return 2;
}
Minified (Output: 75 bytes):
export const function1=()=>1;export const function2=()=>(console.log(2),2);
Stop Creating Variables in Functions
Though the minifier can do inline code insertion, trying to reduce the number of variables is a normal idea for optimization.
Example Before Optimization
export const SomeFunction = (x, y) => {
const z = x + y;
console.log(z);
return z;
};
Minified (Output: 71 bytes):
export const SomeFunction=(o,n)=>{const t=o+n;return console.log(t),t};
Example With Optimization
export const SomeFunction = (x, y, z=x+y) => {
console.log(z);
return z;
};
Minified (Output: 58 bytes):
export const SomeFunction=(o,n,c=o+n)=>(console.log(c),c);
Conclusion
Efficiently managing npm dependencies is essential for ensuring that your projects remain maintainable, secure, and performant. By adopting best practices as mentioned above, you can significantly optimize your build processes. Tools like cost-of-modules
and depcheck
, paired with thoughtful coding practices, provide actionable ways to reduce bloat and improve code quality.
The strategies outlined in this guide — whether minimizing unused dependencies, optimizing object properties, or leveraging modern JavaScript features — contribute to a leaner codebase and faster build times. As you integrate these techniques into your workflow, you'll not only enhance your project’s efficiency but also create a scalable foundation for future development. The journey toward streamlined npm builds begins with consistent and mindful optimization.
Opinions expressed by DZone contributors are their own.
Comments