perf(routing): prefetch route data in loaders and remove the 300ms delay on IC reads - #441
Open
yhabib wants to merge 3 commits into
Open
perf(routing): prefetch route data in loaders and remove the 300ms delay on IC reads#441yhabib wants to merge 3 commits into
yhabib wants to merge 3 commits into
Conversation
…read floor Navigating to a route serialised two waits that had no reason to be sequential: the route chunk had to download and execute before the component mounted, and only then did its `useQuery` calls reach the network. Measured on nns.icp.net, /neurons spent 1092ms fetching a 20KB chunk plus 481ms executing it before the first `listNeurons` request left the browser at t=1581ms. Route loaders now warm the cache while the chunk is still in flight: - Agents move out of `AgentPoolProvider`'s effect into module singletons, so a loader can reach the network before React mounts. The provider consumes the same instances, so there is still one agent per identity. - Query key and fetcher for each query live together in `common/queries`, shared by the `use*` hooks and the loaders. A drifted key would turn a prefetch into a silently wasted call, so this makes drift structural rather than a convention. - Dashboard, neurons and voting get loaders that fire both legs of their certified queries without awaiting them. Navigation is never blocked; the mounting component attaches to the in-flight promise. Separately, every IC read was floored at 300ms by `MIN_ASYNC_DELAY` to keep skeletons from flashing. Warm query calls return in well under 100ms, so this was pure added latency on the fast leg and on refetches that show stale data rather than a skeleton. The floor is gone; the flash it guarded against is now handled where it belongs, by holding the indicator back 120ms instead of holding the data back 300ms.
|
✅ No security or compliance issues detected. Reviewed everything up to 154eacb. Security Overview
Detected Code Changes
|
📊 Build Bundle StatsThe latest build generated the following assets: |
Contributor
There was a problem hiding this comment.
Pull request overview
This PR improves navigation latency by starting IC reads during route loading (before React mounts) and by removing the artificial 300ms minimum delay on query reads, replacing it with a short delay before showing loading indicators.
Changes:
- Added per-route loaders (dashboard/neurons/voting) that prefetch both certified/non-certified query legs into the shared React Query cache without blocking navigation.
- Centralized query keys + fetchers into
common/queries/*(including certified key helpers and prefetch helpers) to prevent loader/hook cache-key drift. - Removed
MIN_ASYNC_DELAYand introduced a delayed loading-indicator flag to avoid skeleton flicker without delaying data.
Reviewed changes
Copilot reviewed 23 out of 23 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| src/governance-app-frontend/src/routes/_auth/voting/index.tsx | Adds a voting route loader that prefetches the relevant proposals filter before mount. |
| src/governance-app-frontend/src/routes/_auth/neurons/index.tsx | Adds a neurons route loader to warm neurons/balance queries. |
| src/governance-app-frontend/src/routes/_auth/dashboard/index.tsx | Adds a dashboard route loader to warm metrics + neurons/balance queries. |
| src/governance-app-frontend/src/routes/__root.tsx | Switches to a typed router context so loaders can access a shared QueryClient. |
| src/governance-app-frontend/src/common/utils/initializer.ts | Provides queryClient via router context when creating the router. |
| src/governance-app-frontend/src/common/queries/routeLoaders.ts | New per-route prefetch orchestration using shared query descriptors. |
| src/governance-app-frontend/src/common/queries/routeLoaders.test.tsx | New test to ensure loader-prefetched cache keys match the consuming hook keys. |
| src/governance-app-frontend/src/common/queries/prefetch.ts | New helpers to prefetch both legs of certified (infinite) queries. |
| src/governance-app-frontend/src/common/queries/icpLedger.ts | New ICP ledger query descriptor (key + fetchers) shared by hooks/loaders. |
| src/governance-app-frontend/src/common/queries/governance.ts | New governance query descriptors and request builders shared by hooks/loaders. |
| src/governance-app-frontend/src/common/queries/certified.ts | New centralized certified/non-certified key derivation and response wrapping. |
| src/governance-app-frontend/src/common/hooks/useQueryThenUpdateCall.ts | Removes minimum delay and standardizes keys via certified.ts helpers. |
| src/governance-app-frontend/src/common/hooks/useInfiniteQueryThenUpdateCall.ts | Removes minimum delay and standardizes keys via certified.ts helpers. |
| src/governance-app-frontend/src/common/hooks/useDelayedFlag.ts | New hook to delay the loading indicator rather than delaying data fetches. |
| src/governance-app-frontend/src/common/hooks/icpLedger/useIcpLedgerAccountBalance.ts | Switches to shared ICP ledger query descriptor (key + fetcher). |
| src/governance-app-frontend/src/common/hooks/governance/useGovernanceProposals.ts | Switches to shared proposals query descriptor + request builder. |
| src/governance-app-frontend/src/common/hooks/governance/useGovernanceNeurons.ts | Switches to shared neurons query descriptor + request builder. |
| src/governance-app-frontend/src/common/hooks/governance/useGovernanceMetrics.ts | Switches to shared metrics query descriptor. |
| src/governance-app-frontend/src/common/constants/extra.ts | Replaces MIN_ASYNC_DELAY with LOADING_INDICATOR_DELAY. |
| src/governance-app-frontend/src/common/components/QueryStates.tsx | Uses delayed loading flag to avoid skeleton flashes for fast responses. |
| src/governance-app-frontend/src/common/canisters/index.ts | New non-React canister accessors for loaders (using shared agents). |
| src/governance-app-frontend/src/common/canisters/agents.ts | New module-level agent singletons to enable network calls pre-mount. |
| src/governance-app-frontend/src/app/contexts/agentPoolProvider.tsx | Updates provider to consume the new agent singletons and clear on logout. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…ange
The staking wizard stopped opening on the third neuron in the mobile e2e
run: the "Done" click on the previous success dialog did not close it, so
the next click landed on a blocked background.
Modal state on these routes lives in the search params, and a route with
a loader and no `staleTime` reruns that loader on every navigation to
itself — so `?openWizard=true` -> `{}` was pushing each open and close
through the loader lifecycle.
`staleTime: Infinity` scopes the loader to what it is for: warming the
cache when the route is entered. Voting keeps `proposalFilter` in
`loaderDeps`, so a filter change still reruns it.
Review follow-ups on the loading-indicator delay: - `useDelayedFlag` reset `elapsed` from the effect cleanup, which conflates unmount with an `active` transition. Moving it into the effect body trips `react-hooks/set-state-in-effect`, so it now adjusts during render, which is React's documented pattern for reacting to a changed input. - The loader test asserted the `accountBalance` call count outside `waitFor`. The prefetches are not awaited, so that could pass or fail on timing; both counts are now waited on together.
Collaborator
Author
artkorotkikh-dfinity
approved these changes
Aug 7, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation
Two waits that don't need to be sequential were stacked on every navigation: the route chunk had to download and run before the component mounted, and only then did its queries hit the network. On nns.icp.net, going to
/neuronsspent 1092ms downloading a 20KB chunk and 481ms running it before the firstlistNeuronscall left the browser at t=1581ms.On top of that,
MIN_ASYNC_DELAYheld every IC read for at least 300ms so skeletons would not flash. Warm query calls come back in well under 100ms, so this was just added latency, and it also slowed down refetches that show stale data instead of a skeleton.Changes
AgentPoolProvidereffect into module singletons, so loaders can reach the network before React mounts. The provider uses the same instances, so there is still one agent per identity.common/queries, where the cache key and the fetcher for each query live together and are shared by theuse*hooks and the loaders. A drifted key would turn a prefetch into a wasted call, so this makes them impossible to define separately.MIN_ASYNC_DELAYand thewithMinimumDelaywrapping inuseQueryThenUpdateCallanduseInfiniteQueryThenUpdateCall.useDelayedFlagand used it inQueryStatesto hold the skeleton back 120ms, so a fast response goes straight to content instead of flashing.