zxcvbn is the most widely used password strength estimator with 1M npm downloads a week. It's also 389KB gzipped and hasn't shipped a commit since 2017. Most sign-up forms are hauling that around just to block password123.
Poor password UX is a real conversion problem. A strength meter that adds 389KB to your bundle delays page load — on mobile, measurably so. Users who hit a slow registration page don't wait. They leave. The irony is that most of that weight goes toward catching passwords nobody is actually using to register on your site.
So I built passcore - 3.0KB gzipped and 98.4% detection rate on real breach data - same as zxcvbn, benchmarked against a deduped list of passwords pulled live from RockYou, Adobe, HIBP, and other major leak lists.
zxcvbn takes ~9.7ms to load — it's parsing 389KB of dictionary into memory on every cold start. passcore loads in ~0.2ms. It evaluates a password in ~2,600 nanoseconds. For a registration form, it's effectively invisible — no jank, no layout shift, no contribution to your Core Web Vitals score. The strength meter shows up before the user finishes typing their first character.
How it works:
passcore runs five detection layers on every password:
- Dictionary - All entries sourced directly from breach data, not a generic word list
- Keyboard patterns -
qwerty,asdf,1234, numpad walks - Repeats -
aaaa,ababab - Sequences -
abcdef,123456 - L33t speak - decodes
p@ssw0rd→password,m0nk3y→monkey, then dictionary lookup
The dictionary is small by design. Every entry was chosen because it appears in real breach data - not because it's a common English word. Password1! is caught not by a 40k word list but by stripping the suffix and checking if the core word is in the breach list. It is.
The scoring model:
passcore returns a score from 0 to 4 - same scale as zxcvbn.
The detection layers run first. A dictionary match, keyboard pattern, repeat, sequence, or l33t substitution scores 0 or 1 immediately - no further calculation. If none of those fire, scoring falls through to length and character variety: uppercase, lowercase, digits, symbols. A password that clears all five layers but is only 6 characters long still scores low.
There's also a length floor, aligned with NIST SP 800-63B: passwords 20+ characters score at least 3, passwords 30+ characters score 4, regardless of character variety. A passphrase like correct-horse-battery-staple is vastly harder to crack than P@ss1 - the scoring reflects that.
The research:
Getting to 98.4% detection required more than a dictionary lookup. A few problems that came up during development:
Word+affix patterns: Password1!, Admin123, Welcome1 - extremely common in breach data, none of them are in any dictionary as-is. The fix was a matchCommonRoot layer: strip leading and trailing non-alpha characters, check if what's left is a breach word. It is, every time, for this class of password.
L33t speak with separators: N0=Acc3ss decodes to no=access. A naive l33t decoder finds no dictionary match and passes it. The fix was to split the decoded string on non-alpha characters and check each segment independently. access is in the breach list. Caught.
Missing critical roots: Running against real breach lists exposed that admin, test, user, login, pass weren't in the dictionary - meaning Admin123, test1234, user2024 all slipped through. Added those five. Caught.
Switching looks like this:
// before
import zxcvbn from 'zxcvbn';
const { score } = zxcvbn(password);
// after
import { passcore } from 'passcorelib';
const { score } = passcore(password);
One caveat: result.feedback.warning becomes result.warning, making it one level flatter.
| zxcvbn | zxcvbn-ts | passcore | |
|---|---|---|---|
| Bundle (gzipped) | 389 KB | 855 KB | 3.0 KB |
| Speed | 77,578 ns/op | 839,991 ns/op | 2,622 ns/op |
| Detection rate | 98.4% | 98.4% | 98.4% |
| Maintained | No | Yes | Yes |
The tradeoff:
The tradeoff is dictionary size: 329 entries vs 40k+. But the passwords responsible for most credential stuffing aren't obscure literary references - they're Password1!, baseball123, keyboard walks, and l33t variants of the top breach list. passcore catches those.
So that's the bet passcore makes: that 329 targeted entries catch more of what actually matters than 40,000 words that cover everything, including passwords no one uses and/or no attacker is trying. The benchmark agrees — 98.4% detection rate across 370 real breach passwords, same as zxcvbn, at 130x less weight. For the 1% that need exhaustive coverage, use zxcvbn.
TL;DR — zxcvbn is 389KB and abandoned. passcore is 3KB, same detection rate, actively maintained. If bundle size matters to you, it's a near drop-in swap.
Top comments (0)