Measured against v0.1.2-alpha.4 (4abcf33), renderer driven by the harness (Chromium + desktop/e2e/tauri-fixture.js) with window.__TAURI_INTERNALS__.invoke wrapped to log every command and its start/end time, and performance.now() marks around App.vue's onMounted.
desktop/src/App.vue:63-74 boots the app one await at a time:
await vault.checkStatus();
await hosts.load();
await keys.load();
await identities.load();
await workspaces.load();
await update.registerListeners();
await update.check();
Each line waits for the previous one to come back from Rust before it asks its own question, and three of them fan out into further serial calls of their own (stores/vault.ts:85-95, stores/hosts.ts:30-33, registerListeners at stores/update.ts:66-92). The last one is an HTTPS request to the GitHub API.
What the renderer actually does at launch
The instrumented log for one boot, offsets in ms from the start of onMounted (measured, mocked IPC so every response is instant):
list_hosts @-0.3 -> 1.1 HostList.onMounted
list_identities @-0.1 -> 1.1 HostList.onMounted
vault_is_initialized @ 0.0 -> 1.2 App.onMounted starts here
list_groups @ 1.2 -> 3.5
is_vault_unlocked @ 1.2 -> 3.5
biometric_available @ 4.4 -> 4.4
biometric_passphrase_stored @ 4.4 -> 4.4
list_hosts @ 4.4 -> 4.4
list_groups @ 4.4 -> 5.2
list_keys @ 5.3 -> 5.3
list_identities @ 5.4 -> 5.8
list_workspaces @ 6.5 -> 6.5
plugin:event|listen @ 6.5 -> 6.5
plugin:event|listen @ 6.5 -> 6.5
check_for_updates @ 6.6 -> 6.6 HTTPS to api.github.com
15 invokes, 12 of them strictly one-after-another, ending on the network call. Three points fall out of that log:
Children mount before the parent. HostList is the default view, and Vue runs a child's onMounted before its parent's, so HostList.vue:74-75 has already asked for hosts and identities before App.vue starts. useIdentitiesStore de-dupes with an in-flight promise (stores/identities.ts:32-33), the others do not, so list_hosts and list_groups are each issued twice — and because the calls are serialised, the second pair arrives after the first has already settled, so even the identities de-dupe does not catch it there (list_identities also goes out twice).
Nothing in the chain needs to be in a chain. list_hosts, list_groups, list_identities, list_workspaces all read state.store and return disjoint slices of it (commands.rs:22,61,91,254). list_keys reads plaintext key metadata out of the vault file and does not require an unlocked vault (commands.rs:146 → core/src/vault.rs:54). vault_is_initialized and is_vault_unlocked are two independent infallible reads (commands.rs:130,136). No store reads another store's result during boot. The only genuine ordering constraint in the whole path is inside refreshBiometricState: the credential probe is deliberately gated on initialized and on a generation check, so that a probe superseded by an enrolment/deletion never starts — stores/vault.test.ts and stores/vault.biometric-concurrency.test.ts both pin that behaviour and it should stay serial.
The update check runs twice in release builds, and the Rust one is invisible. desktop/src-tauri/src/main.rs:39-46 already spawns a silent check two seconds after setup for not(debug_assertions), and App.vue:71 runs a second one on every launch. The Rust check emits an update-available event — but api.onUpdateAvailable (desktop/src/api.ts:132) is never called from anywhere in desktop/src, so the backend check currently updates no UI at all, while its frontend twin is the thing holding the mount open on a network round-trip.
What it costs
The fixture answers instantly, so a raw harness run measures only renderer overhead: 6.8 ms median to complete onMounted. That is the point — nearly all of the real cost is round-trip latency the mock does not have. Injecting a per-call delay to model it (median of 15 runs, wall clock measured, latency modelled):
| modelled per-call latency |
onMounted completes |
| 0 ms (mock, renderer overhead only) |
6.8 ms |
| 2 ms every call |
89.3 ms |
2 ms local + 300 ms for check_for_updates |
386.3 ms |
Two thirds to seven eighths of that is the app waiting on a queue it created itself.
Fixing it
Promise.all over the six independent boot loads; Promise.all inside vault.checkStatus() and hosts.load() for the pairs that are genuinely independent; the same in-flight-promise de-dupe identities already has added to hosts; and update.check() dispatched without await, letting the existing shouldNotify watcher (App.vue:43-45) open the modal when the answer arrives. refreshBiometricState stays serial.
Same harness, same method, median of 15:
|
invokes at boot |
serial round-trips |
2 ms uniform |
2 ms + 300 ms update check |
| before |
15 |
12 |
89.3 ms |
386.3 ms |
| after |
12 |
3 |
49.4 ms |
59.5 ms |
|
−3 |
−9 |
−45% |
−85% |
The three duplicate reads disappear because the parallel dispatch puts App's calls in flight while HostList's are still open, so the de-dupe catches them.
The honest counterpart: the wall-clock figures come from a Chromium harness with mocked IPC and a setTimeout-based latency injector, whose clamping adds a fixed cost to both arms — the 49.4 ms "after" figure is not 3 × 2 ms plus overhead, it is dominated by the injector. Treat the deltas as sound and the absolute numbers as an upper bound; the result that does not depend on any of that is the round-trip count, 12 serial down to 3, with the HTTPS request no longer on the path at all.
PR to follow.
Measured against
v0.1.2-alpha.4(4abcf33), renderer driven by the harness (Chromium +desktop/e2e/tauri-fixture.js) withwindow.__TAURI_INTERNALS__.invokewrapped to log every command and its start/end time, andperformance.now()marks aroundApp.vue'sonMounted.desktop/src/App.vue:63-74boots the app oneawaitat a time:Each line waits for the previous one to come back from Rust before it asks its own question, and three of them fan out into further serial calls of their own (
stores/vault.ts:85-95,stores/hosts.ts:30-33,registerListenersatstores/update.ts:66-92). The last one is an HTTPS request to the GitHub API.What the renderer actually does at launch
The instrumented log for one boot, offsets in ms from the start of
onMounted(measured, mocked IPC so every response is instant):15 invokes, 12 of them strictly one-after-another, ending on the network call. Three points fall out of that log:
Children mount before the parent.
HostListis the default view, and Vue runs a child'sonMountedbefore its parent's, soHostList.vue:74-75has already asked for hosts and identities beforeApp.vuestarts.useIdentitiesStorede-dupes with an in-flight promise (stores/identities.ts:32-33), the others do not, solist_hostsandlist_groupsare each issued twice — and because the calls are serialised, the second pair arrives after the first has already settled, so even the identities de-dupe does not catch it there (list_identitiesalso goes out twice).Nothing in the chain needs to be in a chain.
list_hosts,list_groups,list_identities,list_workspacesall readstate.storeand return disjoint slices of it (commands.rs:22,61,91,254).list_keysreads plaintext key metadata out of the vault file and does not require an unlocked vault (commands.rs:146→core/src/vault.rs:54).vault_is_initializedandis_vault_unlockedare two independent infallible reads (commands.rs:130,136). No store reads another store's result during boot. The only genuine ordering constraint in the whole path is insiderefreshBiometricState: the credential probe is deliberately gated oninitializedand on a generation check, so that a probe superseded by an enrolment/deletion never starts —stores/vault.test.tsandstores/vault.biometric-concurrency.test.tsboth pin that behaviour and it should stay serial.The update check runs twice in release builds, and the Rust one is invisible.
desktop/src-tauri/src/main.rs:39-46already spawns a silent check two seconds after setup fornot(debug_assertions), andApp.vue:71runs a second one on every launch. The Rust check emits anupdate-availableevent — butapi.onUpdateAvailable(desktop/src/api.ts:132) is never called from anywhere indesktop/src, so the backend check currently updates no UI at all, while its frontend twin is the thing holding the mount open on a network round-trip.What it costs
The fixture answers instantly, so a raw harness run measures only renderer overhead: 6.8 ms median to complete
onMounted. That is the point — nearly all of the real cost is round-trip latency the mock does not have. Injecting a per-call delay to model it (median of 15 runs, wall clock measured, latency modelled):onMountedcompletescheck_for_updatesTwo thirds to seven eighths of that is the app waiting on a queue it created itself.
Fixing it
Promise.allover the six independent boot loads;Promise.allinsidevault.checkStatus()andhosts.load()for the pairs that are genuinely independent; the same in-flight-promise de-dupeidentitiesalready has added tohosts; andupdate.check()dispatched withoutawait, letting the existingshouldNotifywatcher (App.vue:43-45) open the modal when the answer arrives.refreshBiometricStatestays serial.Same harness, same method, median of 15:
The three duplicate reads disappear because the parallel dispatch puts
App's calls in flight whileHostList's are still open, so the de-dupe catches them.The honest counterpart: the wall-clock figures come from a Chromium harness with mocked IPC and a
setTimeout-based latency injector, whose clamping adds a fixed cost to both arms — the 49.4 ms "after" figure is not 3 × 2 ms plus overhead, it is dominated by the injector. Treat the deltas as sound and the absolute numbers as an upper bound; the result that does not depend on any of that is the round-trip count, 12 serial down to 3, with the HTTPS request no longer on the path at all.PR to follow.