Skip to content

perf(routing): prefetch route data in loaders and remove the 300ms delay on IC reads - #441

Open
yhabib wants to merge 3 commits into
mainfrom
perf/route-loaders-and-query-delay
Open

perf(routing): prefetch route data in loaders and remove the 300ms delay on IC reads#441
yhabib wants to merge 3 commits into
mainfrom
perf/route-loaders-and-query-delay

Conversation

@yhabib

@yhabib yhabib commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator

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 /neurons spent 1092ms downloading a 20KB chunk and 481ms running it before the first listNeurons call left the browser at t=1581ms.

On top of that, MIN_ASYNC_DELAY held 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

  • Moved agent creation out of the AgentPoolProvider effect 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.
  • Added common/queries, where the cache key and the fetcher for each query live together and are shared by the use* hooks and the loaders. A drifted key would turn a prefetch into a wasted call, so this makes them impossible to define separately.
  • Added loaders to dashboard, neurons and voting that fire both legs of their certified queries without awaiting them, so navigation is not blocked and the component attaches to the request already in flight.
  • Removed MIN_ASYNC_DELAY and the withMinimumDelay wrapping in useQueryThenUpdateCall and useInfiniteQueryThenUpdateCall.
  • Added useDelayedFlag and used it in QueryStates to hold the skeleton back 120ms, so a fast response goes straight to content instead of flashing.
  • Added a test that fails if a loader and its hook stop sharing a cache key.

…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.
@yhabib
yhabib requested a review from a team as a code owner July 30, 2026 18:33
Copilot AI review requested due to automatic review settings July 30, 2026 18:33
@zeropath-ai

zeropath-ai Bot commented Jul 30, 2026

Copy link
Copy Markdown

No security or compliance issues detected. Reviewed everything up to 154eacb.

Security Overview
Detected Code Changes
Change Type Relevant files
Enhancement ► src/governance-app-frontend/src/common/canisters/agents.ts
    Add agent singleton management and exported getters for anonymous/authenticated agents
Enhancement ► src/governance-app-frontend/src/common/canisters/index.ts
    Add canister accessors usable outside React with resolveAgent logic
Enhancement ► src/governance-app-frontend/src/common/canisters/agents.ts
    Implement getAnonymousAgent, getAuthenticatedAgent, clearAuthenticatedAgent, and internal caching
Enhancement ► src/governance-app-frontend/src/common/constants/extra.ts
    Introduce LOADING_INDICATOR_DELAY constant and related comments
Enhancement ► src/governance-app-frontend/src/common/hooks/governance/useGovernanceMetrics.ts
    Switch to governanceMetricsQuery descriptor for metrics
Enhancement ► src/governance-app-frontend/src/common/hooks/governance/useGovernanceNeurons.ts
    Switch to governanceNeuronsQuery descriptor and simplify parameters
Enhancement ► src/governance-app-frontend/src/common/hooks/governance/useGovernanceProposals.ts
    Switch to governanceProposalsQuery descriptor and remove hardcoded DEFAULT_OPTIONS
Enhancement ► src/governance-app-frontend/src/common/hooks/icpLedger/useIcpLedgerAccountBalance.ts
    Use icpLedgerAccountBalanceQuery for balance queries and remove canister reference
Enhancement ► src/governance-app-frontend/src/common/hooks/useDelayedFlag.ts
    Add new hook to delay loading indicator appearances
Enhancement ► src/governance-app-frontend/src/common/hooks/useInfiniteQueryThenUpdateCall.ts
    Refactor to use certified/non-certified query keys and remove minimum delay logic
Enhancement ► src/governance-app-frontend/src/common/queries/certified.ts
    Add certified/non-certified query key utilities and withCertifiedFlag helper
Enhancement ► src/governance-app-frontend/src/common/queries/governance.ts
    Introduce governance-related query descriptors and helpers (neurons, metrics, proposals)
Enhancement ► src/governance-app-frontend/src/common/queries/icpLedger.ts
    Add icpLedgerAccountBalanceQuery for balance reads with certified flag support
Enhancement ► src/governance-app-frontend/src/common/queries/prefetch.ts
    Add prefetchCertifiedQuery and prefetchCertifiedInfiniteQuery utilities
Enhancement ► src/governance-app-frontend/src/common/queries/routeLoaders.ts
    Add route load prefetched data helpers (dashboard, neurons, proposals)
Enhancement ► src/governance-app-frontend/src/common/utils/initializer.ts
    Expose queryClient in router context for route loaders
Enhancement ► src/governance-app-frontend/src/routes/__root.tsx
    Add RouterContext type and Route to use context-aware root route with queryClient
Enhancement ► src/governance-app-frontend/src/routes/_auth/dashboard/index.tsx
    Prefetch dashboard route data via loader and set infinite staleTime
Enhancement ► src/governance-app-frontend/src/routes/_auth/neurons/index.tsx
    Prefetch neurons route data via loader and set infinite staleTime
Enhancement ► src/governance-app-frontend/src/routes/_auth/voting/index.tsx
    Prefetch voting route data via loader with dependencies and set infinite staleTime
Enhancement ► src/governance-app-frontend/src/common/queries/routeLoaders.test.tsx
    Add tests for route loader prefetching behavior
Enhancement ► src/governance-app-frontend/src/common/queries/routeLoaders.ts
    Add prefetching logic for neurons, balance, metrics, proposals, and route-specific data
Enhancement ► src/governance-app-frontend/src/common/queries/routeLoaders.ts
    Implement prefetching for dashboard and voting routes based on identity
Enhancement ► src/governance-app-frontend/src/common/queries/routeLoaders.ts
    Add sessionIdentity and accountIdentifierOf helpers for identity-derived data
Enhancement ► src/governance-app-frontend/src/common/queries/prefetch.ts
    Extend prefetching to support both non-certified and certified queries, including infinite queries
Enhancement ► src/governance-app-frontend/src/common/queries/routeLoaders.test.tsx
    Test that route loaders warm exact cache entries read by neurons hook

@github-actions

github-actions Bot commented Jul 30, 2026

Copy link
Copy Markdown

📊 Build Bundle Stats

The latest build generated the following assets:

dist/index.html                                           2.47 kB │ gzip:   0.85 kB
dist/assets/index-DSwNl_yb.css                          131.57 kB │ gzip:  21.37 kB
dist/assets/externalServices-CK4eoRia.js                  0.19 kB │ gzip:   0.16 kB
dist/assets/address-C-4H442a.js                           0.20 kB │ gzip:   0.15 kB
dist/assets/icpLedger-C8IVtQi5.js                         0.23 kB │ gzip:   0.21 kB
dist/assets/Skeleton-zAxeOFaI.js                          0.46 kB │ gzip:   0.34 kB
dist/assets/useTvlValue-DoIGwJyt.js                       0.57 kB │ gzip:   0.39 kB
dist/assets/numbers-DJtvedQs.js                           0.58 kB │ gzip:   0.36 kB
dist/assets/service-CffYLUtU.js                           0.59 kB │ gzip:   0.37 kB
dist/assets/Spinner-BbBi8NCG.js                           0.59 kB │ gzip:   0.41 kB
dist/assets/PageHeader-Czh9Jiub.js                        0.77 kB │ gzip:   0.45 kB
dist/assets/CertifiedBadge-BRMhiDXr.js                    0.81 kB │ gzip:   0.48 kB
dist/assets/Separator-nlFj1fYk.js                         0.83 kB │ gzip:   0.49 kB
dist/assets/useIcpIndex-BCnvOxYX.js                       1.07 kB │ gzip:   0.63 kB
dist/assets/rolldown-runtime-CMxvf4Kt.js                  1.21 kB │ gzip:   0.67 kB
dist/assets/EmptyActionState-C8K6Bev4.js                  1.61 kB │ gzip:   0.83 kB
dist/assets/addressBook-C8D7WWeQ.js                       1.66 kB │ gzip:   0.99 kB
dist/assets/useCommandPaletteSettings-lLPvnkO6.js         1.73 kB │ gzip:   0.86 kB
dist/assets/useGovernanceAppCanister-l5i_Y-7e.js          1.91 kB │ gzip:   0.96 kB
dist/assets/AnimatedNumber-DX_QBNwy.js                    1.96 kB │ gzip:   1.11 kB
dist/assets/useIcpIndexTransactionsPolling-Bd8hqy6t.js    2.03 kB │ gzip:   1.09 kB
dist/assets/ToggleGroup-CfD82vQ_.js                       3.09 kB │ gzip:   1.41 kB
dist/assets/tickers-CrcBnHJF.js                           3.10 kB │ gzip:   1.47 kB
dist/assets/Switch-6NFSPaMn.js                            3.22 kB │ gzip:   1.44 kB
dist/assets/SensitiveValue-D61aIGuQ.js                    7.33 kB │ gzip:   3.28 kB
dist/assets/routes-aYfeYqXd.js                            7.46 kB │ gzip:   2.47 kB
dist/assets/badge-Bdvlq6FE.js                             8.49 kB │ gzip:   2.57 kB
dist/assets/spamFilter-QaCcYQjm.js                        8.54 kB │ gzip:   3.81 kB
dist/assets/MutationDialog-DXF4yS8Y.js                    9.53 kB │ gzip:   3.43 kB
dist/assets/events-CriNg-2h.js                           10.38 kB │ gzip:   4.11 kB
dist/assets/_id-CZaJCDs1.js                              14.33 kB │ gzip:   4.30 kB
dist/assets/_auth-ddwGwba9.js                            15.60 kB │ gzip:   5.74 kB
dist/assets/accounts-CXzczity.js                         23.72 kB │ gzip:   8.18 kB
dist/assets/GetTokens-Bh9NxNWA.js                        41.92 kB │ gzip:  14.90 kB
dist/assets/settings-Dhs4CxSU.js                         42.51 kB │ gzip:  12.20 kB
dist/assets/dashboard-Bs6pVCcq.js                        50.18 kB │ gzip:  15.02 kB
dist/assets/button-ClHF0ndL.js                           58.23 kB │ gzip:  20.00 kB
dist/assets/neurons-CnEsttbE.js                          67.94 kB │ gzip:  20.23 kB
dist/assets/voting-DBoylH-P.js                           69.21 kB │ gzip:  21.14 kB
dist/assets/vendor-md-CLh1GvDS.js                        73.29 kB │ gzip:  19.98 kB
dist/assets/neuronDetail-CbuHIscM.js                     77.03 kB │ gzip:  22.87 kB
dist/assets/index-BfXQhBrR.js                            80.20 kB │ gzip:  25.28 kB
dist/assets/vendor-tanstack-BztRJxNp.js                 161.18 kB │ gzip:  49.35 kB
dist/assets/vendor-core-react-qLCIanUQ.js               189.81 kB │ gzip:  59.76 kB
dist/assets/vendor-recharts-CC-WrRXd.js                 223.45 kB │ gzip:  64.00 kB
dist/assets/vendor-icp-DP4ARddz.js                      411.37 kB │ gzip: 104.08 kB
dist/assets/vendor-libs-C_IwxdbG.js                     554.14 kB │ gzip: 180.78 kB

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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_DELAY and 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.

Comment thread src/governance-app-frontend/src/common/hooks/useDelayedFlag.ts
yhabib added 2 commits July 30, 2026 21:14
…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.
@yhabib

yhabib commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator Author

Next #442 — first piece of finding #3 (stop fetching the collapsed proposals list), stacked on this branch.

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.

3 participants