TechFrontend
Featured

How discovering a tiny Yarn command unlocked big optimizations

Profile Picture

Sivanesh Shanmugam

7 min read

How discovering a tiny Yarn command unlocked big optimizations

A single yarn command improved the performance of our app significantly. And The impact was immediate:

  • 40% improvement in HMR time (excluding TypeScript parsing)
  • 45% reduction in node_modules size
  • 9% improvement in yarn start time
  • 22% faster installs or package upgrades
  • 17% improvement in upgrade times
  • 12% lower CPU usage during builds

All from a single command I didn’t even know existed a week ago. Here’s what happened…

I recently ran into an issue trying to upgrade a package in one of our projects. The error messages made no sense at first: it turned out another dependency was still using an older version of that same package, causing a conflict. This got me curious about how Yarn resolves dependencies and what that yarn.lock file was really doing. I dove into our repository’s yarn.lock and began studying it to understand what was happening under the hood.

As I pored over yarn.lock, I noticed something interesting: many packages appeared multiple times with different versions. In other words, our project was installing duplicate versions of the same dependency. For example, Yarn might install debug@2.6.6 for one part of the app, even though debug@2.6.8 was already present elsewhere and could satisfy all version ranges. This is known as dependency duplication. In practice, it means our node_modules held redundant copies of packages, one for each version range that appeared in the dependency tree.

To illustrate this, imagine two libraries in our project both depend on a tooltip package, but one wants version ^5.0.0 and the other ^5.3.0. Yarn might end up installing two copies of tooltip (one at 5.0.0, one at 5.3.0) rather than reusing a single version.

The result is a heavier bundle and slower installs. In short, these extra versions hurt performance and bloat our app. And the pain doesn’t stop at node_modules. Those duplicate versions creep into every part of our development life cycle: installs take longer, the dev server takes its sweet time to boot, live reloads feel sluggish, Docker images get unnecessarily heavy, and even spinning up a container turns into a waiting game.

Why duplicates happen in Yarn

Understanding why Yarn does this by default helps explain the fix. Yarn is conservative about upgrading transitive dependencies: if you install a package at one time and then later install another that could use a newer version, Yarn will not retroactively bump the first one. For example, if at first you install libA (which pulls in libB@1.1.2) and then later add libC (which pulls in libB@1.1.3), you’ll end up with both libB@1.1.2 and libB@1.1.3 in your tree. This can happen invisibly whenever dependencies evolve over time.

I also learned about the handy yarn why <package> command. Running yarn why <name> shows why a package is installed and where it was pulled in from. Using yarn why helped me trace which parts of the app needed the older version of my troublesome package. It confirmed that multiple version requirements were indeed leading to duplicates.

Using yarn-deduplicate to clean up

With this understanding, I wanted to deduplicate the lockfile. The easiest solution was a small CLI tool called yarn-deduplicate that automatically cleans up yarn.lock. You can use this either by installing it globally or using npx.

To install it globally, you can run:

npm install -g yarn-deduplicate

(or you can use npx to run it without installing it globally)

To see the list of packages that has duplicates, you can run:

yarn-deduplicate --list

This command provides the list of packages that has duplicates.

To apply the changes directly to the lockfile, you can run:

yarn-deduplicate

This command scans the lockfile and chooses the minimal set of versions that satisfy all semver ranges. In my case, it consolidated many of the duplicates into single entries. The tool’s default strategy is to use the highest compatible version or the fewest total versions, and it updates the yarn.lock accordingly.

✨ I can clearly see a 3,000 lines reduction in the yarn.lock file of my repository!

Now you can install the dependencies with the updated lockfile:

yarn install

And now you can start the development server and test whether all functionalities are working as expected. If something broke, identify which package (or version) is causing the issue and use Yarn resolutions to enforce specific versions for that problematic dependency.

Impressive results after deduplication

Once again displaying the metrics again for reference.

  • 40% improvement in HMR time (excluding TypeScript parsing)
  • 45% reduction in node_modules size
  • 9% improvement in yarn start time
  • 22% faster installs or package upgrades
  • 17% improvement in upgrade times
  • 12% lower CPU usage during builds

By removing all those duplicated entries from yarn.lock, our bundle shrank and installs became faster. Builds that once took longer to resolve dependencies sped up, and our webpack bundles dropped a bit in size.

With all these I was expecting to see a significant improvement in the application's performance. But it was not the case for me. The improvement was not as significant as I expected.

Why? Because in my repository all the duplications were primarily on the dev dependencies. And the dev dependencies are not included in the bundle. So the impact was not as significant as I expected.

But, if there are a lot of duplicates in your project that are included in the bundle, you will see a significant improvement in the application's performance.

Resolving remaining version conflicts

Most of the app kept working fine after deduplication. In a few cases, certain packages needed the older version to function correctly. If a part of our app started breaking after the upgrade, I went into package.json and used Yarn’s resolutions field to pin a compatible version. For example:

"resolutions": {
  "lodash": "4.17.21",
  "some-lib/lodash": "4.17.21"
}

This ensures all parts of the project use the exact version we specify. Using resolutions, I ensured that any “sweet spot” version of a package was used everywhere, avoiding conflicts without reintroducing unwanted duplicates.

What's next?

You might be thinking, “Great, the issue is fixed for now!” But how do we make sure it never creeps back in? With multiple team members adding and upgrading dependencies, it’s all too easy for duplicates to sneak in again over time.

The long-term fix is to upgrade your tooling. For example, you can switch to Yarn Berry — a complete rewrite of Yarn that’s faster, more efficient, and handles deduplication automatically. Or, consider using one of the other modern package managers listed below that prevent duplicates by default.

  • npm – Since v7, npm deduplicates dependencies during npm install by default, flattening the tree and reusing existing versions if possible.

  • pnpm – Uses a content-addressable store and a virtual node_modules structure that avoids duplicates unless version ranges truly conflict.

  • Yarn Berry (v2+) – Deduplicates aggressively as part of its resolver; with PnP, it eliminates node_modules entirely, and even with the node_modules linker, it keeps things flat.

  • Bun – Installs packages in a flat, deduplicated structure by default, similar to pnpm, for maximum speed and disk efficiency.

Wrapping it up

In the end, what started as a small curiosity about a yarn.lock file turned into a cascade of optimizations across our entire development workflow. One simple command shaved seconds off reloads, trimmed thousands of lines from our lockfile, reduced disk usage, and even lightened the load on our CPUs.

It’s proof that learning the fundamentals isn’t just “nice to have”, it’s a superpower. Sometimes, the smallest bits of knowledge lead to the biggest optimizations. And in this case, all it took was one tiny Yarn command to make a massive difference.

Thanks for reading! Hope it helps! 🚀

MailLinkedInGitHubTwitter