Resolve fallback chapter info lazily per displayed spine item - #288
Open
mbret wants to merge 1 commit into
Open
Conversation
buildStaticChaptersInfo eagerly resolved href-based fallback chapter info for every spine item at book open, but that map is only ever read for the few items actually displayed (mapChapterInfo). On large books this was an O(spineItems × tocEntries) main-thread pass whose result was mostly discarded. Replace it with createStaticChaptersResolver, which resolves on demand and memoizes per spine-item id, so the cost is proportional to the items the reader visits. Each resolution also drops an O(spineItems) findIndex by reusing a prebuilt href → index map. Output is identical, including the duplicate-href / duplicate-id (last-write-wins) edge cases. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014NrSMiUDLXhYPYar2dg3KP
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
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.
Target
Subsystem: pagination → chapter-info tracking (
packages/core/src/enhancers/pagination).When a book is opened,
trackPaginationInfobuildschaptersDataonce. Part of that wasbuildStaticChaptersInfo(manifest, tocIndex), which eagerly resolved the href-based fallback chapter info for every spine item and stored it in a{ [id]: ChapterInfo }record.That record is only ever read for the handful of spine items actually on screen (
mapChapterInforeadschaptersInfo[beginItem.id]/[endItem.id], and only as a fallback when the DOM-based resolver returns nothing). On a large book (hundreds → 1000+ spine items) this is a chunk of main-thread work done at open whose result is mostly thrown away — it delays the first pagination/chapter readout with no user-visible payoff.Changes
Mechanism (one sentence): resolve the fallback chapter info on demand and memoize per spine-item id instead of eagerly for the whole spine, so the work becomes proportional to the items the reader visits rather than to book size.
buildStaticChaptersInfo→createStaticChaptersResolver: a resolver with aget(spineItemId)that resolves lazily and caches the result (including cachedundefined).O(spineItems × tocEntries)at open; lazy cost isO(visitedItems × tocEntries)spread across navigation. Unvisited items (the majority on a large book) cost nothing.findChapterChainByHref, replace the per-callmanifest.spineItems.findIndex(...)with a lookup into a prebuilthref → firstIndexmap (getSpineItemIndexByHref, already used bybuildTocIndex).O(spineItems)scan from each resolution (an accidentalO(spineItems²)pass across the old eager build).Behavior is unchanged: same
ChapterInfofor the same input, first-occurrence-wins for duplicate hrefs (matchesfindIndex) and last-write-wins for duplicate spine-item ids (matches the oldrecord[id] = …assignment).createStaticChaptersResolver/StaticChaptersResolverare internal to the pagination enhancer (not part of the package's public entry point and not referenced ingitbook/), so no public surface or docs change is required — checked.Impact (measured)
Micro-benchmark of the resolver logic at realistic scale, simulating "open a book, then read N distinct chapters (the rest never displayed), with a few page turns each". Old = eager-resolve all items at open; new = lazy + memoized. Output verified identical (including duplicate-href and duplicate-id cases).
The saved work is the fallback resolution for every spine item the reader never opens — which on a large book is nearly all of them. Small books (few spine items) are unaffected either way.
Gates
Run with the deps installed for this repo. Note: the pinned Node (v25, per
.nvmrc) could not be provisioned in this environment, and the@prose-reader/cfipackage's nativerolldown/vitebuild fails here as a result — a pre-existing, environment-only limitation confirmed on a clean checkout, unrelated to this diff. Given that:packages/core): full suite 186/186 pass (workspace deps resolved from source to work around the unbuilt@prose-reader/cfi), includingchapters.test.tsandtrackPaginationInfo.test.ts.tsc): the only errors are the 3 pre-existingCannot find module '@prose-reader/cfi'ones (unbuilt dep); zero new errors in the changed files.biome check): clean on all changed files (also enforced by the pre-commit hook).build/ CI on Node 25 will exercise the remaining gates.Backlog (found, not taken)
findChapterChainByHrefstill scans the wholetocIndexper resolution (O(tocEntries), dominated byisPossibleTocItemCandidateForHref'sendsWithchecks). The existingbuildTocCandidatesBySpineHrefpre-groups candidates by spine href, but it uses a different match predicate (hrefMatchesWithoutAnchor), so reusing it here would change chapter-resolution semantics — left alone deliberately.SpineItemsLoader(spine/loader) doesindexesToLoad.includes(index)inside a loop over all spine items; aSetmakes itO(1)per item. Only meaningful whennumberOfAdjacentSpineItemToPreLoadisInfinity(load-whole-book) — different subsystem, deferred.getSpineItemFromIframe(spine/locator) does anO(spineItems).findon every passed-through pointer event; a frame→item map would make itO(1). Hot path, but needs load/unload map maintenance — deferred.🤖 Generated with Claude Code
https://claude.ai/code/session_014NrSMiUDLXhYPYar2dg3KP
Generated by Claude Code