Skip to content

web: accelerate and measure wallet startup - #58

Merged
jamaljsr merged 2 commits into
mainfrom
perf/web-wallet-startup
Aug 6, 2026
Merged

web: accelerate and measure wallet startup#58
jamaljsr merged 2 commits into
mainfrom
perf/web-wallet-startup

Conversation

@Roasbeef

@Roasbeef Roasbeef commented Jul 24, 2026

Copy link
Copy Markdown
Member

Summary

In this PR, we make browser wallet startup measurable and move the
precompressed WASM artifact onto the browser's native streaming compilation
path. When a host serves wavewalletdk.wasm.gz as application/wasm with
Content-Encoding: gzip, the browser can decompress and compile the module in
one pipeline. The existing buffered gzip and raw WASM paths remain as
fallbacks.

We also add opt-in structured performance samples for runtime readiness, WASM
loading, wallet create and unlock RPCs, post-RPC state adoption, sync polling,
and passkey ceremonies. The web demo uses those samples in a repeatable
Playwright benchmark with checked-in p95 budgets.

Technical Notes

Instrumentation stays disabled unless the caller supplies onPerformance.
Timing calls and event allocations are skipped in that case. A diagnostic
callback is isolated from wallet behavior, so an exception from the reporter
cannot break an operation.

The benchmark creates a fresh browser context and wallet for every sample. It
also fails if getInfo adoption needs more than one attempt. The latest
five-run result using lightninglabs/wavelength#1048 reported:

  • Cold runtime ready p95: 276 ms.
  • WASM load and instantiate p95: 138 ms.
  • Create usable p95: 2,319 ms.
  • Wallet create total p95: 1,886 ms.
  • Unlock usable p95: 806 ms.
  • Wallet unlock total p95: 669 ms.
  • Budget violations: 0.

To separate the two contributions, the SDK and HTTP/WASM changes were measured
before applying the public-KDF optimization:

  • WASM load and instantiate p95 fell from 186 ms to 134 ms, a 52 ms or 28%
    improvement.
  • Runtime clientReady p95 fell from 307 ms to 260 ms, a 47 ms or 15%
    improvement.
  • Wallet create and unlock remained approximately 2.73 seconds and 1.21
    seconds. There was no meaningful operation-level improvement from the SDK
    changes alone.

The larger operation gains come from the dependent public-KDF change:
approximately 0.9 seconds from create and 0.55 seconds from unlock. The private
KDF remains unchanged.

Steps to Test

  1. Run the package tests:

    pnpm --filter @lightninglabs/wavelength-core test
    pnpm --filter @lightninglabs/wavelength-web test
  2. Run workspace checks and builds:

    pnpm typecheck
    pnpm build
    pnpm --filter web-wallet-demo run build
  3. Stage a local Wavelength runtime and run the browser budget:

    WAVELENGTH_DIR=/path/to/wavelength \
      pnpm --filter web-wallet-demo run wasm:local
    pnpm perf:web

Related Issues & Pull Requests

Closes #53

Depends on:

@jamaljsr jamaljsr left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I measured the branch on a throttled link before reading the diff, so the numbers below are mine rather than a re-read of the description.

What I tested:

  • Staged the v0.1.0 runtime asset set, built the demo, and measured cold load then reload in the same browser context at 50 Mbps using the instrumentation this PR adds. Cold wasmTotal was 3232 ms with wasmCompileInstantiate at 3230 ms, and the sample carried {"path":"gzip","streaming":true,"decompression":"http"}, so the Content-Encoding: gzip streaming route works end to end.
  • The reload paid the same 3253 ms. Repeat-fetching the module in the page kept transferSize at 19,929,433 bytes on all three loads while a ~1 MiB control asset served with identical headers dropped to 0 after the first, so the browser really is declining to store it.
  • pnpm build, pnpm typecheck (all 8 projects) and pnpm --filter @lightninglabs/wavelength-web test are clean. I didn't run the Playwright smoke suite.

One finding doesn't fit inline, because the file isn't part of this diff:

The reference doc's compressed-loading description is now false, and the new API is undocumented. apps/docs/src/content/docs/reference/wavelength-web.mdx:362 says the client "prefers the gzip-compressed wavewalletdk.wasm.gz binary, inflating it through a DecompressionStream and instantiating the resulting bytes." Under the configuration this PR's READMEs now tell hosts to adopt, it does neither. These pages are hand-maintained: scripts/gen-api-docs.mts only writes apps/docs/src/data/api/wallet.json, so nothing regenerates them.

Newly public surface is also missing there: onPerformance on WebClientOptions (wavelength-web.mdx:37) and on WalletEngineOptions (wavelength-core.mdx:419), the new optional parameter on registerPasskeyWallet and assertPasskeyPrf, and createWebPasskeyCeremony entirely. Grepping onPerformance across both reference pages returns nothing. api-links.ts needs the new export too.

Worth noting that #59 adds the Content-Type and Content-Encoding guidance to hosting-runtime-assets.mdx, so that gap closes in the stack. The reference pages are untouched by #59 and stay stale through both.

Comment thread packages/web/src/runtime.ts Outdated
Comment thread packages/web/src/runtime.ts Outdated
Comment thread packages/core/src/engine/engine.ts
Comment thread apps/web-wallet-demo/src/screens/onboarding/UnlockScreen.tsx
@jamaljsr
jamaljsr marked this pull request as ready for review August 5, 2026 18:51
@jamaljsr
jamaljsr force-pushed the perf/web-wallet-startup branch from b466545 to 84f4313 Compare August 5, 2026 19:08
Comment thread packages/web/src/runtime.ts Outdated
@jamaljsr
jamaljsr force-pushed the perf/web-wallet-startup branch from 84f4313 to 164ff12 Compare August 5, 2026 20:34
Comment thread packages/web/src/runtime.ts Outdated
In this commit, we add opt-in structured performance events across runtime startup, WebAssembly loading, wallet RPCs, state adoption, sync polling, and passkey ceremonies. The reporter is isolated from wallet behavior, so a failing diagnostic callback cannot disrupt operations.

We also stream HTTP-decompressed gzip content directly into WebAssembly compilation when the host advertises the correct headers. Existing buffered gzip and raw WASM paths remain as fallbacks.
In this commit, we add repeatable Playwright samples for cold load, create, reload, and unlock, with checked-in p95 budgets and a single-attempt state-adoption constraint. The demo opts into structured samples only when the harness installs its callback.

We also serve precompressed WASM with the headers required by the streaming path and document how to reproduce measurements with pinned or local runtime assets.
@jamaljsr
jamaljsr force-pushed the perf/web-wallet-startup branch from 164ff12 to 1d1110e Compare August 6, 2026 04:50
@jamaljsr

jamaljsr commented Aug 6, 2026

Copy link
Copy Markdown
Member

The wasm loader was rewritten rather than patched

Heads up that this PR's diff has changed shape since the last review. The header handling turned into a permutation table that kept producing findings: six rounds of review, each one patching one more combination of Content-Type and Content-Encoding, and each patch adding another flag the recovery path had to remember. The last round found the same defect the round before had reported fixed.

The root cause was that the loader tried to predict what the body was from headers and then recover when the prediction was wrong, which meant the recovery had to know which prediction it made. That is what bred the flags.

It now reads the first four bytes and branches on the magic number instead (1f 8b for gzip, 00 61 73 6d for wasm), and hands instantiateStreaming a response it labels application/wasm itself, since the host's content type is unreliable either way. Headers no longer drive control flow at all. httpDecoded, alreadyDecoded, the two-way recovery and the MIME-case handling are all gone, because the cases they existed for cannot arise.

Three practical consequences:

  • Configurations that used to fall back to the multi-megabyte uncompressed asset now load the compressed one. Both of the ones reported on this PR are covered, and the tests are a table of six host setups that all take the same path.
  • The compressed path stays streaming. It previously inflated into an ArrayBuffer and did a non-streaming compile; inflated bytes now feed compilation as they arrive. Cold load measures about 20% faster than before the rewrite.
  • No header setup is needed for compression any more, so the hosting guide's advice on it is gone. Cross-origin hosts still need Cross-Origin-Resource-Policy, which is unchanged.

The smoke test now serves the compressed asset as raw gzip so the SDK's own inflate path gets real-browser coverage; the benchmark keeps the transport-inflates configuration so its recorded numbers stay comparable.

Worth re-reviewing packages/web/src/runtime.ts and the worker mirror from scratch rather than reading the incremental diff.

@claude

claude Bot commented Aug 6, 2026

Copy link
Copy Markdown

Code review

No issues found. Checked for bugs and CLAUDE.md compliance.

@jamaljsr
jamaljsr merged commit 2a06c8d into main Aug 6, 2026
12 checks passed
@jamaljsr
jamaljsr deleted the perf/web-wallet-startup branch August 6, 2026 14:26
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.

web/core: speed up wasm load and wallet unlock, add perf instrumentation + benchmarking harness

2 participants