perf(home): stop counting the whole library for the two home rows - #4110
Merged
Conversation
The "Recently added" and "Recently played" rows each ask for 15 games and render 15 covers, but both requests still made the server count every game in the library and then discarded the number. On a large library that count is the expensive part of the request, so opening the home page paid for two whole-library counts nobody sees. Both rows now send with_total=false, the option added for the gallery in rommapp#4062. The flag has to reach the request and the cache-clear pattern together: the pattern is matched against the cache key by substring, so adding it to only one side would leave the row permanently stale. The cached service now builds both from a single parameter map per row so they cannot drift, and the tests assert the clear pattern still matches the key the request writes. Fixes rommapp#4099 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Contributor
Greptile SummaryThe PR avoids unnecessary whole-library counts for the two fixed-size home rows while preserving their cache invalidation contract.
Confidence Score: 5/5The PR appears safe to merge with no concrete correctness or security issues identified. The backend explicitly supports omitting totals, affected consumers only use response items, and request/invalidation serialization remains synchronized through shared parameter objects. Important Files Changed
Reviews (1): Last reviewed commit: "perf(home): stop counting the whole libr..." | Re-trigger Greptile |
gantoine
approved these changes
Aug 4, 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.
Description
Fixes #4099
The home page renders two fixed rows, "Recently added" and "Continue playing", each requesting 15 games. Both also asked the server to count the result set and then discarded the number, since the rows only ever draw 15 covers.
GET /api/romsalready acceptswith_total(added for the gallery in #4062). This passeswith_total=falseon both home requests.Measured against a real 83,130-game library (RomM 5.1.1-beta.1), median of 8 alternated runs:
The win is concentrated in "Recently added", whose count spans the whole library. "Continue playing" carries
last_played=true, so its count covers only the played subset (90 rows here) and was never expensive. The flag is applied there too for symmetry and because the saving scales with a user's played library, but it is not where the time goes: with the count already off, that request measures 0.168s unfiltered, 0.477s once thelast_playedfilter is applied, and 0.692s once sorted bylast_played. That cost belongs to #4067, not here.#4062 deliberately left these two alone. The home rows go through the client-side Cache API layer, where
clearCacheForPatterndoes a substring match against a cache key built asnew URLSearchParams(params).toString(). The request params and the cache-clear pattern were two independent copies of the same literal map, so adding a parameter to one side and not the other, or at a different position, would silently stop those rows from ever being invalidated again.So rather than introduce a fourth copy of the literal, each row's params now live in a single
constthat both the request and its clear pattern read from, which makes the desync unrepresentable rather than merely avoided.Testing
npm run test- 53 files, 640 tests passing (up 4, all additive; no existing test changed).npm run typecheck- clean.trunk fmt && trunk check- no issues.with_totalto the clear pattern only) makes both fail, and they pass again once reverted.with_total=falsereturnstotal: nullwith the same 15 items in the same order; the response envelope keys and every per-item field are otherwise unchanged. Timings above.total, but end-to-end confirmation would be watching the two home/api/romsrequests carrywith_total=false.Anything a reviewer should pay attention to
const, never at a call site. A parameter present on the request but not the clear pattern (or in a different order) makes the cache key stop matching the clear pattern, and the row goes permanently stale with no error. That hazard is exactly why this was carved out of perf(roms): let a gallery window fetch skip the library count #4062, and the two invariant tests exist to catch it.totalis nownullfor these two responses. Verified nothing consumes it:stores/roms.tsdestructures onlyitems, and both home views read the resulting store arrays.CustomLimitOffsetPage_SimpleRomSchema_.totalis already typednumber | null, so no regeneration is needed.resolve_total()already returnsNonewhenwith_totalis false and the ROM id index is not being built, which is what both rows send.config.paramsas read-only: the axiosparamsSerializerandCacheService.generateCacheKeyeach construct a freshURLSearchParams. Nothing mutates the object, so the request and the clear pattern cannot diverge at runtime.src/services/**; the v1 freeze gate coversfrontend/src/{views,components,console,layouts}/**. Both the v1 and v2 home pages benefit, since they sharestores/roms.ts.AI Assistance Disclosure
AI assistance (Claude Code) was used for this contribution, covering code generation, the unit tests, and this pull request description. All changes were reviewed and verified locally by me before submission. Any replies I post on this PR are written by me unless stated otherwise.
Checklist