perf(core): cut redundant queries on content pages#1498
Conversation
- Cache getWidgetArea per request (it was the only content helper not request-cached). - Fetch taxonomy term usage-counts once per request via a shared request-cached aggregate, instead of re-running the full content_taxonomies GROUP BY for every taxonomy widget (Categories + Tags each ran it). - Make getTermsForEntries cache-aware: reuse already-hydrated per-entry terms from the request cache and only query the misses. Returns private copies so callers mutating the result can't poison the cache, and orders the miss path by label to match the hydration primer. All same-shape, all backends. Adds regression tests for cache reuse and mutation-safety. Updates the sqlite query-count snapshot (post route -1).
🦋 Changeset detectedLatest commit: da62330 The changes in this PR will be included in the next version bump. This PR includes changesets to release 14 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
docs | da62330 | Jun 16 2026, 07:34 AM |
🦋 Changeset detectedLatest commit: 8c6f889 The changes in this PR will be included in the next version bump. This PR includes changesets to release 14 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
emdash-demo-cache | da62330 | Jun 16 2026, 07:35 AM |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
emdash-playground | da62330 | Jun 16 2026, 07:36 AM |
Query-count snapshot changes20 routes changed, total Δ -24 queries. SQLite
D1
Comparing snapshot files between base and head. Updated automatically on each push. |
@emdash-cms/admin
@emdash-cms/auth
@emdash-cms/auth-atproto
@emdash-cms/blocks
@emdash-cms/cloudflare
@emdash-cms/contentful-to-portable-text
emdash
create-emdash
@emdash-cms/gutenberg-to-portable-text
@emdash-cms/plugin-cli
@emdash-cms/plugin-types
@emdash-cms/registry-client
@emdash-cms/registry-lexicons
@emdash-cms/sandbox-workerd
@emdash-cms/x402
@emdash-cms/plugin-ai-moderation
@emdash-cms/plugin-atproto
@emdash-cms/plugin-audit-log
@emdash-cms/plugin-color
@emdash-cms/plugin-embeds
@emdash-cms/plugin-field-kit
@emdash-cms/plugin-forms
@emdash-cms/plugin-webhook-notifier
commit: |
Overlapping PRsThis PR modifies files that are also changed by other open PRs:
This may cause merge conflicts or duplicated work. A maintainer will coordinate. |
|
/review |
|
Changeset is well-formed, user-facing, present tense. This PR is high-quality, narrow in scope, correctly tested, has performance evidence, and the author already self-reviewed adversarially. I cannot find a real bug. LGTM! |
What does this PR do?
Cuts redundant database queries when rendering content pages. All changes are same-shape, behavior-preserving, and help every backend (Node SQLite, D1, Postgres) — no template changes required.
Three fixes, found by tracing the query waves on a single-post route:
getWidgetAreaper request. It was the only content helper not wrapped inrequestCached, so a page rendering the same area twice re-queried.getTaxonomyTermsre-ran a fullcontent_taxonomies GROUP BY taxonomy_idscan for every taxonomy — so the Categories and Tags widgets each ran the same unscoped aggregate. The counts are locale-independent (keyed bytranslation_group), so they're now fetched once per request via a sharedrequestCachedentry.getTermsForEntriescache-aware. It always issued a batched query even when the per-entry terms were already hydrated and primed in the request cache (e.g. related-posts tags). It now reuses primed entries and only queries the misses. To preserve the previous contract it returns private copies (callers that mutate the result can't poison the shared cache) and orders the miss path by label to match the hydration primer.Verified with the query-count harness: the single-post fixture route drops 18 → 17 queries (the fixture under-exercises the widget paths; on a richer layout with both Categories+Tags widgets and related-posts term lookups the saving is larger). Updated
scripts/query-counts.snapshot.sqlite.json; the d1 snapshot will be refreshed by CI.This is the first ("Tier 1") slice of a broader warm-query-reduction effort; a follow-up will add a transparent middleware prefetch for site-global layout data (menus/widgets/taxonomies). Two further candidates were investigated and deliberately deferred: dropping the per-request byline-fields version read (cross-isolate consistency trade-off) and replacing the Archives widget's unbounded fetch with a
GROUP BYcount (changes the widget's data contract).Type of change
Checklist
pnpm typecheckpassespnpm lintpassespnpm testpasses (or targeted tests for my change) — fullemdashsuite (3934) passes; added 2 regression testspnpm formathas been runemdashpatch)AI-generated code disclosure
Screenshots / test output
Query-count harness (sqlite), single-post route:
db.count18 → 17. An adversarial review pass on thegetTermsForEntrieschange caught and fixed three regressions before this PR: shared-array aliasing (now returns private copies), inconsistent ordering between cache-hit and DB-miss entries (now both label-ordered), and a rejected peeked promise bypassing theisMissingTableErrorgraceful-degrade guard (rejections now fall back to the guarded query path). Regression tests added for cache reuse and mutation-safety.Try this PR
Open a fresh playground →
A full working EmDash site, deployed from this branch. Each visit gets its own session-scoped sandbox: no login needed and no shared state. Try the admin, edit content, hit the public site.
Tracks
perf/reduce-warm-queries. Updated automatically when the playground redeploys.