Skip to content

perf: cache getSiteSetting() per request + document perf patterns#663

Merged
ascorbic merged 1 commit into
mainfrom
cache-get-site-setting
Apr 19, 2026
Merged

perf: cache getSiteSetting() per request + document perf patterns#663
ascorbic merged 1 commit into
mainfrom
cache-get-site-setting

Conversation

@ascorbic
Copy link
Copy Markdown
Collaborator

What does this PR do?

Two things in one PR — a small fix and the docs update it should have come with the first time.

Fix: cache `getSiteSetting(key)` per request

`getSiteSetting` was firing an un-cached `options` table read on every call. PR #613 added a call from `EmDashHead.astro` (to surface `seo.googleVerification` / `seo.bingVerification` as meta tags), which means every page render now pays one extra D1 round-trip just for SEO settings.

On colos close to the primary (EUW/USE) that's ~10–20 ms. On APS/APE, where the primary is thousands of kilometres away, it's 30–100 ms of avoidable warm-render latency. Perf monitor data shows APS warm_mw stepped from ~90 ms → ~140–200 ms around the PR #613 deploy at 11:02 UTC today.

Wrapping each key in `requestCached("siteSetting:${key}", ...)` dedupes concurrent callers within a render. `getSiteSettings()` (plural) already uses the same pattern.

Docs: "Performance: caching and query patterns" in AGENTS.md

Captures what we've been rediscovering all day:

Type of change

  • Bug fix
  • Performance improvement
  • Documentation

Checklist

  • I have read CONTRIBUTING.md
  • `pnpm typecheck` passes
  • `pnpm lint` passes
  • `pnpm test` passes (2418 core tests)
  • `pnpm format` has been run
  • I have added a changeset

AI-generated code disclosure

  • This PR includes AI-generated code

getSiteSetting(key) was firing an options-table read on every call,
which meant EmDashHead's new per-render seo lookup (PR #613) added a
D1 round-trip to every page. On APS/APE colos that's 30-100ms of
avoidable warm-render latency. Wrapping in requestCached("siteSetting:
${key}", ...) dedupes concurrent callers and matches the pattern
getSiteSettings() already uses.

Also adds a "Performance: caching and query patterns" section to
AGENTS.md covering requestCached, the globalThis+Symbol singleton
pattern (bundler-safe), the anti-pattern of "has any" probes, after()
for deferring bookkeeping, and the query-count harness. Everything I
had to relearn today.
Copilot AI review requested due to automatic review settings April 19, 2026 14:34
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 19, 2026

🦋 Changeset detected

Latest commit: f479526

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 12 packages
Name Type
emdash Patch
@emdash-cms/cloudflare Patch
@emdash-cms/fixture-perf-site Patch
@emdash-cms/perf-demo-site Patch
@emdash-cms/cache-demo-site Patch
@emdash-cms/admin Patch
@emdash-cms/auth Patch
@emdash-cms/blocks Patch
@emdash-cms/gutenberg-to-portable-text Patch
@emdash-cms/x402 Patch
create-emdash Patch
@emdash-cms/plugin-embeds Patch

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

@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented Apr 19, 2026

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
emdash-demo-cache f479526 Apr 19 2026, 02:35 PM

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Apr 19, 2026

Open in StackBlitz

@emdash-cms/admin

npm i https://pkg.pr.new/@emdash-cms/admin@663

@emdash-cms/auth

npm i https://pkg.pr.new/@emdash-cms/auth@663

@emdash-cms/blocks

npm i https://pkg.pr.new/@emdash-cms/blocks@663

@emdash-cms/cloudflare

npm i https://pkg.pr.new/@emdash-cms/cloudflare@663

emdash

npm i https://pkg.pr.new/emdash@663

create-emdash

npm i https://pkg.pr.new/create-emdash@663

@emdash-cms/gutenberg-to-portable-text

npm i https://pkg.pr.new/@emdash-cms/gutenberg-to-portable-text@663

@emdash-cms/x402

npm i https://pkg.pr.new/@emdash-cms/x402@663

@emdash-cms/plugin-ai-moderation

npm i https://pkg.pr.new/@emdash-cms/plugin-ai-moderation@663

@emdash-cms/plugin-atproto

npm i https://pkg.pr.new/@emdash-cms/plugin-atproto@663

@emdash-cms/plugin-audit-log

npm i https://pkg.pr.new/@emdash-cms/plugin-audit-log@663

@emdash-cms/plugin-color

npm i https://pkg.pr.new/@emdash-cms/plugin-color@663

@emdash-cms/plugin-embeds

npm i https://pkg.pr.new/@emdash-cms/plugin-embeds@663

@emdash-cms/plugin-forms

npm i https://pkg.pr.new/@emdash-cms/plugin-forms@663

@emdash-cms/plugin-webhook-notifier

npm i https://pkg.pr.new/@emdash-cms/plugin-webhook-notifier@663

commit: f479526

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Improves render-path performance by deduplicating getSiteSetting(key) lookups within a single request, and adds internal documentation capturing common caching/query pitfalls and preferred patterns.

Changes:

  • Wrap getSiteSetting(key) in requestCached to avoid repeated options table reads per render.
  • Add a new “Performance: caching and query patterns” section to AGENTS.md.
  • Add a changeset documenting the per-request caching behavior change.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
packages/core/src/settings/index.ts Cache getSiteSetting(key) per-request via requestCached to reduce repeated DB reads.
AGENTS.md Document caching/query-count patterns and operational guidance for perf-sensitive helpers.
.changeset/cache-get-site-setting.md Patch changeset describing the performance fix and rationale.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread AGENTS.md

A few rules and patterns cover 90% of the footguns.

**Always add requestCached to query helpers called from templates.** Page-level template code runs inside the ALS request context, so the per-request cache (`src/request-cache.ts`) deduplicates identical calls within a single render. A single un-cached helper called from three widgets turns into three primary-routed reads on a page that should have made one. Rule of thumb: if a helper takes stable arguments (slug, key, entry ID) and can be called from multiple components, wrap it.
Copy link

Copilot AI Apr 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AGENTS.md refers to the per-request cache as src/request-cache.ts, but the implementation lives at packages/core/src/request-cache.ts (and is imported throughout core as ../request-cache.js). Updating the path here will make the guidance easier to follow.

Suggested change
**Always add requestCached to query helpers called from templates.** Page-level template code runs inside the ALS request context, so the per-request cache (`src/request-cache.ts`) deduplicates identical calls within a single render. A single un-cached helper called from three widgets turns into three primary-routed reads on a page that should have made one. Rule of thumb: if a helper takes stable arguments (slug, key, entry ID) and can be called from multiple components, wrap it.
**Always add requestCached to query helpers called from templates.** Page-level template code runs inside the ALS request context, so the per-request cache (`packages/core/src/request-cache.ts`) deduplicates identical calls within a single render. A single un-cached helper called from three widgets turns into three primary-routed reads on a page that should have made one. Rule of thumb: if a helper takes stable arguments (slug, key, entry ID) and can be called from multiple components, wrap it.

Copilot uses AI. Check for mistakes.
Comment thread AGENTS.md

`requestCached` caches the _promise_, so concurrent callers share the in-flight query. On error the entry is deleted so the next call retries.

**Module-scope singletons must live on `globalThis`.** Vite duplicates modules across chunks during SSR bundling. A plain `let cache: X | null = null` in a module becomes _two_ variables if two chunks inline the module -- defeating the singleton. Use a `Symbol.for` key on `globalThis`, as `request-context.ts` does. See also `packages/core/src/bylines/index.ts` (`bylinesHolder`) for the pattern applied to a boolean cache. The fix cut ~2 cold-start queries per D1 isolate.
Copy link

Copilot AI Apr 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This sentence points to packages/core/src/bylines/index.ts and mentions a bylinesHolder globalThis singleton pattern, but that file currently has no bylinesHolder (or any globalThis/Symbol.for singleton cache). Either update the reference to a file that actually demonstrates the pattern (e.g. packages/core/src/request-context.ts / packages/core/src/request-cache.ts) or remove the example so the docs don’t send readers on a dead end.

Suggested change
**Module-scope singletons must live on `globalThis`.** Vite duplicates modules across chunks during SSR bundling. A plain `let cache: X | null = null` in a module becomes _two_ variables if two chunks inline the module -- defeating the singleton. Use a `Symbol.for` key on `globalThis`, as `request-context.ts` does. See also `packages/core/src/bylines/index.ts` (`bylinesHolder`) for the pattern applied to a boolean cache. The fix cut ~2 cold-start queries per D1 isolate.
**Module-scope singletons must live on `globalThis`.** Vite duplicates modules across chunks during SSR bundling. A plain `let cache: X | null = null` in a module becomes _two_ variables if two chunks inline the module -- defeating the singleton. Use a `Symbol.for` key on `globalThis`, as `packages/core/src/request-context.ts` and `packages/core/src/request-cache.ts` do. The fix cut ~2 cold-start queries per D1 isolate.

Copilot uses AI. Check for mistakes.
@ascorbic ascorbic merged commit 38d637b into main Apr 19, 2026
38 checks passed
@ascorbic ascorbic deleted the cache-get-site-setting branch April 19, 2026 14:42
@emdashbot emdashbot Bot mentioned this pull request Apr 19, 2026
ascorbic added a commit that referenced this pull request Apr 19, 2026
Adds peekRequestCache() to request-cache.ts so a narrower query can
opportunistically satisfy itself from a broader one already loaded.

getSiteSetting(key) now first peeks for the "siteSettings" batch
result (populated by getSiteSettings()) and reads the requested key
from there if present. Falls back to a per-key cached query if the
batch hasn't been loaded.

Net effect on the blog-demo layout: getSiteSettings() is already
called by Base.astro, so the EmDashHead getSiteSetting("seo") call
from PR #613 now costs zero extra queries instead of one
primary-routed round-trip per render. Query-count snapshot is
unchanged — the +1 queries PR #663 couldn't dedupe are now gone.
ascorbic added a commit that referenced this pull request Apr 19, 2026
Adds peekRequestCache() to request-cache.ts so a narrower query can
opportunistically satisfy itself from a broader one already loaded.

getSiteSetting(key) now first peeks for the "siteSettings" batch
result (populated by getSiteSettings()) and reads the requested key
from there if present. Falls back to a per-key cached query if the
batch hasn't been loaded.

Net effect on the blog-demo layout: getSiteSettings() is already
called by Base.astro, so the EmDashHead getSiteSetting("seo") call
from PR #613 now costs zero extra queries instead of one
primary-routed round-trip per render. Query-count snapshot is
unchanged — the +1 queries PR #663 couldn't dedupe are now gone.
ascorbic added a commit that referenced this pull request Apr 19, 2026
* perf: getSiteSetting(key) piggybacks on cached getSiteSettings()

Adds peekRequestCache() to request-cache.ts so a narrower query can
opportunistically satisfy itself from a broader one already loaded.

getSiteSetting(key) now first peeks for the "siteSettings" batch
result (populated by getSiteSettings()) and reads the requested key
from there if present. Falls back to a per-key cached query if the
batch hasn't been loaded.

Net effect on the blog-demo layout: getSiteSettings() is already
called by Base.astro, so the EmDashHead getSiteSetting("seo") call
from PR #613 now costs zero extra queries instead of one
primary-routed round-trip per render. Query-count snapshot is
unchanged — the +1 queries PR #663 couldn't dedupe are now gone.

* add changeset
0aveRyan pushed a commit to 0aveRyan/emdash that referenced this pull request Apr 27, 2026
…dash-cms#663)

getSiteSetting(key) was firing an options-table read on every call,
which meant EmDashHead's new per-render seo lookup (PR emdash-cms#613) added a
D1 round-trip to every page. On APS/APE colos that's 30-100ms of
avoidable warm-render latency. Wrapping in requestCached("siteSetting:
${key}", ...) dedupes concurrent callers and matches the pattern
getSiteSettings() already uses.

Also adds a "Performance: caching and query patterns" section to
AGENTS.md covering requestCached, the globalThis+Symbol singleton
pattern (bundler-safe), the anti-pattern of "has any" probes, after()
for deferring bookkeeping, and the query-count harness. Everything I
had to relearn today.
0aveRyan pushed a commit to 0aveRyan/emdash that referenced this pull request Apr 27, 2026
…-cms#664)

* perf: getSiteSetting(key) piggybacks on cached getSiteSettings()

Adds peekRequestCache() to request-cache.ts so a narrower query can
opportunistically satisfy itself from a broader one already loaded.

getSiteSetting(key) now first peeks for the "siteSettings" batch
result (populated by getSiteSettings()) and reads the requested key
from there if present. Falls back to a per-key cached query if the
batch hasn't been loaded.

Net effect on the blog-demo layout: getSiteSettings() is already
called by Base.astro, so the EmDashHead getSiteSetting("seo") call
from PR emdash-cms#613 now costs zero extra queries instead of one
primary-routed round-trip per render. Query-count snapshot is
unchanged — the +1 queries PR emdash-cms#663 couldn't dedupe are now gone.

* add changeset
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants