Skip to content

perf: cut the popup's serialized storage round trips on cold start - #1583

Open
charllesgalves wants to merge 1 commit into
Authenticator-Extension:devfrom
charllesgalves:perf/popup-cold-start
Open

perf: cut the popup's serialized storage round trips on cold start#1583
charllesgalves wants to merge 1 commit into
Authenticator-Extension:devfrom
charllesgalves:perf/popup-cold-start

Conversation

@charllesgalves

Copy link
Copy Markdown

perf: cut the popup's serialized storage round trips on cold start

Problem

Opening the popup runs roughly fifty chrome.storage round trips, one after
another
, before Vue can mount. This is the bulk of the perceived "slow to
open" time, and it happens even with a couple of entries and no encryption
configured — so it isn't decryption or data volume.

Two sources account for nearly all of them:

1. BrowserStorage.getStorageLocation() runs at the top of every storage
operation, and each call did:

await UserSettings.updateItems();                       // 1-2 round trips
const managedLocation = await ManagedStorage.get(...);  // 1 round trip

Nothing was cached, so the same values were re-fetched over and over. When
settings live in sync storage, updateItems() costs two reads (local + sync)
rather than one.

2. Everything was awaited in sequence. popup.ts awaited each async Vuex
module in turn, and Advisor awaited its five insight validations in turn —
with each validation calling UserSettings.updateItems() again from scratch,
and two of them additionally calling hasEncryptionKey().

Approximate breakdown of the round trips before mount:

Step Round trips
initial UserSettings.updateItems() 2
Accounts.getModule() 17
Advisor.getModule() 18
Backup.getModule() 2
Menu.getModule() (8x ManagedStorage.get) 10

Changes

  • Cache user settings with invalidation on chrome.storage.onChanged, so
    writes from other extension contexts (background, options page) are still
    observed. commitItems() invalidates explicitly so it never serves a stale
    read back to its own caller.
  • Cache the managed policy. It is a single object, but callers read it one
    key at a time — the menu store alone reads eight. It is now fetched once.
  • Coalesce concurrent readers onto a single in-flight read, in both of the
    above and in getStorageLocation(), so parallel callers don't each re-run
    the auto-detection branch (which can also commit settings).
  • Build the store modules and the i18n catalog concurrently in popup.ts.
  • Run the advisor validations concurrently, preserving output order.
  • Add a missing await in Backup.getModule(), which read
    UserSettings.items before the read it depends on had resolved.

ManagedStorage keeps its original contract: no caller waits more than ~10ms
on managed storage, and a policy that arrives after the timeout still lands in
the cache for later lookups.

Verification

  • tsc --noEmit clean; npm run chrome builds successfully.
  • Measured with a chrome.storage mock that counts get calls:
before after
15x UserSettings.updateItems() 30 gets 2
8x ManagedStorage.get() 8 gets 1

Cache invalidation and concurrent reads were checked in the same harness: a
write followed by a read returns the fresh value, and four concurrent readers
all resolve correctly.

  • The Puppeteer e2e suite could not run in my environment: Chrome 137+ removed
    the --load-extension command-line switch the runner depends on, so
    test-runner.js fails with ERR_BLOCKED_BY_CLIENT on dev as well as on
    this branch
    — the failure is pre-existing and unrelated to these changes.

🤖 Generated with Claude Code

Opening the popup ran roughly fifty chrome.storage round trips one after
another before Vue could mount, which is what makes it feel slow to open
even with a couple of entries and no encryption configured.

Almost all of them come from two places:

- BrowserStorage.getStorageLocation() runs at the top of every storage
  operation, and each call re-read all user settings (one or two round
  trips, depending on whether settings live in sync storage) plus the
  managed policy. Nothing was cached, so the same values were fetched
  over and over.
- The popup awaited each async Vuex module in turn, and the advisor
  store awaited its five insight validations in turn -- with each
  validation re-reading settings from scratch.

Cache the settings and the managed policy, invalidating on
chrome.storage.onChanged so other extension contexts are still observed,
and coalesce concurrent readers onto a single in-flight read. Then build
the store modules and the i18n catalog concurrently, and run the advisor
validations concurrently.

Managed storage keeps its original contract: no caller waits more than
~10ms on it, and a policy that arrives after that still lands in the
cache for later lookups.

Measured against a chrome.storage mock that counts calls:

  15x UserSettings.updateItems():  30 gets -> 2
  8x  ManagedStorage.get():         8 gets -> 1

Also adds a missing await in the backup store, which read
UserSettings.items before the read it depends on had resolved.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant