Benchmarking npm NodeJS Package Versions Without Polluting Your Project
When working with critical dependencies like hashing libraries, it's essential to know what each version is doing under the hood β and what it's costing you. That's where a local speedtest setup shines.
In this guide, we walk through how to benchmark two versions of the same npm package (bcrypt) without polluting your global install, monkey-patching node_modules, or relying on Docker or VMs. We'll isolate each version, test performance using Node.js' perf_hooks, and make it dead simple to compare output.
Check out and clone the Github repo for the complete code.
π§ Intro: Why Benchmarking npm Packages Matters
When working with critical dependencies, like hashing libraries, it's essential to know what each version is doing under the hood β and what it's costing you. That's where a local speedtest setup shines.
Let's walk through how to benchmark two versions of the same npm package (bcrypt), without polluting your global install, monkey-patching node_modules, or relying on Docker or VMs. We'll isolate each version, test performance using Node.js' perf_hooks, and make it dead simple to compare output.
This guide shows you how to:
β Install and isolate two different versions of the same bcrypt npm package
β Compare their runtime performance side-by-side
β Do it all without polluting your global system or node_modules/
Perfect for performance comparisons, regression checks, upgrade testing β or just satisfying your curiosity.
π What Is bcrypt?
bcrypt is a widely used password hashing function designed for secure storage of user passwords. In JavaScript, it's available via npm (bcrypt, bcryptjs) and used in many authentication systems. It's computationally expensive by design β making brute-force attacks slower β but that also makes performance differences between versions worth tracking.
π§Ό Preventing .tgz Bloat in Git Repositories
If you run npm pack inside a Git-tracked directory (like a blog demo or benchmark repo), it will generate .tgz files β e.g., bcrypt-6.0.0.tgz. These are binary tarballs of published npm packages, and they can quickly clutter your version history if committed by mistake.
To prevent that:
β Add *.tgz to .gitignore
Update your .gitignore file to include:
# Ignore local package tarballs (e.g. from npm pack)
*.tgz
This ensures any .tgz files you generate locally are ignored by Git, so you don't accidentally commit them.
π§ Why It Matters
- .tgz files can be large and unnecessary in source control
- They're reproducible via npm pack <package>@<version> anyway
- Keeping them out of Git makes your repo faster to clone and cleaner to review
π Optional: Ignore version test folders
this way you can keep version-specific folders (like versions/v6/, versions/v5/) that only exist for local benchmarking:
/versions/
They can be useful to include if you're making a reproducible benchmark repo for others.
π§ͺ Benchmarking Two Versions of bcrypt Locally (No TS, No Global Installs)
We'll compare:
- bcrypt@6.0.0 (latest as of now)
- bcrypt@5.0.0 (released 5 years ago)
π§± Step 1: Set up your project
Create the project directory (or repo) with mkdir if you haven't already:
mkdir bcrypt-speedtest
cd bcrypt-speedtest
npm init -y
This creates a package.json, and the -y flag just accepts all of the defaults. This should suffice unless you plan on making a benchmark application out of it.
π¦ Step 2: Download .tgz files for each version
This grabs the actual tarballs (like what npm install uses internally):
npm pack bcrypt@6.0.0
npm pack bcrypt@5.0.0
You should now have:
bcrypt-6.0.0.tgz
bcrypt-5.0.0.tgz
These are self-contained versions of the package.
π Step 3: Install each version in its own isolated directory
mkdir -p versions/v6 && cd versions/v6
npm init -y
npm install ../../bcrypt-6.0.0.tgz
cd ../..
mkdir -p versions/v5 && cd versions/v5
npm init -y
npm install ../../bcrypt-5.0.0.tgz
cd ../..
This older version should give you a deprecation warning:
npm install ../../bcrypt-5.0.0.tgz
npm warn deprecated inflight@1.0.6: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.
npm warn deprecated rimraf@2.7.1: Rimraf versions prior to v4 are no longer supported
npm warn deprecated glob@7.2.3: Glob versions prior to v9 are no longer supported
npm warn deprecated npmlog@4.1.2: This package is no longer supported.
npm warn deprecated node-pre-gyp@0.15.0: Please upgrade to @mapbox/node-pre-gyp: the non-scoped node-pre-gyp package is deprecated and only the @mapbox scoped package will recieve updates in the future
npm warn deprecated are-we-there-yet@1.1.7: This package is no longer supported.
npm warn deprecated osenv@0.1.5: This package is no longer supported.
npm warn deprecated gauge@2.7.4: This package is no longer supported.
added 69 packages, and audited 70 packages in 8s
3 packages are looking for funding
run `npm fund` for details
3 moderate severity vulnerabilities
Some issues need review, and may require choosing
a different dependency.
Run `npm audit` for details.
Now you have:
versions/
βββ v5/
β βββ node_modules/bcrypt/...
βββ v6/
β βββ node_modules/bcrypt/...
These two folders each contain an independent version of bcrypt:
$ tree -a -L 2 . -I node_modules
.
βββ .git
β βββ config
β βββ description
β βββ FETCH_HEAD
β βββ HEAD
β βββ hooks
β βββ index
β βββ info
β βββ logs
β βββ objects
β βββ packed-refs
β βββ refs
βββ .gitignore
βββ bcrypt-5.0.0.tgz
βββ bcrypt-6.0.0.tgz
βββ LICENSE
βββ package.json
βββ README.md
βββ speedtest.js
βββ versions
βββ v5
βββ v6
10 directories, 13 files
π§ͺ Step 4: Create speedtest.js to compare them
Use your favorite IDE (like Sublime or VS Code) and create a new speedtest.js file in the root of the project. You can also use the touch terminal command (on a POSIX-compliant system like macOS or Linux) to create the file:
touch speedtest.js
The file should then import perf_hooks to run the benchmark:
// speedtest.js
const { performance } = require("perf_hooks");
const testPassword = "SuperSecret123!";
const saltRounds = 10;
const runAndTime = async (label, hashFn) =οΌ {
const start = performance.now();
await hashFn();
const end = performance.now();
console.log(`${label} took ${(end - start).<span class="md-call-method">toFixed</span>(2)}ms`);
};
(async () =οΌ {
// Load each version via dynamic import
const bcrypt6 = require("./versions/v6/node_modules/bcrypt");
const bcrypt5 = require("./versions/v5/node_modules/bcrypt");
// Run timing benchmarks
await runAndTime("bcrypt v6.0.0", () =οΌ
bcrypt6.hash(testPassword, saltRounds)
);
await runAndTime("bcrypt v5.0.0", () =οΌ
bcrypt5.hash(testPassword, saltRounds)
);
console.log("\nβ
Done.");
})();
This script:
- Dynamically loads both versions of bcrypt directly from their folders
- Hashes a sample password using each one
- Prints out how long each version took
π Step 5: Run the NodeJS Script
node speedtest.js
Example output:
node speedtest.js
bcrypt v6.0.0 took 86.65ms
bcrypt v5.0.0 took 55.03ms
β
Done.
β
Done.
As we can see, the older version of bcrypt is running a lot slower than than v6.
NOTE:Times vary slightly depending on CPU load.
π§ Why This Works
- You're not using global installs, so versions won't conflict
- require() lets you pull specific versions from isolated node_modules
- .tgz ensures you're benchmarking the exact package from the registry
- This method works for any npm package
π Bonus: Loop for a Real NodeJS Stress Test
Want to push each version for a few seconds to see how they hold up over time? Here's a simple looped version:
const { performance } = require("perf_hooks");
const bcrypt6 = require("./versions/v6/node_modules/bcrypt");
const bcrypt5 = require("./versions/v5/node_modules/bcrypt");
const PASSWORD = "SuperSecret123!";
const ROUNDS = 10;
const ITERATIONS = 20;
async function benchmark(label, bcryptModule) {
const start = performance.now();
// Call hash() method in a loop
for (let i = 0; i οΌ ITERATIONS; i++) {
await bcryptModule.hash(PASSWORD, ROUNDS);
}
const end = performance.now();
const total = end - start;
console.log(`${label} (${ITERATIONS} iterations): ${total.<span class="md-call-method">toFixed</span>(2)}ms`);
}
(async () =οΌ {
await benchmark("bcrypt v5.0.0", bcrypt5);
await benchmark("bcrypt v6.0.0", bcrypt6);
})();
Now let's use the node command to run the script again:
node speedtest.js
bcrypt v5.0.0 (20 iterations): 1145.87ms
bcrypt v6.0.0 (20 iterations): 1357.82ms
π Why Is bcrypt v6 Slower Than v5?
Interestingly, our results show that bcrypt@5.0.0 consistently outperforms bcrypt@6.0.0 in hashing speed:
bcrypt v6.0.0 (20 iterations): 1366.76ms
bcrypt v5.0.0 (20 iterations): 1114.31ms
This may surprise some developers expecting performance improvements in newer releases. But newer versions often prioritize security patches, compatibility updates, or bundling improvements over raw speed. In bcrypt's case, the move to newer Node.js versions or underlying binary changes (like prebuilt binaries or upgraded dependencies like node-gyp) could account for the added overhead, or older versions may simply run faster as asynchronous operations.
Bottom line: newer isn't always faster, and this benchmarking technique helps make that transparent.
β Summary
Step | What You Did |
---|---|
β | Used npm pack to grab exact .tgz versions |
β | Installed each in an isolated versions/ folder |
β | Used require() to test them independently |
β | Ran timing benchmarks and looped stress tests |
π Conclusion
This is one of those underutilized tricks that every serious Node.js developer should know. With just a few commands, you can isolate and benchmark multiple versions of any npm package β without needing a VM, Docker, or weird hacking of node_modules.
It's clean, repeatable, and gives you real answers about performance regressions, speed gains, or changes in behavior between versions.
Whether you're working on a package like bcrypt, a Markdown parser, or some custom algorithm β this pattern gives you confidence in your upgrades and insight into your tools.