In 2026, writing a new MD5 library might seem odd. The algorithm is considered outdated, browsers are gradually removing it from Web Crypto API, and Node.js documentation includes warnings about its use.
But reality is more nuanced. MD5 is still very much alive in:
- File checksums (many repositories still publish MD5 hashes)
- Content-based caching (cache keys derived from content)
- Legacy protocols (some APIs still require MD5 signatures)
- Internal identifiers (where security isn't critical)
The problem? Existing implementations are either outdated, lack TypeScript support, or don't work universally (Node.js + browser). I decided to fix this and created pure-md5 — a modern, typed, and adaptive library.
In this article, I'll share the technical decisions, architecture, and show why it might be useful in your projects.
⚠️ Important Security Warning: MD5 is cryptographically insecure. Do not use for passwords, tokens, or digital signatures. Only use for checksums, caching, and compatibility purposes.
⚠️ v0.2.2 Update: Tree-shaking support added!
• Useimport { md5 } from 'pure-md5/md5'for ~1.4KB gzipped
• Full bundle: ~6KB (was<1KB)
• WHATWG streams & improved exports
Why Not Built-in crypto?
The first question that comes to mind: "Why a new library when Node.js has crypto and browsers have SubtleCrypto?"
| Problem | Node.js crypto
|
Browser SubtleCrypto
|
pure-md5 |
|---|---|---|---|
| Works in browser | ❌ | ✅ | ✅ |
| Works in Node.js | ✅ | ❌ | ✅ |
| TypeScript out of box | ❌ (needs @types) | ⚠️ Limited | ✅ |
| MD5 support | ✅ | ⚠️ (being removed) | ✅ |
| Bundle size | Native | Native | ~1.4KB gz1 |
| Dependencies | 0 | 0 | 0 |
1 ~1.4KB gzipped with tree-shaking (md5 only). Full bundle: ~6KB gzipped.
Key advantage of pure-md5: universality. You write code once, and it works everywhere. The library automatically selects the optimal adapter:
// Same code works in both Node.js and browser
import { md5, hashFile } from 'pure-md5';
const hash = md5('hello');
// "5d41402abc4b2a76b9719d911017c592"
const fileHash = await hashFile('large-file.bin');
// { digest: '...', bytesProcessed: 1048576 }
Tree-Shaking Support (New in v0.2.1+)
Important: pure-md5 now supports modern tree-shaking for smaller bundle sizes. Here's how to use it:
Import Strategies
For minimal bundle (recommended):
import { md5 } from 'pure-md5/md5'; // Only md5() function
// Result: ~1.4KB gzipped
For full API:
import { md5, hashFile, createMD5Stream } from 'pure-md5';
// Result: ~6KB gzipped
Why Tree-Shaking Matters
-
md5()alone: ~500 bytes raw, ~1.4KB gzipped - Full library: ~17KB raw, ~6KB gzipped
- Tree-shaking benefits: Only included code that you actually use
Build Tool Configuration
Most modern bundlers enable tree-shaking by default:
Vite:
// vite.config.js
export default {
optimizeDeps: {
include: ['pure-md5/md5'] // Pre-bundle specific entry
}
};
Webpack:
// webpack.config.js
module.exports = {
optimization: {
usedExports: true
}
};
Rollup:
// rollup.config.js
export default {
treeshake: true
};
Architecture: Adapter System
The heart of the library is an adaptive system that tries to use native APIs, falling back to pure JavaScript implementation when needed.
How Detection Works
// Simplified adapter selection logic
function detectAdapter() {
if (typeof process !== 'undefined' && process.versions?.node) {
return nodeAdapter; // Priority 1
}
if (typeof crypto !== 'undefined' && crypto.subtle) {
return webCryptoAdapter; // Priority 2
}
return pureJSAdapter; // Fallback
}
This gives several advantages:
- Performance: Uses native C++ code in Node.js (~1.15M ops/sec)
- Compatibility: Works in older browsers with JS implementation
- Future-proof: Continues working even if WebCrypto removes MD5
Working with Large Files: Streaming & Progress
For files larger than memory, use the streaming API:
import { hashFile } from 'pure-md5';
// Simple file hashing
const result = await hashFile('large-file.bin');
console.log('MD5:', result.digest);
// "5d41402abc4b2a76b9719d911017c592"
console.log('Bytes processed:', result.bytesProcessed);
// 104857600
// With progress tracking
const progress = (percent) => {
console.log(`Progress: ${percent.toFixed(1)}%`);
};
const result = await hashFile('large-file.bin', { onProgress: progress });
Node.js Streams (Recommended for Node.js)
import { createMD5Stream } from 'pure-md5';
import fs from 'fs';
const stream = createMD5Stream();
stream.on('md5', (result) => {
console.log('MD5:', result.digest);
console.log('Bytes:', result.bytesProcessed);
});
fs.createReadStream('large-file.bin').pipe(stream);
WHATWG Streams (Recommended for Browsers)
import { createMD5Stream } from 'pure-md5/stream/whatwg-stream';
const stream = createMD5Stream();
const readable = new ReadableStream({
start(controller) {
// Your data source
controller.enqueue('data');
controller.close();
}
});
const result = await readable.pipeThrough(stream).getReader().read();
console.log('MD5:', result.value.digest);
Under the Hood
The library uses streaming data processing:
class MD5Stream extends Transform {
constructor() {
super();
this.hash = createMD5Context();
this.bytesProcessed = 0;
}
_transform(chunk, encoding, callback) {
this.hash.update(chunk);
this.bytesProcessed += chunk.length;
this.emit('progress', { bytesProcessed: this.bytesProcessed });
callback();
}
_flush(callback) {
this.emit('md5', {
digest: this.hash.digest('hex'),
bytesProcessed: this.bytesProcessed
});
callback();
}
}
This allows hashing files of any size without memory overflow risk.
TypeScript First
In 2026, a library without types is second-class. pure-md5 is written in TypeScript 5.6+ with full type definitions.
// Full type coverage for all APIs
import { md5, hashFile, MD5Result } from 'pure-md5';
const hash: string = md5('hello');
const result: MD5Result = await hashFile('file.txt');
// result.digest: string
// result.bytesProcessed: number
No @types/md5, no any. Everything works out of the box.
Export Paths
The library provides multiple entry points for different use cases:
// Main entry (all exports)
import { md5, hashFile, createMD5Stream } from 'pure-md5';
// Tree-shakeable md5 only
import { md5 } from 'pure-md5/md5';
// Adapters
import { NodeCryptoBackend } from 'pure-md5/adapters/node';
import { WebCryptoBackend } from 'pure-md5/adapters/webcrypto';
import { PureJSBackend } from 'pure-md5/adapters/ie11';
// Detection
import { detectAdapter } from 'pure-md5/detect';
// Streaming
import { createMD5Stream } from 'pure-md5/stream';
import { createMD5Stream } from 'pure-md5/stream/whatwg-stream';
Benchmarks
Size and speed are critical metrics for frontend libraries.
Bundle Size (gzipped)
| Library | Size | Dependencies |
|---|---|---|
| pure-md5 (md5 only) | ~1.4KB | 0 |
| pure-md5 (full) | ~6KB | 0 |
| md5 (pvorb/node-md5) | ~3KB | 3 (charenc, crypt, is-buffer) |
| js-md4 | ~2KB | 0 |
| blueimp-md5 | ~2KB | 0 |
| crypto-js | ~4KB+ | 0 |
| spark-md5 | ~3KB | 0 |
Note: pure-md5 achieves smaller final sizes through tree-shaking, allowing you to import only what you use.
Speed (Operations/Second, Node.js)
| Library | ops/sec |
|---|---|
| Node.js crypto | ~1,200,000 |
| pure-md5 (node adapter) | ~1,150,000 |
| pure-md5 (pure JS) | ~450,000 |
| md5 (pvorb) | ~350,000 |
| crypto-js | ~380,000 |
| spark-md5 | ~420,000 |
In Node.js, pure-md5 nearly matches native crypto performance thanks to its adapter. In browsers, the pure JS implementation is still faster than competitors due to optimizations.
Comparison with md5 (pvorb/node-md5)
The md5 package (from pvorb/node-md5) is one of the most popular MD5 implementations (~2.5M weekly downloads). However, it has several limitations:
Key Differences
| Feature | pure-md5 | md5 (pvorb) |
|---|---|---|
| Compatibility | Node.js + Browser | Node.js only |
| TypeScript | ✅ Out of box | ❌ No types |
| Bundle Size | ~1.4KB¹ | ~3KB |
| Dependencies | 0 | 3 (charenc, crypt, is-buffer) |
| Tree-shaking | ✅ Supported | ❌ CommonJS only |
| Stream API | ✅ Built-in | ❌ None |
| Adapters | Auto-detection | One-size-fits-all |
| Performance | ~1.15M ops/sec | ~350K ops/sec |
| Browser streams | ✅ WHATWG streams | ❌ None |
¹ ~1.4KB with tree-shaking (md5 only). Full bundle: ~6KB.
Usage Examples
// md5 (pvorb) - Node.js only
import md5 from 'md5';
// ❌ Won't work in browser (uses require())
const hash = md5('hello');
// pure-md5 - universal solution
import { md5 } from 'pure-md5/md5'; // Tree-shakeable
// ✅ Works everywhere (Node.js + Browser)
const hash = md5('hello');
Why is md5 Slower?
The md5 package uses byte-by-byte string processing and auxiliary libraries for encoding:
// From md5 (pvorb) - inefficient approach
const str = new String(string);
const bytes = [];
for (let i = 0; i < str.length; i++) {
bytes.push(str.charCodeAt(i));
}
// ... then process byte array
pure-md5 uses a more efficient approach — working with 32-bit words and optimized loops.
Practical Usage Examples
1. Verify Downloaded File Integrity
import { verifyFile } from 'pure-md5';
const isVerified = await verifyFile(
'downloaded-file.zip',
'5d41402abc4b2a76b9719d911017c592' // Expected hash from server
);
if (!isVerified) {
throw new Error('File corrupted during download');
}
// Returns true or false
2. Content-Based Cache Key
import { md5 } from 'pure-md5/md5'; // Tree-shakeable import
function getCacheKey(content) {
return `cache:${md5(content)}`;
}
const key = getCacheKey(JSON.stringify(userData));
// "cache:5d41402abc4b2a76b9719d911017c592"
3. Universal Hashing (Node + Browser)
// Single file works everywhere
import { md5 } from 'pure-md5/md5';
export function generateId(data) {
if (typeof data === 'string') {
return md5(data);
}
// For binary data (ArrayBuffer/Uint8Array)
if (data instanceof Uint8Array || data instanceof ArrayBuffer) {
// Convert to string for md5
return md5(new TextDecoder().decode(data));
}
// For Buffer (Node.js)
return md5(data);
}
4. Streaming API for Large Files (Node.js)
import { createMD5Stream } from 'pure-md5';
import fs from 'fs';
const stream = createMD5Stream();
stream.on('md5', (result) => {
console.log('MD5:', result.digest);
console.log('Bytes:', result.bytesProcessed);
});
fs.createReadStream('large-file.bin').pipe(stream);
5. Streaming API for Large Files (Browser)
import { createMD5Stream } from 'pure-md5/stream/whatwg-stream';
document.querySelector('#fileInput').addEventListener('change', async (e) => {
const file = e.target.files[0];
if (!file) return;
console.log(`Hashing: ${file.name} (${file.size} bytes)`);
const stream = createMD5Stream();
const readable = file.stream();
const reader = readable.pipeThrough(stream).getReader();
const result = await reader.read();
console.log('✅ MD5:', result.value.digest);
console.log('📦 Size:', result.value.bytesProcessed, 'bytes');
});
6. Manual Adapter Selection
When you need explicit control over the backend:
import { NodeCryptoBackend, WebCryptoBackend, PureJSBackend } from 'pure-md5';
// Create backend instances
const nodeBackend = new NodeCryptoBackend();
const hash = await nodeBackend.hash('hello');
console.log(hash);
// Force pure JS (for testing or special environments)
const pureBackend = new PureJSBackend();
const hash2 = await pureBackend.hash('hello');
Security: Where to Use and Where Not To
| Scenario | Use MD5? | Alternative |
|---|---|---|
| File checksums | ✅ | — |
| Cache keys | ✅ | — |
| Internal object IDs | ✅ | — |
| User passwords | ❌ | bcrypt, argon2 |
| JWT tokens | ❌ | HS256, RS256 |
| Digital signatures | ❌ | SHA-256, EdDSA |
Rule: If an attacker could benefit from hash manipulation — don't use MD5.
Installation & Quick Start
npm install pure-md5
# or
yarn add pure-md5
# or
pnpm add pure-md5
Basic Usage (Tree-Shakeable)
import { md5 } from 'pure-md5/md5';
// String input
const hash = md5('hello');
// "5d41402abc4b2a76b9719d911017c592"
Full API Usage
import { md5, hashFile, createMD5Stream } from 'pure-md5';
// String input
const hash = md5('hello');
// "5d41402abc4b2a76b9719d911017c592"
// File hashing
const result = await hashFile('path/to/file.txt');
// Streaming
const stream = createMD5Stream();
fs.createReadStream('file.bin').pipe(stream);
CDN Usage
<!-- Minimal (md5 only) -->
<script src="https://unpkg.com/pure-md5@latest/md5.js"></script>
<script>
console.log(md5('hello')); // "5d41402abc4b2a76b9719d911017c592"
</script>
<!-- Full bundle -->
<script src="https://unpkg.com/pure-md5@latest/dist/index.js"></script>
<script>
console.log(md5('hello')); // "5d41402abc4b2a76b9719d911017c592"
</script>
Conclusion
pure-md5 doesn't try to revive MD5 for security purposes. It's a tool for specific tasks where MD5 is still used, but the implementation should be modern, typed, and universal.
If you need file checksums, cache keys, or legacy API compatibility — pure-md5 gives you the best developer experience without extra dependencies.
Links:

Top comments (0)