DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Related

  • Why You Don’t Need That New JavaScript Library
  • Wow, pnpm, You’re Really Fast
  • Creating a Web Project: Key Steps to Identify Issues
  • Reactive Programming in React With RxJS

Trending

  • Spring Data Neo4j: How to Update an Entity
  • How to Use Terraform Import Block for Importing Resources
  • Git Tags vs AWS Tags: A Tag-Tastic Showdown!
  • From Data to Decisions: Visualizing SAP Insights With Python
  1. DZone
  2. Software Design and Architecture
  3. Performance
  4. Streamline npm Packages: Optimize and Boost Performance

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.

By 
Aishwarya Murali user avatar
Aishwarya Murali
·
Jan. 22, 25 · Analysis
Likes (0)
Comment
Save
Tweet
Share
3.2K Views

Join the DZone community and get the full member experience.

Join For Free

Sluggish 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. 

JavaScript
 
"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.

JavaScript
 
  "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

JavaScript
 
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):

JavaScript
 
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

JavaScript
 
export const greetUser = (name) => {
  console.log("Hello, " + name + "!");
}
greetUser("Jack");
greetUser("Jill");
greetUser("Bob");


Minified (Output: 110 bytes):

JavaScript
 
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

JavaScript
 
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):

JavaScript
 
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

JavaScript
 
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):

JavaScript
 
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

JavaScript
 
export function function1() {
  return 1;
}

export function function2() {
  console.log(2);
  return 2;
}


Minified (Output: 89 bytes): 

JavaScript
 
export function function1(){return 1}export function function2(){return console.log(2),2}


Example After Optimization

JavaScript
 
export const function1 = () => 1;

export const function2 = () => {
  console.log(2);
  return 2;
}


Minified (Output: 75 bytes):

JavaScript
 
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

JavaScript
 
export const SomeFunction = (x, y) => {
  const z = x + y;
  console.log(z);
  return z;
};


Minified (Output: 71 bytes):

JavaScript
 
export const SomeFunction=(o,n)=>{const t=o+n;return console.log(t),t};


Example With Optimization

JavaScript
 
export const SomeFunction = (x, y, z=x+y) => {
  console.log(z);
  return z;
};


Minified (Output: 58 bytes):

JavaScript
 
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.

Dependency Npm (software) Performance

Opinions expressed by DZone contributors are their own.

Related

  • Why You Don’t Need That New JavaScript Library
  • Wow, pnpm, You’re Really Fast
  • Creating a Web Project: Key Steps to Identify Issues
  • Reactive Programming in React With RxJS

Partner Resources


Comments

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends: