-
Notifications
You must be signed in to change notification settings - Fork 1.1k
perf(core): eagerly prefetch site-global layout data on public pages #1509
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "emdash": patch | ||
| --- | ||
|
|
||
| Speeds up public page loads on remote databases (D1, Durable Objects) by eagerly warming site-global layout data (menus, widget areas, taxonomy term lists, settings) at the start of the request, so the layout's per-component reads overlap into roughly one round trip instead of executing serially. Transparent to site code; no template changes needed. |
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,77 @@ | ||
| /** | ||
| * Eager, transparent prefetch of site-global "chrome" data. | ||
| * | ||
| * On a public page render, the shared layout pulls the same site-global data on | ||
| * every request -- menus, widget areas, taxonomy term lists, site settings -- | ||
| * but each is awaited inside a separately-rendered Astro component, so they | ||
| * execute as serial DB round trips. This fires them all CONCURRENTLY at the | ||
| * very start of the request, before `next()`: | ||
| * | ||
| * - On remote backends (D1, Durable Objects) the round trips overlap instead | ||
| * of serializing, collapsing ~N sequential RTTs into ~1 wall-clock RTT. On | ||
| * a coalescing backend they additionally batch into a single round trip. | ||
| * - The results land in the per-request `requestCached` store under the exact | ||
| * keys the layout helpers use, so when the components render they hit a warm | ||
| * (in-flight or resolved) cache entry instead of issuing their own query. | ||
| * | ||
| * Nothing here changes what templates call -- it warms the real helpers, so the | ||
| * cache keys and value shapes are guaranteed identical. The caller gates this to | ||
| * the public-page path on a request-scoped (remote) backend; it is a no-op-ish | ||
| * waste on synchronous local SQLite, so don't call it there. | ||
| * | ||
| * Fire-and-forget: never awaited by middleware, never throws (a prefetch failure | ||
| * must not affect the request -- the helpers will simply run on demand). | ||
| */ | ||
|
|
||
| import { getDb } from "../loader.js"; | ||
| import { getMenu } from "../menus/index.js"; | ||
| import { setRequestCacheEntry } from "../request-cache.js"; | ||
| import { getSiteSettings } from "../settings/index.js"; | ||
| import { getTaxonomyDefs, getTaxonomyTerms } from "../taxonomies/index.js"; | ||
| import { getWidgetAreas } from "../widgets/index.js"; | ||
|
|
||
| /** Warm widget areas: one bulk load, primed under each per-area cache key. */ | ||
| async function prefetchWidgetAreas(): Promise<void> { | ||
| const areas = await getWidgetAreas(); | ||
| // getWidgetArea(name) caches under `widget-area:${name}` and returns the same | ||
| // WidgetArea shape getWidgetAreas yields, so priming here makes those calls hit. | ||
| for (const area of areas) { | ||
| setRequestCacheEntry(`widget-area:${area.name}`, area); | ||
| } | ||
| } | ||
|
|
||
| /** Warm every taxonomy's term list via the real helper (primes per-name keys). */ | ||
| async function prefetchTaxonomyTerms(): Promise<void> { | ||
| const defs = await getTaxonomyDefs(); | ||
| await Promise.allSettled(defs.map((def) => getTaxonomyTerms(def.name))); | ||
| } | ||
|
|
||
| /** Warm every menu via the real helper (primes `menu:${name}:${locale}`). */ | ||
| async function prefetchMenus(): Promise<void> { | ||
| const db = await getDb(); | ||
| // The layout calls getMenu(name) with hardcoded names; we can't know them, so | ||
| // discover every menu name and warm them all (small, bounded chrome table). | ||
| const rows = await db.selectFrom("_emdash_menus").select("name").distinct().execute(); | ||
| const names = [...new Set(rows.map((r) => r.name))]; | ||
| await Promise.allSettled(names.map((name) => getMenu(name))); | ||
| } | ||
|
|
||
| /** | ||
| * Concurrently warm the site-global layout data for the current request. | ||
| * Safe to call only inside the request ALS frame that owns the (remote) | ||
| * request-scoped db. Never throws. | ||
| */ | ||
| export async function prefetchLayoutData(): Promise<void> { | ||
| try { | ||
| await Promise.allSettled([ | ||
| getSiteSettings(), | ||
| prefetchMenus(), | ||
| prefetchWidgetAreas(), | ||
| prefetchTaxonomyTerms(), | ||
| ]); | ||
| } catch (error) { | ||
| // Defensive: Promise.allSettled shouldn't reject, but never let a prefetch | ||
| // failure surface to the request. | ||
| console.error("[emdash] layout prefetch failed (non-fatal):", error); | ||
| } | ||
| } | ||
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
Oops, something went wrong.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[suggestion] This
SELECT DISTINCT name FROM _emdash_menusruns on every anonymous HTML request and isawaited before the per-menugetMenu(name)calls, so on a non-coalescing D1 session it adds a serial round trip that partially offsets the overlap win this PR is after (on a coalescing backend it batches for free, which is the better case). Menu names change only when an admin creates/renames/deletes a menu, so this is a good candidate for an isolate-scoped cache mirroringgetSiteSettings()'sSITE_SETTINGS_CACHE_KEY/invalidateSiteSettingsCache()(settings/index.ts), invalidated from the menu write paths. After the first request per isolate the discovery would be a free cache hit instead of a per-request query. Not a correctness issue — the prefetch is correct as written — just the one per-request cost worth eliminating if the measurement shows the discovery RTT matters.