Password meter: zxcvbn's 870 kB of dictionaries load with the page, before anybody touches the password field #1150
Labels
No labels
accessibility
authentication
breaking change
bug
documentation
enhancement
interface
internationalisation
observability
performance
security
tier
1
tier
2
tier
3
tier/4
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
Postulo/postulo#1150
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What exists today
templates/account/partials/password_meter.html:13-15emits three deferred scripts, included byaccount/base_entrance.html:16andaccount/base_manage_password.html:11whenever the form haspassword1(sign-up, reset from key, change, set). Sizes measured with the venv's gzip and brotli:static/js/vendor/zxcvbn/core.jslanguage-common.jslanguage-en.jsAll three are fetched and compiled as the page loads, although
app.jsuses them only inside theinputhandler (static/js/app.js:2628-2679):readyZxcvbn()(2559-2579) builds the checker on the first keystroke, and returns null, drawing nothing, if the library is not there.The scout's "English pack always" is not waste:
language-enis both the advice (shown only on English pages, 2664-2678) and the English dictionary that scores passwords made of English words, which people type whatever language the page is in. Dropping it on other pages would weaken the estimate. That part of the claim is withdrawn.One accessibility detail found on the way:
meterFor()(2581-2597) inserts the display, live region included, on the firstinputevent and writes the word into it in the same task. A region that is created and filled at once is not reliably announced by every screen reader; the fix below inserts it earlier, which helps.What would change
<template id="password-meter">and moves the three script URLs into data attributes on it (for exampledata-scripts="{% static ... %} {% static ... %} {% static ... %}"), so the pages still name the vendored files from Postulo's origin and the CSP (script-src 'self') is unchanged.app.js, on the firstfocusin(orinput, for a field filled by a password manager without focus) ofinput[data-password-meter]: insert the meter display with its emptyrole="status"region at once, then append the three<script>elements in order (async = falsekeeps the order). When the last one loads, run the estimate for the field's current value, so a password typed or pasted while the files were arriving is scored without another keystroke.tests/test_password_meter.py'sSCRIPT in htmlchecks still hold (the URL is in the page); one unit test should assert the scripts are no longer<script>elements on the page, and the e2e test gains a check that the status region exists after focus and before typing.What it saves or protects
large on the pages that carry it: 1.74 MB of script (869 kB gzip, 773 kB Brotli) no longer downloaded and parsed by whoever opens sign-up, password change, set or reset and leaves without typing a password; parsed later, off the first paint, by whoever does
Accessibility
must be checked: the meter's
role="status"region must be in the page, empty, before its first word is written, and the word must still be announced once per change; guarded bytests/e2e/test_password_meter.py::test_the_meter_speaks_as_you_type(word in the status region, advice outside it) andtests/test_password_meter.py(meter, template, script reference androle="status"on sign-up, change and set; none on sign-in)Decisions
Found by the efficiency and reliability audit of 2026-10-30 (finding A4; index postulo/postulo#1108). Checked against
mainatdaee30b15(2026-10-30).