Dev.to · 5 min read

One of our nine cached queries has exactly one caller: the function whose only job is to warm its cache

One of our nine cached queries has exactly one caller: the function whose only job is to warm its cache

React's cache() memoises a function for the duration of one server request. The preload pattern that goes with it is documented like this: call the fetch at the top of the page without awaiting it, render components that call the same cached function, and the components get the already-started promise instead of starting a second query. We have both. lib/db/cache.ts is 219 lines and exports nine cache()-wrapped queries. lib/db/preload.ts is 42 lines and exports two helpers that fire them: export function preloadDashboardData(userId: string) { // Fire all queries in parallel - don't await void getCachedUser(userId); void getCachedUserStats(userId); void getCachedUserSessions(userId, { limit: 10, offset: 0 }); } Both helpers have exactly one caller each: the dashboard page and the settings page. I went to check what each one buys, because void-ing a promise is the kind of code that is either load-bearing or decorative and looks the same either way. On the dashboard it is decorative preloadDashboardData(user.id); // All these queries run in parallel and use React cache() for deduplication // All these queries run in parallel and use React cache() for deduplication. const [planInfo, dbUser, userStats, { sessions }, ...] = await Promise.all([ getUserPlan(user.id), getCachedUser(user.id), getCachedUserStats(user.id), getCachedUserSessions(user.id, { limit: 10, offset: 0 }), ... ]); The preload fires three queries. Two lines later the same three are awaited inside a Promise.all that would have started them in parallel anyway. The preload gains the microseconds between those two statements and nothing else. (The duplicated comment line is real and is in the file twice, once without a full stop.) That is not the same as saying cache() is doing nothing here. The dashboard layout also reads the user row: // app/dashboard/layout.tsx const [dbUser, displayCurrency, priceRegion, schoolDiscount] = await Promise.all([ getCachedUser(user.id), ... ]); Layout and page are separate component trees rendered for the same request, and there is no prop drilling between them. Without cache() that is two identical SELECTs per dashboard render. With it, the second one is a hit. getCachedUserStats leans on the same thing deliberately: instead of issuing a narrower query for two columns, it awaits getCachedUser and reads the columns off the row that is already in flight. So the win here is deduplication across trees, which needs no preload call at all. The preload is the part of the pattern we did not need, bolted onto the part we did. On the settings page it costs two queries export function preloadSettingsData(userId: string) { void getCachedUser(userId); void getCachedAuditLogs(userId, 50); } The settings page calls that helper on line 30 and then never reads either result. It does not call getCachedUser. The one thing it needs from the row, whether the account is an OAuth account, comes off the auth user it already has. It does not render the activity log at all. The only thing on the page that touches audit_logs is an export button, and it fetches /api/user/audit-log?limit=1000 when somebody clicks it. That is a different request, and that route calls getCachedAuditLogsWithPagination, which is a different function and therefore a different cache entry. A cache() entry does not survive the response that created it. Grep the whole repository for getCachedAuditLogs and you get four hits: its definition, a comment in the schema file naming it next to the pagination variant, and the two lines in preload.ts that import and call it. It has exactly one caller, and that caller exists to warm the cache that nothing reads. The cost is not theoretical. Every render of the settings page runs a 50-row ordered read of audit_logs for the signed-in user, plus a full-row user read, and discards both. The other half: caches that outlive a request cache() is per request. Our population statistics are per game and change once a night, so they use unstable_cache instead, keyed per game id at module scope: unstable_cache(fetcher, [`population-stats-${gameId}`], { ... }) That one is cross-request and invalidated by tag from the nightly cron. The repository also wraps it in a cache() for RSC use: getCachedStatsWithDefaultRSC = cache((gameId: string) => this.getCachedStatsWithDefault(gameId)); That wrapper has zero callers. Every consumer, including the public API route, calls the non-RSC version, which is correct for a route handler: a handler serves one request and calls it once, so the per-request memo has nothing to deduplicate. Across the repo there are 18 cache() call sites in five files. Nine are the database layer above, and the rest wrap auth, plan resolution, the onboarding profile read and that unused statistics wrapper. The three questions worth asking about any preload call Does anything other than this component read the cached function? If not, the preload is decoration and Promise.all is doing the work. Is the reader in the same request? A client component fetching an API route is not. Nothing warmed on the server reaches it. Is it the same function with the same arguments? getCachedAuditLogs(id, 50) and getCachedAuditLogsWithPagination(id, 20, 0) share a table and nothing else. See it The per-request cache is invisible from outside by design: it exists so one render makes one query. The cross-request one is visible, because its result is public per game: curl -s https://cogniprep.app/api/games/population-stats/wlc-cognitive The lastUpdated field in that response is stamped at 02:00 UTC by the nightly job, and the same value comes back no matter how often you ask, which is unstable_cache plus a tag invalidation rather than a per-request memo. The page that reads it for a human is the Wonderlic practice hub. If you want to audit your own: grep for every function you wrapped in cache(), then grep for its callers. Any wrapped function with fewer than two call sites in a single request is paying the import and buying nothing, and any preload whose values are awaited on the next line is a comment with a void in front of it.

This is a summary aggregated from Dev.to. Read the complete article on the original site:

Read full article at Dev.to

More Programming & Dev News