Skip to content

feat: defer member/team registries on detail routes (#156)#185

Merged
wab merged 4 commits into
mainfrom
feat/defer-detail-loaders
May 15, 2026
Merged

feat: defer member/team registries on detail routes (#156)#185
wab merged 4 commits into
mainfrom
feat/defer-detail-loaders

Conversation

@wab

@wab wab commented May 15, 2026

Copy link
Copy Markdown
Contributor

Closes #156.

Summary

Move non-critical loader work off the blocking await on the three detail routes (blog, jobs, clients). Primary content (<h1>, body, hero, sidebar) paints first; member registry resolution streams in afterwards via <Suspense> + <Await>.

Per-route scope

Route Deferred Kept sync (why)
blog/$slug author footer (member registry) article, toc, intro
($lang)/jobs/$slug hiring contact footer article, sections, JSON-LD (crawlers need it on first paint)
($lang)/clients/$slug team footer article, resolvedTools (above-the-fold sidebar)

Changes

  • app/routes/_main.blog.$slug.tsxloadMemberRegistry() no longer awaited; author returned as Promise<Member | null>.
  • app/components/blog/blog-article.tsx — author footer wrapped in <Suspense fallback={null}><Await>.
  • app/routes/_main.($lang).jobs.$slug.tsx — same pattern for contact. ld (JSON-LD) stays sync.
  • app/routes/_main.($lang).clients.$slug.tsxloadMemberRegistry() deferred for resolvedTeam. loadToolRegistry() stays sync (sidebar metas).
  • app/components/stories/story-article.tsxStoryTeamBlock wrapped in <Suspense>.

Tests

  • app/routes/_main.blog.$slug.test.ts — happy-path split into "primaries sync" + "author is a Promise resolving to expected value".
  • app/routes/_main.($lang).jobs.$slug.test.ts — new test asserting contact is a Promise; ld stays sync.
  • app/routes/_main.($lang).clients.$slug.test.tsnew file, 4 cases: 404 (no slug), 404 (fetch failure), sync primaries, deferred team.

All 414 tests pass (pnpm test:run). Typecheck + Biome clean.

Measurement plan

Single-run lab PSI is too noisy to credit a defer-pattern win (we showed ±30-50 % LCP variance across consecutive runs on this site). The real signal lives in Vercel Speed Insights p75 INP / TBT once production traffic accumulates.

  • Re-check Vercel Speed Insights on the 3 detail routes ~7 days post-merge (≈ 2026-05-22)
  • Update docs/project/performance-baseline.md Changelog with delta vs 2026-05-13 site-wide RUM

Test plan

  • pnpm typecheck && pnpm check && pnpm test:run all green locally
  • CI green on PR
  • Manual smoke on preview deploy: blog detail, jobs detail, clients detail — verify footer (author / hiring contact / team) renders after primary content with no layout shift
  • Networktab: verify no waterfall regression on detail routes

wab added 3 commits May 15, 2026 11:42
loadMemberRegistry now resolves outside the loader's await, returned
as a Promise. BlogArticle wraps the author footer in <Suspense>+<Await>
so primary content (h1, body, toc) paints without waiting for the
member registry. Targets INP improvement per #156.
loadMemberRegistry resolves outside the loader's await for the jobs
detail route, returned as a Promise. The HiringContact footer block
is now wrapped in <Suspense>+<Await>. JSON-LD stays on the critical
path so crawlers receive the JobPosting schema on first paint.
loadMemberRegistry resolves outside the loader's await for client
story pages, returned as a Promise. StoryArticle wraps StoryTeamBlock
(footer) in <Suspense>+<Await>. resolvedTools stays on the critical
path because StoryMetas renders tool icons in the above-the-fold
sidebar.

Adds loader test coverage for the route — 4 cases: missing slug,
fetch failure, sync primaries, deferred team.
@vercel

vercel Bot commented May 15, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
ocobo Ready Ready Preview May 15, 2026 9:56am

H1 — restore loader concurrency: fire loadMemberRegistry() before
awaiting fetchBlogpost/fetchJob/fetchStory so the registry call no
longer serialises behind the primary fetch. Preserves the parallelism
of the original Promise.all without re-introducing the critical-path
await.

H2 — add errorElement={null} to all three <Await> boundaries
(blog-article, jobs route, story-article). Prevents a future-throwing
registry resolver from escalating to the root ErrorBoundary after
streaming has already started.
@wab

wab commented May 15, 2026

Copy link
Copy Markdown
Contributor Author

Code review feedback addressed in c3ef9c8.

H1 (blocking) — loader concurrency restored. Fire loadMemberRegistry() before awaiting the primary fetch in all 3 routes so the registry call overlaps with fetchBlogpost / fetchJob / fetchStory (matches the parallelism of the original Promise.all).

H2 (blocking) — errorElement={null} on all 3 <Await> boundaries. Prevents a future-throwing registry resolver from escalating to the root ErrorBoundary mid-stream.

Not addressed (low priority, can be follow-ups):

  • M1 — component-level RTL tests for the Suspense/Await wiring. Loader-level assertions cover the deferred contract; render tests would add belt-and-braces.
  • M2 — outcome.data as unknown as ... cast. Worth a separate invokeLoader<T> generic refactor across all loader tests, not in this PR.
  • N1 — resolveDeferred helper to dedupe the loadMemberRegistry().then(resolver) pattern. Optional, current spelling is clear.
  • N2 — explicit expect(data.article).not.toBeInstanceOf(Promise) assertions. Nice-to-have boundary lock; not load-bearing.

All 414 tests passing locally (pnpm test:run). Route tests specifically 40/40. Typecheck + Biome clean.

@wab wab merged commit 6c928c7 into main May 15, 2026
5 checks passed
@wab wab deleted the feat/defer-detail-loaders branch May 15, 2026 09:59
wab added a commit that referenced this pull request May 15, 2026
Captured via scripts/perf/baseline-run.sh against prod URLs. Same 6
routes as the 2026-05-14 baseline. Single-run lab data — high variance
across re-runs (±30-50% on LCP), so not a credible regression signal
on its own. Kept for trend reference once we accumulate medians or
shipped PRs to compare against (e.g. #156 deferred footers landed
between 2026-05-14 and now).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Perf: defer() metadata + related items on detail pages

1 participant