Cache stubs, prefetch next page, parallelize author profile fetch (AI)
resolveStub() now caches every resolved stub (not just ones that get hydrated), so a repeat visit skips the listing fetch for articles nothing has ever rendered too, not only already-viewed ones. Once a page's cards render, the next page's content hydrates in the background so "next" is usually an instant cache hit. loadAuthorView fetches the profile and article list in parallel instead of sequentially. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AXSVPS3UmmASiiprPfcnUd
This commit is contained in:
1 parent
81fb2a2414
commit
202f88b206
3 files changed
+38
-13
No files matched your search
@@ -360,12 +360,14 @@ for articles about to be rendered: the current page's cards (`hydrateArticles()`
|
||||
every result of a search. A large site's list is therefore fast to show regardless of how many
|
||||
articles it has — only ~20 articles' worth of content is ever fetched per page view.
|
||||
|
||||
- **Cache** — once an article is hydrated, the result is cached per post directory in IndexedDB
|
||||
(`cmjs/js/cache.js`), keyed by directory path and invalidated by comparing the live `mtime`
|
||||
against the cached one — a stub whose directory `mtime` hasn't changed reuses the cached,
|
||||
already-hydrated result directly, skipping both the listing and content fetch. If IndexedDB is
|
||||
unavailable (private browsing, disabled storage), discovery transparently falls back to
|
||||
fetching everything live on every visit.
|
||||
- **Cache** — every resolved stub is cached per post directory in IndexedDB (`cmjs/js/cache.js`),
|
||||
keyed by directory path and invalidated by comparing the live `mtime` against the cached one —
|
||||
as soon as that comparison matches, indexing reuses the cached entry directly and skips the
|
||||
listing fetch too, whether or not the article has ever actually been hydrated. Hydration
|
||||
re-caches the same entry with the fetched content added, so once *any* visit renders an
|
||||
article, every later visit skips its content fetch as well, until its directory's `mtime`
|
||||
changes. If IndexedDB is unavailable (private browsing, disabled storage), discovery
|
||||
transparently falls back to fetching everything live on every visit.
|
||||
- **Cache schema version** — the cached records store the full derived article shape, not raw
|
||||
content, so a code change to how articles are derived (in `cmjs/js/discovery.js`) can make old
|
||||
cached entries stale in *structure*, not just content — and a directory's `mtime` won't catch
|
||||
@@ -382,6 +384,9 @@ articles it has — only ~20 articles' worth of content is ever fetched per page
|
||||
also visits directories with the most recent `mtime` first (a best-effort heuristic, not the
|
||||
final display order — that's always the authoritative `.date` marker), so the first results
|
||||
tend to already be close to correct.
|
||||
- **Prefetching** — once the current page has fully rendered, the next page's articles are
|
||||
hydrated in the background (fire-and-forget), so clicking "next" is usually an instant cache
|
||||
hit rather than a fresh round-trip.
|
||||
- **Crawl health** — if every author's directory listing fails outright, or very few of the
|
||||
known post directories loaded, a small warning banner is shown above the article list instead
|
||||
of silently rendering an empty page (see `getCrawlStats()` / `renderCrawlHealthWarning()` in
|
||||
|
||||
@@ -205,13 +205,17 @@ async function buildStub(dir) {
|
||||
}
|
||||
}
|
||||
|
||||
// Cache-aware: a directory whose mtime matches its cached (already-hydrated)
|
||||
// entry is reused directly, skipping the listing fetch entirely; otherwise
|
||||
// builds a fresh, unhydrated stub from the listing alone.
|
||||
// Cache-aware: a directory whose mtime matches its cached entry (stub *or*
|
||||
// already-hydrated, either is fine here) is reused directly, skipping the
|
||||
// listing fetch entirely; otherwise builds a fresh stub from the listing
|
||||
// and caches that too, so a repeat visit skips the listing fetch even for
|
||||
// an article nothing has hydrated yet — not just already-viewed ones.
|
||||
async function resolveStub(dir) {
|
||||
const cached = await getCacheEntry(dir.root);
|
||||
if (cached && dir.mtime != null && cached.mtime === dir.mtime) return cached.article;
|
||||
return buildStub(dir);
|
||||
const stub = await buildStub(dir);
|
||||
if (stub) await setCacheEntry(dir.root, { mtime: dir.mtime, article: stub });
|
||||
return stub;
|
||||
}
|
||||
|
||||
// Fields only derivable from an article's actual markdown content (not its
|
||||
@@ -316,7 +320,7 @@ async function runCrawl() {
|
||||
const crawlOrder = [...slugDirs].sort((a, b) => mtimeSortValue(b) - mtimeSortValue(a));
|
||||
|
||||
// Wave 3 — per slug directory, cache-aware and concurrency-limited: a
|
||||
// directory whose mtime matches its cached (already-hydrated) entry
|
||||
// directory whose mtime matches its cached entry (stub or hydrated)
|
||||
// skips the listing fetch entirely; otherwise just its listing (for
|
||||
// .tag/.date/draft markers) — not its markdown content, which is
|
||||
// deferred to hydrateArticle() until something actually renders it.
|
||||
|
||||
+18
-2
@@ -276,6 +276,15 @@ export async function loadArticleList(filter = null, page = 1) {
|
||||
const pageArticles = pageStubs.length ? await hydrateArticles(pageStubs) : [];
|
||||
if (generation !== currentGeneration || myToken !== renderToken) return;
|
||||
renderArticleListBody(view, { total: articles.length, pageArticles, page: clampedPage, pages, filter, done });
|
||||
|
||||
// Once the list is fully indexed (not mid-background-crawl, where
|
||||
// page boundaries could still shift), warm the cache for the next
|
||||
// page so clicking "next" is instant. Fire-and-forget — it touches
|
||||
// no DOM, just fetches and caches, so it's safe to keep running
|
||||
// even if the user has since navigated elsewhere.
|
||||
if (done && clampedPage < pages) {
|
||||
hydrateArticles(articles.slice(clampedPage * PAGE_SIZE, (clampedPage + 1) * PAGE_SIZE));
|
||||
}
|
||||
};
|
||||
|
||||
const unsubscribe = onArticlesProgress((partialArticles, done) => { render(partialArticles, done); });
|
||||
@@ -321,8 +330,15 @@ export async function loadAuthorView(author, page = 1) {
|
||||
view.innerHTML = `<p class="loading">${t('loadingGeneric')}</p>`;
|
||||
clearSearchInput();
|
||||
try {
|
||||
const profile = await fetchAuthorProfile(author);
|
||||
await loadArticleList({ type: 'author', value: author }, page);
|
||||
// Independent of each other — fetching in parallel instead of
|
||||
// sequentially shaves a round-trip. Safe to run together:
|
||||
// fetchAuthorProfile() never touches the DOM, and the profile card
|
||||
// below is only inserted once both have settled, so there's
|
||||
// nothing for loadArticleList()'s own view updates to race with.
|
||||
const [profile] = await Promise.all([
|
||||
fetchAuthorProfile(author),
|
||||
loadArticleList({ type: 'author', value: author }, page),
|
||||
]);
|
||||
if (page === 1 && profile) view.insertBefore(renderAuthorProfile(profile), view.firstChild);
|
||||
} catch (error) { view.innerHTML = `<p class="error">${t('articlesLoadError', { message: error.message })}</p>`; }
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user