Skip to content

Fix: reduce N+1 backend requests triggered by RSC prefetch#4888

Merged
hlbmtc merged 7 commits into
mainfrom
fix/question-metadata-prefetch-cache
Jun 17, 2026
Merged

Fix: reduce N+1 backend requests triggered by RSC prefetch#4888
hlbmtc merged 7 commits into
mainfrom
fix/question-metadata-prefetch-cache

Conversation

@ncarazon

@ncarazon ncarazon commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Summary

Next.js 16 changed RSC prefetching behavior: it calls generateMetadata() for every visible <Link> on a page during server-side rendering, causing an N+1 request pattern on the main feed – 12–20 individual /api/posts/{id}/ requests to Django on every page load.

Two complementary fixes were applied to eliminate this N+1:

  • Introduced a lightweight cached metadata fetch path (with_cp=false + revalidate: 60) used by generateMetadata across question, tournament, and community pages, so repeated prefetch triggers within 60s are served from Next.js server-side cache without hitting Django
  • Added prefetch={false} to all <Link> components visible in feeds and post cards to prevent Next.js from triggering RSC prefetch for every visible link in the viewport

Note: KeyFactorsTileView was investigated as a source of additional with_cp=true requests – these are legitimate client-side fetches to render the "% chance" label when CP data is absent from the coherence links response, not part of the N+1 pattern

Summary by CodeRabbit

  • New Features

    • Added support for conditionally including profile statistics, and updated profile pages to request the enriched stats when needed.
  • Chores

    • Improved metadata freshness by applying explicit 60-second revalidation for tournament, community, post, notebook, and prediction-flow metadata (including question-page metadata).
    • Standardized metadata/post fetching options and added cached metadata fetch for question pages.
    • Reduced unnecessary link prefetching across many cards and navigation elements by introducing a configurable prefetch behavior, with small related rendering/timestamp cleanups.

@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR optimizes Next.js metadata caching and link prefetching across frontend pages and components: it adds explicit 60-second ISR revalidation to page metadata generation functions, extends API methods to accept fetch options, disables automatic link prefetching across 20+ components, and introduces a dedicated metadata-focused post fetcher. The backend user profile API now conditionally fetches statistics based on a query parameter, with corresponding frontend type hierarchy updates.

Changes

Frontend: ISR Revalidation and Link Prefetch Optimization

Layer / File(s) Summary
API layer: fetch options support
src/services/api/posts/posts.shared.ts, src/services/api/projects/projects.shared.ts
PostsApi.getPost and ProjectsApi methods (getTournament, getCommunity) now accept optional FetchOptions parameter to enable callers to pass Next.js revalidation config like { next: { revalidate: 60 } }. getTournaments also sets revalidate: 60 inline.
Metadata-focused post fetcher with revalidation
src/app/(main)/questions/[id]/[[...slug]]/utils/get_post.ts
New getPostForMetadata helper fetches posts without customer context and with 60-second revalidation; exported cachedGetPostForMetadata memoizes these requests via React cache.
Page metadata generation with ISR setup
src/app/(main)/(tournaments)/tournament/[slug]/page.tsx, src/app/(main)/c/[slug]/page.tsx, src/app/(main)/c/[slug]/[id]/[[...postSlug]]/page.tsx, src/app/(main)/notebooks/[id]/[[...slug]]/page.tsx, src/app/(main)/questions/[id]/[[...slug]]/page.tsx, src/app/(prediction-flow)/tournament/[slug]/prediction-flow/page.tsx
Multiple generateMetadata functions updated to fetch data with 60-second revalidation; questions page now uses dedicated cachedGetPostForMetadata for metadata.
UI component prefetch prop support
src/components/ui/button.tsx, src/components/post_card/basic_post_card/comment_status.tsx
Button and CommentStatus components extended with optional prefetch prop that flows through to underlying next/link.
Question page components: disable prefetch
src/app/(main)/questions/[id]/components/key_factors/..., src/app/(main)/questions/[id]/components/sidebar/similar_questions/similar_question_card.tsx, src/app/(main)/questions/components/coherence_links/display_coherence_link.tsx
Links in question key factors, similar questions, and coherence links now set prefetch={false}.
Card and feed components: disable prefetch
src/components/comment_feed/..., src/components/communities_feed/community_feed_card.tsx, src/components/conditional_tile/conditional_card.tsx, src/components/consumer_post_card/basic_consumer_post_card.tsx, src/components/forecast_card.tsx, src/components/news_card.tsx, src/components/post_card/..., src/components/post_default_project.tsx
Post card variants and feed cards disable prefetch on navigation links via prefetch={false}.
Tournament tile: stable time calculation and prefetch
src/components/posts_feed/feed_tournament_tile.tsx
FeedTournamentTile captures timestamp once per render and reuses for consistent time-based calculations; Link prefetch disabled; getStatusLabel refactored to accept now parameter.

Backend: User Profile Statistics and Type Refactoring

Layer / File(s) Summary
User profile API: conditional statistics
users/views.py
user_profile_api_view parses include_stats query parameter and conditionally applies serialize_user_stats to profile response only when requested.
Profile API overloads and searchUsers type update
src/services/api/profile/profile.shared.ts
ProfileApi.getProfileById expanded with overloaded signatures controlled by includeStats option; searchUsers now returns User[] instead of UserProfile[].
Profile pages request statistics
src/app/(main)/accounts/profile/[id]/(overview)/page.tsx, src/app/(main)/accounts/profile/[id]/layout.tsx, src/app/(main)/accounts/profile/[id]/track-record/page.tsx
MedalsPage, ProfileLayout, and TrackRecord page now call getProfileById with includeStats: true.
User type hierarchy and component updates
src/app/(main)/accounts/profile/components/*, src/services/api/profile/profile.shared.ts, src/types/projects.ts, src/types/users.ts
spam_count field moved from UserProfileStats into UserProfile; ProfileMenu, SocialMediaSection, UserInfo, and TournamentMember components updated for UserProfileWithStats and User types.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • Metaculus/metaculus#4083: Both PRs modify generateMetadata in tournament prediction-flow page to fetch tournament for title generation, with this PR adding explicit { next: { revalidate: 60 } } caching.
  • Metaculus/metaculus#4586: Both PRs modify key_factor_tile_view.tsx Link behavior; this PR disables prefetch while the retrieved PR adjusts navigation handling.
  • Metaculus/metaculus#4339: This PR's updates to feed_tournament_tile.tsx (refactoring getStatusLabel and disabling prefetch) build on tournament tiles feed feature from the retrieved PR.

Suggested reviewers

  • elisescu
  • cemreinanc
  • hlbmtc

Poem

🐰 Hops through links with caution light,
Prefetching halted, bandwidth tight,
Metadata cached for sixty beats,
Each page revalidates, oh what treats!
Stats arrive on demand, no waste in sight!

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the main objective of the PR: reducing N+1 backend requests triggered by RSC prefetch behavior in Next.js 16.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/question-metadata-prefetch-cache

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@front_end/src/app/`(main)/questions/[id]/[[...slug]]/utils/get_post.ts:
- Around line 52-54: getPostForMetadata currently calls ServerPostsApi.getPost
directly which bypasses getPost()’s legacy recovery (legacyGetPostId +
permanentRedirect) and causes NEXT_NOT_FOUND during generateMetadata; update
getPostForMetadata to use the existing getPost (or cachedGetPostForMetadata if
caching is needed) so legacyGetPostId() and permanentRedirect() paths are
preserved when generating metadata for page.tsx's generateMetadata.

In `@front_end/src/components/posts_feed/feed_tournament_tile.tsx`:
- Line 26: The current useMemo usage in feed_tournament_tile.tsx (const now =
useMemo(() => Date.now(), [])) freezes the timestamp for the component lifetime;
replace it with a plain per-render timestamp (const now = Date.now()) inside the
FeedTournamentTile component body so time-based labels update each render, and
remove the now useMemo import if it's no longer needed; update any related logic
that assumed a mount-only timestamp to use this per-render now.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 5f91fb13-8dec-44f5-813a-0375273c04bc

📥 Commits

Reviewing files that changed from the base of the PR and between 41be389 and fffa11a.

📒 Files selected for processing (27)
  • front_end/src/app/(main)/(tournaments)/tournament/[slug]/page.tsx
  • front_end/src/app/(main)/c/[slug]/[id]/[[...postSlug]]/page.tsx
  • front_end/src/app/(main)/c/[slug]/page.tsx
  • front_end/src/app/(main)/notebooks/[id]/[[...slug]]/page.tsx
  • front_end/src/app/(main)/questions/[id]/[[...slug]]/page.tsx
  • front_end/src/app/(main)/questions/[id]/[[...slug]]/utils/get_post.ts
  • front_end/src/app/(main)/questions/[id]/components/key_factors/item_view/question_link/question_link_key_factor_item.tsx
  • front_end/src/app/(main)/questions/[id]/components/key_factors/questions_feed_view/key_factor_tile_view.tsx
  • front_end/src/app/(main)/questions/[id]/components/sidebar/similar_questions/similar_question_card.tsx
  • front_end/src/app/(main)/questions/components/coherence_links/display_coherence_link.tsx
  • front_end/src/app/(prediction-flow)/tournament/[slug]/prediction-flow/page.tsx
  • front_end/src/components/comment_feed/comment_post_preview/compact_comment_post_card.tsx
  • front_end/src/components/communities_feed/community_feed_card.tsx
  • front_end/src/components/conditional_tile/conditional_card.tsx
  • front_end/src/components/consumer_post_card/basic_consumer_post_card.tsx
  • front_end/src/components/forecast_card.tsx
  • front_end/src/components/news_card.tsx
  • front_end/src/components/post_card/basic_post_card/comment_status.tsx
  • front_end/src/components/post_card/basic_post_card/index.tsx
  • front_end/src/components/post_card/basic_post_card/status_rail.tsx
  • front_end/src/components/post_card/community_disclaimer.tsx
  • front_end/src/components/post_card/compact_search_post_card.tsx
  • front_end/src/components/post_default_project.tsx
  • front_end/src/components/posts_feed/feed_tournament_tile.tsx
  • front_end/src/components/ui/button.tsx
  • front_end/src/services/api/posts/posts.shared.ts
  • front_end/src/services/api/projects/projects.shared.ts

Comment thread front_end/src/components/posts_feed/feed_tournament_tile.tsx Outdated
@github-actions

github-actions Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Cleanup: Preview Environment Removed

The preview environment for this PR has been destroyed.

Resource Status
🌐 Preview App Deleted
🗄️ PostgreSQL Branch Deleted
⚡ Redis Database Deleted
🔧 GitHub Deployments Removed
📦 Docker Image Retained (auto-cleanup via GHCR policies)

Cleanup triggered by PR close at 2026-06-17T17:40:50Z

@ncarazon ncarazon marked this pull request as ready for review June 12, 2026 16:40

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
front_end/src/app/(main)/accounts/profile/components/social_media_section.tsx (1)

90-90: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add noopener noreferrer for external links opened in a new tab.

Line 90 opens user-provided URLs with target="_blank" but omits rel="noopener noreferrer", which leaves a reverse-tabnabbing path.

Suggested patch
-            <Link href={link} target="_blank" rel="ugc">
+            <Link href={link} target="_blank" rel="ugc noopener noreferrer">
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@front_end/src/app/`(main)/accounts/profile/components/social_media_section.tsx
at line 90, The Link component in the social_media_section.tsx file that opens
user-provided URLs with target="_blank" currently only has rel="ugc" but is
missing the security attributes needed to prevent reverse-tabnabbing attacks.
Update the rel attribute on the Link element to include both "ugc" and "noopener
noreferrer" by combining them as a space-separated string in the rel prop.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In
`@front_end/src/app/`(main)/accounts/profile/components/social_media_section.tsx:
- Line 90: The Link component in the social_media_section.tsx file that opens
user-provided URLs with target="_blank" currently only has rel="ugc" but is
missing the security attributes needed to prevent reverse-tabnabbing attacks.
Update the rel attribute on the Link element to include both "ugc" and "noopener
noreferrer" by combining them as a space-separated string in the rel prop.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d472015a-8722-4633-b038-6d09f9570714

📥 Commits

Reviewing files that changed from the base of the PR and between 94aa1e7 and 5ace5fc.

📒 Files selected for processing (9)
  • front_end/src/app/(main)/accounts/profile/[id]/(overview)/page.tsx
  • front_end/src/app/(main)/accounts/profile/[id]/layout.tsx
  • front_end/src/app/(main)/accounts/profile/[id]/track-record/page.tsx
  • front_end/src/app/(main)/accounts/profile/components/profile_menu.tsx
  • front_end/src/app/(main)/accounts/profile/components/social_media_section.tsx
  • front_end/src/app/(main)/accounts/profile/components/user_info.tsx
  • front_end/src/services/api/profile/profile.shared.ts
  • front_end/src/types/projects.ts
  • front_end/src/types/users.ts

@hlbmtc hlbmtc deployed to testing_env June 17, 2026 17:22 — with GitHub Actions Active

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
front_end/src/services/api/profile/profile.shared.ts (1)

27-30: ⚡ Quick win

Align this.get generic with the overload contract.

At Line 27, this.get<UserProfileWithStats>(...) overstates the response shape when includeStats is false. Use the union type here so implementation typing matches the overload behavior.

Proposed change
-    return await this.get<UserProfileWithStats>(
+    return await this.get<UserProfile | UserProfileWithStats>(
       `/users/${id}/${encodeQueryParams({ include_stats: includeStats })}`,
       fetchOptions
     );
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@front_end/src/services/api/profile/profile.shared.ts` around lines 27 - 30,
The generic type argument in the this.get call on line 27 is always set to
UserProfileWithStats, but this overstates the response shape when includeStats
is false. Change the generic type from UserProfileWithStats to a union type that
accounts for both response shapes (UserProfile | UserProfileWithStats or similar
union based on your overload contract) so the typing accurately reflects what
the API actually returns based on the includeStats parameter value.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@front_end/src/services/api/profile/profile.shared.ts`:
- Around line 27-30: The generic type argument in the this.get call on line 27
is always set to UserProfileWithStats, but this overstates the response shape
when includeStats is false. Change the generic type from UserProfileWithStats to
a union type that accounts for both response shapes (UserProfile |
UserProfileWithStats or similar union based on your overload contract) so the
typing accurately reflects what the API actually returns based on the
includeStats parameter value.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: bfb4f5d6-6b2c-4f67-804c-2d578f4014b0

📥 Commits

Reviewing files that changed from the base of the PR and between 5ace5fc and 8961d84.

📒 Files selected for processing (3)
  • front_end/src/app/(main)/accounts/profile/[id]/layout.tsx
  • front_end/src/app/(main)/accounts/profile/components/social_media_section.tsx
  • front_end/src/services/api/profile/profile.shared.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • front_end/src/app/(main)/accounts/profile/components/social_media_section.tsx

@hlbmtc hlbmtc merged commit e80aa70 into main Jun 17, 2026
17 checks passed
@hlbmtc hlbmtc deleted the fix/question-metadata-prefetch-cache branch June 17, 2026 17:40
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.

2 participants