Password meter: zxcvbn's 870 kB of dictionaries load with the page, before anybody touches the password field #1150

Closed
opened 2026-10-31 03:04:22 +00:00 by tiagoagueda · 0 comments
Owner

What exists today

templates/account/partials/password_meter.html:13-15 emits three deferred scripts, included by account/base_entrance.html:16 and account/base_manage_password.html:11 whenever the form has password1 (sign-up, reset from key, change, set). Sizes measured with the venv's gzip and brotli:

file bytes gzip -9 Brotli
static/js/vendor/zxcvbn/core.js 107,605 24,480 21,006
language-common.js 429,124 226,017 203,129
language-en.js 1,201,765 618,575 549,313
total 1,738,494 869,072 773,448

All three are fetched and compiled as the page loads, although app.js uses them only inside the input handler (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-en is 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 first input event 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

  • The template keeps the <template id="password-meter"> and moves the three script URLs into data attributes on it (for example data-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 first focusin (or input, for a field filled by a password manager without focus) of input[data-password-meter]: insert the meter display with its empty role="status" region at once, then append the three <script> elements in order (async = false keeps 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.
  • No library is needed; this is the browser's own script loading.
  • tests/test_password_meter.py's SCRIPT in html checks 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 by tests/e2e/test_password_meter.py::test_the_meter_speaks_as_you_type (word in the status region, advice outside it) and tests/test_password_meter.py (meter, template, script reference and role="status" on sign-up, change and set; none on sign-in)

Decisions

  • Load on focus or on first input? Recommended: on focus, with first input as a fallback; focus gives the files the time it takes to start typing, and the empty region is in place before anything is written into it.
  • Keep the English pack on non-English pages? Recommended: yes; it is the dictionary that catches English-word passwords, not only the advice.

Found by the efficiency and reliability audit of 2026-10-30 (finding A4; index postulo/postulo#1108). Checked against main at daee30b15 (2026-10-30).

## What exists today `templates/account/partials/password_meter.html:13-15` emits three deferred scripts, included by `account/base_entrance.html:16` and `account/base_manage_password.html:11` whenever the form has `password1` (sign-up, reset from key, change, set). Sizes measured with the venv's gzip and brotli: | file | bytes | gzip -9 | Brotli | | --- | --- | --- | --- | | `static/js/vendor/zxcvbn/core.js` | 107,605 | 24,480 | 21,006 | | `language-common.js` | 429,124 | 226,017 | 203,129 | | `language-en.js` | 1,201,765 | 618,575 | 549,313 | | total | 1,738,494 | 869,072 | 773,448 | All three are fetched and compiled as the page loads, although `app.js` uses them only inside the `input` handler (`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-en` is 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 first `input` event 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 - The template keeps the `<template id="password-meter">` and moves the three script URLs into data attributes on it (for example `data-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 first `focusin` (or `input`, for a field filled by a password manager without focus) of `input[data-password-meter]`: insert the meter display with its empty `role="status"` region at once, then append the three `<script>` elements in order (`async = false` keeps 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. - No library is needed; this is the browser's own script loading. - `tests/test_password_meter.py`'s `SCRIPT in html` checks 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 by `tests/e2e/test_password_meter.py::test_the_meter_speaks_as_you_type` (word in the status region, advice outside it) and `tests/test_password_meter.py` (meter, template, script reference and `role="status"` on sign-up, change and set; none on sign-in) ## Decisions - **Load on focus or on first input?** Recommended: on focus, with first input as a fallback; focus gives the files the time it takes to start typing, and the empty region is in place before anything is written into it. - **Keep the English pack on non-English pages?** Recommended: yes; it is the dictionary that catches English-word passwords, not only the advice. --- Found by the efficiency and reliability audit of 2026-10-30 (finding A4; index postulo/postulo#1108). Checked against `main` at `daee30b15` (2026-10-30).
tiagoagueda added this to the 0.6.0 milestone 2026-10-31 03:04:22 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Postulo/postulo#1150
No description provided.