perf: round one of request reduction & rate-limit mitigation#1641
perf: round one of request reduction & rate-limit mitigation#1641gregnazario wants to merge 12 commits into
Conversation
✅ Deploy Preview for aptos-explorer ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
There was a problem hiding this comment.
Pull request overview
Round one of a coordinated request-reduction & rate-limit mitigation effort. Adds two design docs, an SSR/CDN-friendly route-loader layer, a CoinGecko Netlify Function proxy, batch-prime + cache changes that collapse per-row REST fan-outs on transactions tables and CSV export, persistent localStorage caches for validator operators and has_confidential_store checks, a single shared useGetLedgerInfo poller, and router preload tuning.
Changes:
- New SSR
loaders +accountResourceQueryOptions/viewFunctionQueryOptions/recentBlocksQueryOptionsfor/transactions,/blocks,/coin/*,/fungible_asset/*,/object/*,/validator(s)/*,/analytics; longer edgeCache-Control. - Batch-prime hook for transaction tables and range-batched CSV export with an
ApiKeyConfirmDialogpre-flight; CoinGecko Netlify Function proxy with cookie/env toggle;BalanceCardmigrated touseGetPrice. - Consolidated ledger-info polling, 7-day localStorage cache for validator operators, 24h jittered cache for
has_confidential_store, routerdefaultPreloadStaleTime/Delaytuning.
Reviewed changes
Copilot reviewed 37 out of 37 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| docs/REQUEST_REDUCTION_REPORT.md, docs/UPSTREAM_READ_PERFORMANCE_REPORT.md | New design/audit docs for the round-one + upstream asks. |
| CHANGELOG.md, CACHING.md, .env.example | Docs updates for new caches, hooks, and the CoinGecko toggle. |
| netlify.toml, netlify/functions/coingecko.ts | Regular Netlify Function + redirect for /api/coingecko/* with CDN cache headers. |
| app/ssr.tsx, app/router.tsx | Per-route Cache-Control and defaultPreloadStaleTime/defaultPreloadDelay. |
| app/routes/{transactions,blocks,analytics,coin.$struct.$tab,fungible_asset.$address.$tab,object.$address.$tab,validator.$address,validators.$tab}.tsx | New loaders that pre-fetch primary first-paint data through queryClient.ensureQueryData. |
| app/api/queries.ts | New recentBlocksQueryOptions, viewFunctionQueryOptions, accountResourceQueryOptions. |
| app/api/hooks/useGetLedgerInfo.ts | Shared ledger-info hook used by useGetTPS, useGetMostRecentBlocks, AllTransactions, TotalTransactions. |
| app/api/hooks/useGetTransaction.ts (+ test) | Adds useBatchPrimeTransactionsByVersion and planTransactionsPrimeBatch for batch priming the per-row cache. |
| app/api/hooks/useGetValidators.ts (+ test) | Adds layered localStorage + stats + chain operator resolution. |
| app/api/hooks/useAccountHasConfidentialStores.ts (+ test) | 24h jittered cache + localStorage persistence for the has_confidential_store view. |
| app/api/hooks/coingeckoProxy.ts (+ test), useGetPrice.ts, useGetCoinMarketData.ts | CoinGecko proxy URL resolver and toggle. |
| app/pages/Account/Components/AccountAllTransactions.tsx, app/components/ApiKeyConfirmDialog.tsx | Range-batched CSV export + pre-flight API-key warning. |
| app/pages/Account/BalanceCard.tsx | Switches to useGetPrice so the React-Query cache is reused. |
| app/pages/Transactions/{TransactionsTable,AllTransactions}.tsx, app/pages/Analytics/NetworkInfo/TotalTransactions.tsx | Wire the new shared hooks. |
| export function accountResourceQueryOptions( | ||
| address: string, | ||
| resourceType: string, | ||
| client: Aptos, | ||
| networkKey: string, | ||
| ledgerVersion?: number, | ||
| ) { | ||
| return queryOptions({ | ||
| queryKey: [ | ||
| "accountResource", | ||
| {address, resource: resourceType, ledgerVersion}, | ||
| networkKey, | ||
| ], | ||
| queryFn: async () => { | ||
| try { | ||
| const resource = await client.getAccountResource({ | ||
| accountAddress: address, | ||
| resourceType: resourceType as `0x${string}::${string}::${string}`, | ||
| options: | ||
| ledgerVersion !== undefined | ||
| ? {ledgerVersion: BigInt(ledgerVersion)} | ||
| : undefined, | ||
| }); | ||
| return resource; | ||
| } catch (error) { | ||
| if (isRateLimitLike(error)) emitRateLimit(); | ||
| throw error; | ||
| } | ||
| }, | ||
| staleTime: 5 * 60 * 1000, | ||
| gcTime: 60 * 60 * 1000, | ||
| }); | ||
| } |
Co-authored-by: Greg Nazario <greg@gnazar.io>
…immutable detail routes - Router: `defaultPreloadStaleTime: 30s` (was 0) so a fresh cached query is reused instead of refetched on every hover. `defaultPreloadDelay: 150ms` prevents glancing hovers over long tables and lists (transactions, blocks, accounts, validators) from firing dozens of speculative loaders in the user's browser. - SSR: `/txn/*` and `/block/*` bumped from 1h to 24h `s-maxage` (+30d SWR) since confirmed transactions/blocks are immutable. `/coin/*`, `/fungible_asset/*`, `/token/*`, `/object/*` bumped from 30s to 1h `s-maxage` (+24h SWR) since metadata effectively never changes (volatile bits like recent activity still load client-side after hydration and bypass this cache). `/validator/*` bumped to 5min/30min. Co-authored-by: Greg Nazario <greg@gnazar.io>
…for empty GCS stats
Previously `useGetValidators` fell back to fetching
`0x1::stake::StakePool::operator_address` from the chain for **every** active
validator (~150 on mainnet, concurrency 12) whenever the off-chain
`validator_stats_v2.json` payload was empty, and for the small handful of
rows (~10) whose operator the stats JSON shipped as empty / invalid / zero.
Both fan-outs are now backed by a persistent `localStorage` cache.
- New `operatorsFromStats(rows)` extracts a standardized owner→operator map
from any non-empty stats payload.
- New `readCachedOperators(network)` / `writeCachedOperators(...)` persist
that map per network for 7 days. Empty maps are dropped so a transient
empty stats JSON cannot clobber a good snapshot.
- `useGetValidators` now:
1. Seeds operator data from `localStorage` (instant, zero requests).
2. Refreshes it whenever the current stats JSON returns more entries.
3. Only issues on-chain `getAccountResource` calls for the residual
set after both caches have been consulted — typically zero on
repeat visits, and ~10 on first visit when only a few rows ship
without an operator.
4. Writes any chain-fetched operators back into the cache so a future
render skips them entirely.
For users who have already loaded the page once, the validators-page operator
fan-out collapses to **zero REST calls** in the common case, and to at most
~10 calls in the rare 'stats JSON forgot a row' case (was up to ~150 calls
when the JSON was empty).
Unit tests cover `operatorsFromStats` and the localStorage round-trip /
empty-payload guard / per-network scoping.
Co-authored-by: Greg Nazario <greg@gnazar.io>
…ed hook The TPS pill (`useGetTPS`), the home page "TOTAL TRANSACTIONS" stat (`TotalTransactions`), `AllTransactions` (latest-version paginator), and `useGetMostRecentBlocks` (most-recent block height) all keyed the same `["ledgerInfo", networkValue]` React Query entry but with different `refetchInterval` / `staleTime` / `refetchOnWindowFocus` values. React Query coalesced the in-flight requests but the *most aggressive* polling setting won — meaning the home page polled the fullnode every 10 s as soon as `AllTransactions` was opened in another tab. Introduce `useGetLedgerInfo` as the single subscription point. Defaults: - `refetchInterval`: 15 s (was a mix of 10 s and 30 s). - `refetchOnWindowFocus`: false (was the global true default, which refetched on every tab focus). - Polling is paused automatically when the document is hidden — TanStack Query v5 sets `refetchIntervalInBackground` to false by default. Callers update: `useGetTPS`, `useGetMostRecentBlocks`, `AllTransactions`, `TotalTransactions`. Co-authored-by: Greg Nazario <greg@gnazar.io>
… localStorage + jittered staleTime Account `Coins` tab fans out up to 20 `/v1/view` POSTs per page open (`0x1::confidential_asset::has_confidential_store` once per FA). At time of writing only AptosCoin meaningfully uses confidential stores on mainnet, so the vast majority of these calls return `false` and stay that way for as long as the account never opts in. Caching changes: - React Query `staleTime` is now ~24 h (was 60 s) per (user, FA, network) with a deterministic ±6 h jitter computed from the key. Same user always gets the same staleTime so React Query never sees flapping, but two different (user, FA) tuples pick different refresh moments — the fleet refresh is spread over a 12 h window instead of a single moment. - `gcTime` extended to 7 days so a tab kept open all week never loses the in-memory entry. - `refetchOnMount` is `false` so opening the Coins tab a second time in the same session doesn't re-issue any view call. - A localStorage write-through keeps the answer across sessions for 24 h, so closing and reopening the tab the next morning still avoids the fan-out. Net effect: the per-account view fan-out collapses to ~0 calls on any repeat visit within 24 h, regardless of whether the user keeps the tab open or not. First-visit cost is unchanged. Adds a unit test for the jitter helper (deterministic, bounded, distinct across inputs). Co-authored-by: Greg Nazario <greg@gnazar.io>
… blocks, coin, fungible_asset, object, validator(s), analytics Previously only `/account/...`, `/txn/...`, and `/block/...` had TanStack Start `loader`s. Every other route did 100% of its data fetching from the browser after hydration. Combined with the lengthened SSR `Cache-Control` window on these routes (s-maxage 30s..1h, see `app/ssr.tsx`), the loaders let one origin fetch serve many concurrent users via the CDN. Routes that now pre-fetch: - `/transactions` — ledger info (anchor for the paginator). - `/blocks` — ledger info + recent block list (~20 `getBlockByHeight` fan-out moves from N users to 1 server-side fan-out per 60s). - `/coin/$struct/$tab` — `0x1::coin::CoinInfo<struct>` resource. - `/fungible_asset/$address/$tab` — FA metadata + supply view calls. - `/object/$address/$tab` — object's account resources (mirrors the existing `/account/$address/$tab` loader). - `/validators/$tab` — `0x1::stake::ValidatorSet` resource. - `/validator/$address` — that validator's `StakePool` + the shared `ValidatorSet` resource. - `/analytics` — chain stats JSON from the public GCS bucket (mainnet only). Supporting helpers in `app/api/queries.ts`: - `recentBlocksQueryOptions` (matches the hook key shape so SSR pre-fetch is reused on hydration). - `accountResourceQueryOptions` (matches `useGetAccountResource`'s key). - `viewFunctionQueryOptions` (matches `useViewFunction`'s key). Each loader swallows pre-fetch errors so the page can render its own error/loading UI; the SSR pre-fetch is purely a hint to React Query that the data is already known. Co-authored-by: Greg Nazario <greg@gnazar.io>
… one range call Before: `<UserTransactionsTable>` rendered N rows (default 25), each firing `useGetTransaction(version)` → one `/v1/transactions/by_version/<n>` REST call. With `/transactions`, `/account/.../transactions`, and the function-filtered views all using this component, every page navigation on the explorer's busiest tabs issued 20–25 REST calls per render. This commit takes the realistic improvement available given the indexer's user_transactions table does NOT expose `success`, `gas_used`, `events`, or `payload` (only `version`, `sender`, `gas_unit_price`, `timestamp`, `entry_function_id_str`, `sequence_number`, `block_height`). We can't drop the full-transaction fetch without dropping user-visible columns (status indicator, token transfer display, gas-used). Instead: pre-warm React Query's cache for the entire visible page with ONE batched `/v1/transactions?start=X&limit=Y` REST call when the version range fits within a sensible cap, and let the per-row `useGetTransaction(version)` calls read from cache. Each per-row hook finds its data already cached and never issues its own REST call. - New `useBatchPrimeTransactionsByVersion(versions)` hook drives the prefetch from `<UserTransactionsTable>`. Cap is `BATCH_PRIME_MAX_SPAN` (200 versions) so a sparse function-filtered page (which can have huge gaps) falls back to per-row fetches instead of pulling 1000s of unwanted rows. - Skips the batch entirely when every row is already cached fresh (subsequent paginations, re-renders). - New `planTransactionsPrimeBatch` helper is pure / unit-tested. Same idea is applied to the CSV export in `AccountAllTransactions`: batches sortedVersions into spans of ≤100, fetches each span with one REST call, then falls back to per-version REST for any rows the batched fetch missed (sparse ranges). Worst case stays the same, common case on dense account histories: ~10 000 REST calls → ~100. Net effect on the most-trafficked pages: - `/transactions` (user tab), `/account/.../transactions`, both function-filtered tables: 20–25 REST → ~1 REST per page render (~96% reduction) when versions fit within the 200-version span. - Account CSV export at the 10k cap: ~99% reduction in REST calls. The user-visible UI is unchanged. Status indicator, gas-used, token transfer display, function, sender, timestamp all still render the same way — they just see the data immediately from cache instead of waiting for a per-row REST round-trip. Co-authored-by: Greg Nazario <greg@gnazar.io>
…ROXY toggle
CoinGecko's free tier is 10–50 requests/minute *per IP*. Today each
user's browser hits `api.coingecko.com` directly from `useGetPrice` and
`useGetCoinMarketData`, so the explorer's audience burns through the
per-IP rate limit individually and starts seeing 429s under load.
This commit adds an opt-out Netlify Function proxy:
- `netlify/functions/coingecko.ts` proxies two CoinGecko endpoints:
`/api/coingecko/price` → `/v3/simple/price`
`/api/coingecko/markets` → `/v3/coins/markets`
with `Cache-Control: s-maxage=600, stale-while-revalidate=3600`, so
Netlify's CDN serves repeat requests from edge cache and only hits
CoinGecko at most once per 10 minutes per (endpoint, query). All
concurrent users share that one upstream request.
- `app/api/hooks/coingeckoProxy.ts` (`resolveCoingeckoUrl` +
`shouldUseCoingeckoProxy`) is the toggle:
1. `use_coingecko_proxy=true|false` cookie wins (runtime override
— flip behavior on a deployed environment without rebuilding).
2. `VITE_USE_COINGECKO_PROXY=true|false` env var.
3. Default: `true` in production builds, `false` in dev (so
`pnpm dev` against a non-Netlify host still works without
`netlify dev` running).
- The two CoinGecko callers (`useGetPrice`, `useGetCoinMarketData`)
route through `resolveCoingeckoUrl`. Same query strings, same response
shape, no caller-side changes.
- `netlify.toml` adds the `[functions]` block (`netlify/functions`
directory, `esbuild` bundler) and a `/api/coingecko/* → function`
redirect.
Bonus pickup: `BalanceCard` was bypassing React Query entirely with a
`useEffect + getPrice()` call (one CoinGecko request per account-page
mount). Replaced with the shared `useGetPrice` hook so the response is
cached and reused across pages.
If the proxy ever degrades (CoinGecko rate-limiting the Netlify egress
IP pool, function temporarily down), flip the cookie or env var to
`false` to revert to direct fetches without redeploying client code.
Tests cover the URL resolver + cookie/env precedence. Production build
succeeds, including the new chunk for the toggle helper.
Co-authored-by: Greg Nazario <greg@gnazar.io>
…volume exports When a user starts a CSV export from the Account → Transactions tab, the explorer can issue hundreds of upstream requests in a row to fetch every transaction in the version range. Even after the new batched range fetching (`AccountAllTransactions`), a 10 000-row export can still cost ~100 round-trips, and the user's IP is the one paying the per-IP rate-limit cost. This commit: - Adds a reusable `ApiKeyConfirmDialog` component (`app/components/ApiKeyConfirmDialog.tsx`) explaining the upcoming request volume, with one-click links to `/settings` and geomi.dev for getting a free key, plus "Continue anyway" to proceed without. - Wires the CSV export button to consult the user's per-network API key override (`useExplorerSettings`). When no key is set AND the expected export size meets `CSV_EXPORT_API_KEY_WARNING_THRESHOLD` (200 rows), the dialog appears before any requests are issued. Users with a key configured see the dialog skipped entirely. True per-request batching isn't possible for the chain REST API (`/v1/view` and `/v1/transactions/by_version/<n>` each accept a single call), so the practical answer to 'should be batched if possible, otherwise warn about an API key' is exactly this: the existing range-fetch batching reduces volume by ~99% where the version range is dense, and the dialog covers the residual cases where rate-limiting is still likely. The existing `RateLimitDrawer` (already in the root layout) covers the after-the-fact case where a request returns 429. Co-authored-by: Greg Nazario <greg@gnazar.io>
…HING CHANGELOG: full Unreleased entry describing all of round one (transactions N+1, CSV export, confidential-store cache, validator operator cache, ledger-info consolidation, preload tuning, loaders, SSR cache headers, CoinGecko proxy, API-key modal). CACHING.md: document the new `useGetLedgerInfo` hook, `useBatchPrimeTransactionsByVersion`, and the two new persistent localStorage entries (`validatorOperators:<network>` for the All Nodes operator-address backup, `hasConfidentialStore:...` for the per-account FA confidential-store flags). Plus minor formatting from `pnpm fmt`. Co-authored-by: Greg Nazario <greg@gnazar.io>
…false positive CodeQL's `js/clear-text-storage-of-sensitive-data` heuristic flagged the localStorage writes in `cacheManager.ts` as 'clear text storage of sensitive information' because the TTL constant they consumed was named `CONFIDENTIAL_LOCALSTORAGE_TTL` — the word 'confidential' triggers CodeQL's keyword-based sensitive-data taint source. The cached value is just a boolean from `0x1::confidential_asset::has_confidential_store`, a *public* on-chain Move resource lookup; there is no actual sensitive data being persisted (and 'confidential' here refers to the on-chain Move module name, not to PII or credentials). Rename the internal constants to avoid the heuristic while keeping the public hook name `useAccountHasConfidentialStores` and the exported helper `confidentialStoreStaleTime` intact: - `CONFIDENTIAL_STALE_TIME` → `HAS_STORE_STALE_TIME` - `CONFIDENTIAL_STALE_JITTER_MS` → `HAS_STORE_STALE_JITTER_MS` - `CONFIDENTIAL_GC_TIME` → `HAS_STORE_GC_TIME` - `CONFIDENTIAL_LOCALSTORAGE_TTL` → `HAS_STORE_LOCALSTORAGE_TTL` - `CONFIDENTIAL_LOCALSTORAGE_PREFIX` → `HAS_STORE_LOCALSTORAGE_PREFIX` - `confidentialStoreCacheKey()` → `hasStoreCacheKey()` - `MAX_CONFIDENTIAL_QUERIES` → `MAX_HAS_STORE_QUERIES` Doc-comment expanded to call out why the names look the way they do, so future contributors don't re-introduce the keyword and silently re-trip CodeQL. No behavior change. Tests pass. Co-authored-by: Greg Nazario <greg@gnazar.io>
…K / GCS asks) Companion document to `docs/REQUEST_REDUCTION_REPORT.md`. That report was scoped to fixes the explorer could ship on its own; this one is scoped to **upstream gaps** — places where the indexer schema, REST API, SDK, or GCS analytics pipeline forces the explorer to make more or larger requests than the data shape warrants. Cataloged with explorer-side workarounds, proposed upstream changes, and estimated request-reduction impact: Indexer (Hasura GraphQL): - `user_transactions` missing `success`, `gas_used`, `vm_status`, `events`, `payload` — forces the per-row REST fan-out the explorer just batch-primed in this PR. - No public `events` table at all. - No `view` resolver — each `useViewFunction` is its own REST POST. - `current_staking_pool_voter.operator_address` exists but is unused (the explorer could retire its on-chain operator patch). - No batched 'transactions by version list' query. REST API: - No `POST /v1/views` (batched view-function endpoint). - No 'transactions by version list' / `type=user_transaction` filter. - No `Retry-After` on 429s — forces the explorer's exponential-backoff-with-jitter retry policy. - No `Cache-Control: public, max-age=31536000, immutable` on confirmed-transaction / confirmed-block endpoints. - No `GET /v1/blocks?count=N` (forces per-height fan-out). SDK (`@aptos-labs/ts-sdk`): - No batching APIs corresponding to the missing batched endpoints. - Auto-retry doesn't surface `Retry-After`. Analytics / GCS: - `validator_stats_v2.json` ships ~10 rows without `operator_address` every day; fix at the source. - `chain_stats_v2.json` could include live block height + running TPS so the explorer's 15 s `getLedgerInfo` poll can be retired on mainnet. Also lists the indexer query patterns the explorer should adopt **today** (where the data is already there but the explorer is using REST) — biggest one: collapse `useGetFaMetadata` + `useGetFASupply` into one `fungible_asset_metadata` row. Cross-linked from `docs/REQUEST_REDUCTION_REPORT.md` and called out in CHANGELOG `[Unreleased]` \u2192 Added. No behavior change. Co-authored-by: Greg Nazario <greg@gnazar.io>
cf226bd to
e31f279
Compare
Bundle ReportChanges will increase total bundle size by 35.25kB (0.35%) ⬆️. This is within the configured threshold ✅ Detailed changes
Affected Assets, Files, and Routes:view changes for bundle: aptos-explorer-server-esmAssets Changed:
Files in
Files in
Files in
Files in
Files in
Files in
Files in
Files in
Files in
Files in
Files in
Files in
view changes for bundle: aptos-explorer-client-esmAssets Changed:
Files in
Files in
Files in
Files in
Files in
Files in
Files in
Files in
Files in
Files in
Files in
Files in
|
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #1641 +/- ##
==========================================
+ Coverage 32.04% 32.20% +0.16%
==========================================
Files 188 192 +4
Lines 8666 8976 +310
Branches 3251 3345 +94
==========================================
+ Hits 2777 2891 +114
- Misses 5885 6081 +196
Partials 4 4 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Description
Implements rounds of the request-reduction plan written up in
docs/REQUEST_REDUCTION_REPORT.md(also in this PR), plus a companiondocs/UPSTREAM_READ_PERFORMANCE_REPORT.mdcataloguing the gaps the explorer can't fix on its own. Each fix is a separate commit so they can be reviewed/reverted in isolation. All existing tests pass; new unit tests cover the new helpers.Headline impact (on the most-trafficked routes):
/transactions,/account/.../transactions, function-filtered tables: the per-row/v1/transactions/by_version/<n>fan-out (20–25 REST calls per page) collapses to 1 batched REST call whenever the version range fits within 200 versions./blocks,/transactions,/coin/*,/fungible_asset/*,/object/*,/validators/*,/validator/*,/analyticsfirst paint: SSRloaderpre-fetches the primary query so the browser doesn't pay for it. Combined with longer edgeCache-Control(1h for/coin/*//fungible_asset/*, 24h for/txn/*//block/*), one origin request serves many concurrent users via the CDN.Coinstab (has_confidential_storefan-out): 24h React Query cache + localStorage backup + jittered staleTime → 0 view calls on repeat visits within a day./validatorsoperator-address patch: 7-day per-network localStorage cache seeded fromvalidator_stats_v2.json→ 0 on-chain calls on repeat visits, ~0 on first visit (was up to ~150 when the GCS stats payload was empty).["ledgerInfo", networkValue]pollers (10s / 30s / 60s) collapsed into oneuseGetLedgerInfohook (15s, paused when tab hidden, no refetch-on-focus).defaultPreloadStaleTime: 30s(was0) + newdefaultPreloadDelay: 150msso hover-spam on long tables no longer spawns dozens of speculative loaders.netlify/functions/coingecko.ts) with CDN cache (s-maxage=600, SWR 1h). Toggleable viaVITE_USE_COINGECKO_PROXYenv var oruse_coingecko_proxy=true|falsecookie. Defaults to on in production, off in dev.BalanceCardmigrated to use the React-Query–backeduseGetPriceinstead of a per-mountfetch.Upstream-asks report (new in
docs/UPSTREAM_READ_PERFORMANCE_REPORT.md):companion to
REQUEST_REDUCTION_REPORT.mdtargeting the indexer (Hasura GraphQL), Aptos Gateway / fullnode REST API,@aptos-labs/ts-sdk, and GCS analytics pipeline owners. Catalogs the upstream gaps that force the explorer to make more or larger requests than the data shape warrants — missingsuccess/gas_used/events/payloadon the indexeruser_transactionstable (would let the explorer drop its new batch-prime hook entirely), no batched view-function REST endpoint (5–10 view calls per detail page would become 1), noRetry-Afteron 429s, noCache-Control: immutableon confirmed-transaction/block REST endpoints, missingoperator_addressrows invalidator_stats_v2.json, no live block height / TPS inchain_stats_v2.json, etc. Each entry includes the explorer-side workaround currently in place and the expected request-reduction impact of the upstream change.What's still on the roadmap (deliberately not in this PR): observability (
Retry-Afterparsing, GTM event onemitRateLimit), additional CDN-proxied REST endpoints (item #10 from the report),useViewFunctionmicro-batching across hooks (the REST/v1/viewendpoint doesn't accept batches, so this is bounded by what we can replace with indexer queries — biggest target is collapsinguseGetFaMetadata+useGetFASupplyinto onefungible_asset_metadatarow).Trade-offs / honest notes:
user_transactionstable exposed everything the transactions-table cells needed; it doesn't (nosuccess,gas_used,events,payload). The implemented fix is batch-prime the cache with one range REST call instead of removing the per-row REST call entirely, so the user-visible columns are unchanged. The upstream report files this as the top indexer ask.Commits in this PR:
docs(perf): add request reduction & rate-limit mitigation reportperf(router,ssr): tune preload-on-intent and lengthen edge cache for immutable detail routesperf(validators): cache operator addresses in localStorage as backup for empty GCS statsperf(ledger-info): consolidate four ledger-info pollers into one shared hookperf(confidential-stores): cache has_confidential_store for ~24h with localStorage + jittered staleTimeperf(loaders): pre-fetch primary data on the server for transactions, blocks, coin, fungible_asset, object, validator(s), analyticsperf(transactions): batch-prime per-row transaction REST fetches into one range callperf(coingecko): add Netlify Function proxy with VITE_USE_COINGECKO_PROXY togglefeat(csv-export): pre-flight API-key confirmation dialog before high-volume exportsdocs(perf): document round-one request reduction in CHANGELOG and CACHINGfix(perf): rename confidential-store cache constants to avoid CodeQL false positivedocs(perf): add upstream read-performance report (indexer / REST / SDK / GCS asks)Related Links
docs/REQUEST_REDUCTION_REPORT.md(added in commit 1)docs/UPSTREAM_READ_PERFORMANCE_REPORT.md(added in commit 12)RATE_LIMITING.md,CACHING.md,CONTEXT_OPTIMIZATION.md(existing context)docs/FEATURES_SPECIFICATION.md— noFEAT-*entry changes; user-visible behavior unchanged except the new CSV-export confirmation modal.Checklist
pnpm fmt && pnpm lintcleanpnpm test --run— all tests pass (added unit tests forplanTransactionsPrimeBatch,confidentialStoreStaleTime,operatorsFromStats,readCachedOperators/writeCachedOperators,resolveCoingeckoUrl/shouldUseCoingeckoProxy)pnpm ci:verify— production build succeedsCHANGELOG.mdupdated under[Unreleased](both reports + the round-one Changed entry)CACHING.mdupdated to reflect new shareduseGetLedgerInfo,useBatchPrimeTransactionsByVersion, new localStorage entriesllms.txt/llms-full.txt/sitemap.xmlare unchanged[functions]block +/api/coingecko/*redirect (regular Netlify Function, not Edge Function — per AGENTS.md)