Skip to content

perf: stop shipping full blog post content to every client bundle#1025

Merged
LadyBluenotes merged 5 commits into
mainfrom
perf/split-blog-content-bundle
Jul 2, 2026
Merged

perf: stop shipping full blog post content to every client bundle#1025
LadyBluenotes merged 5 commits into
mainfrom
perf/split-blog-content-bundle

Conversation

@LadyBluenotes

@LadyBluenotes LadyBluenotes commented Jul 2, 2026

Copy link
Copy Markdown
Member

Problem

src/utils/blog.ts imported allPosts from content-collections at module top level. That generated module inlines the full markdown body of all 56 blog posts. Two components (CategoryArticle for /stack/* pages, and the per-library /docs/blog route) called blog-data functions directly during render with no server boundary, so Rollup hoisted the whole thing into the shared entry chunk — referenced by ~190 of ~200 client chunks, i.e. almost every page on the site.

Measured before this change:

  • Shared entry chunk: 1.3MB raw / 432KB gzip
  • Of that, the allPosts array literal alone: 650KB raw / ~211KB gzip (47% of the entry chunk), shipped to every visitor on every page

Fix

  1. Split src/utils/blog.ts into pure formatters (blog-format.ts, no content-collections import) and data functions that still read allPosts (blog.ts). No behavior change, no leak severed yet.
  2. Added fetchRelatedPostsForLibraries, a createServerFn for /stack/$category that replicates the original client-side flatMap(...).slice(0, 4) logic server-side, preserving exact post order/cutoff. CategoryArticle now reconstructs {post, lib} pairs from loader data + the already-in-memory libraries array instead of calling getPostsForLibrary in render.
  3. Added fetchPostsForLibrary, a wider 7-field createServerFn (matching blog.index.tsx's existing fetchFrontMatters shape) for /docs/blog, which needs authors, headerImage, and library in addition to slug/title/published/excerpt. Added a route loader with staleTime: Infinity (matching blog.index.tsx precedent).

Result (measured via fresh pnpm build)

  • Shared entry chunk: 1.3MB → 728KB raw, 432KB → 225KB gzip (~48% gzip reduction)
  • Full blog markdown content confirmed absent from every file in dist/client/ (was previously present); it now lives only in dist/server/assets/blog-*.js (651KB, server-only, never shipped to the browser)
  • Every other page on the site (docs, home, libraries, etc.) sheds ~207KB gzip of dead weight it never needed

Risk

Low. No UI/design changes — same posts, same order, same badges on every affected page. The only behavior change is where the data is computed (server loader vs. client render), which is standard TanStack Start loader/server-fn usage already used elsewhere in this codebase (blog.index.tsx).

Summary by CodeRabbit

  • New Features

    • Category pages now show related posts more reliably, using data loaded for the page instead of rebuilding it in the browser.
    • Library blog pages now load posts directly as part of the page data, improving consistency when viewing and filtering posts.
  • Bug Fixes

    • Standardized blog author and date formatting across blog cards, recent posts, social proof, RSS, and blog pages.
    • Updated published-date handling to use consistent UTC-based formatting.

@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

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

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
tanstack-com 6eff051 Commit Preview URL

Branch Preview URL
Jul 02 2026, 06:01 AM

@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Blog formatting utilities (author list formatting, date formatting/parsing, library lookup, distinct authors) are extracted into a new blog-format.ts module. Consumers update imports accordingly. Stack category and library blog routes now fetch related/blog posts via new server functions instead of client-side computation.

Changes

Blog utilities refactor and related-posts server fetching

Layer / File(s) Summary
New blog-format module
src/utils/blog-format.ts
Adds formatAuthors, formatPublishedDate, isPublishedDateReleased, publishedDateToUTCString, getBlogLibraries, and getDistinctAuthors exports.
blog.ts refactored
src/utils/blog.ts
Removes local getBlogLibraries implementation and imports it plus isPublishedDateReleased from blog-format.
Server functions for posts
src/utils/blog.functions.ts
Adds fetchRelatedPostsForLibraries and fetchPostsForLibrary server functions with RelatedPost/LibraryBlogPost types; updates import sources.
CategoryArticle server-provided related posts
src/components/stack/CategoryArticle.tsx
Accepts relatedPosts prop, reconstructs post/library pairs via reconstructRelatedPosts instead of client-side fetching.
Stack category route loader
src/routes/stack.$category.tsx
Loader becomes async, fetches related posts via server function, adds staleTime: Infinity, passes relatedPosts to CategoryArticle.
Library blog docs route loader
src/routes/_library/$libraryId/$version.docs.blog.tsx
Uses fetchPostsForLibrary loader with staleTime: Infinity, reads posts via Route.useLoaderData().
Import updates
src/components/BlogCard.tsx, src/components/RecentPostsWidget.tsx, src/components/home/HomeSocialProofSection.tsx, src/routes/blog.$.tsx, src/routes/blog.index.tsx, src/routes/rss[.]xml.ts
Updates imports of formatting helpers to source from ~/utils/blog-format instead of ~/utils/blog.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant StackCategoryPage as stack.$category route
  participant FetchRelatedPosts as fetchRelatedPostsForLibraries
  participant BlogUtils as getPostsForLibrary
  participant CategoryArticle

  StackCategoryPage->>FetchRelatedPosts: request related posts for category library ids
  FetchRelatedPosts->>BlogUtils: getPostsForLibrary per library id
  BlogUtils-->>FetchRelatedPosts: published posts
  FetchRelatedPosts-->>StackCategoryPage: sliced RelatedPost[] (max 4)
  StackCategoryPage->>CategoryArticle: render with slug and relatedPosts
  CategoryArticle->>CategoryArticle: reconstructRelatedPosts(libraries, relatedPosts)
Loading

Possibly related PRs

  • TanStack/tanstack.com#911: Uses BlogCard with the same author/date formatting helpers now moved to ~/utils/blog-format.
  • TanStack/tanstack.com#935: Introduces CategoryArticle//stack/$category related-posts rendering that this PR modifies to use server-fetched data.

Suggested reviewers: KevinVandy

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main performance change: moving full blog post content out of client bundles.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
✨ 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 perf/split-blog-content-bundle

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 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 `@src/utils/blog-format.ts`:
- Around line 60-70: The author sorting in getDistinctAuthors uses localeCompare
without a fixed locale, so the order can vary between SSR and hydration. Update
the sort callback in getDistinctAuthors to use the same pinned locale approach
as formatAuthors by passing a fixed locale like en-US to localeCompare, keeping
author ordering deterministic across runtimes.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: c301cd08-24bb-4c3f-a95b-48525cd291fd

📥 Commits

Reviewing files that changed from the base of the PR and between 616b229 and 6eff051.

📒 Files selected for processing (12)
  • src/components/BlogCard.tsx
  • src/components/RecentPostsWidget.tsx
  • src/components/home/HomeSocialProofSection.tsx
  • src/components/stack/CategoryArticle.tsx
  • src/routes/_library/$libraryId/$version.docs.blog.tsx
  • src/routes/blog.$.tsx
  • src/routes/blog.index.tsx
  • src/routes/rss[.]xml.ts
  • src/routes/stack.$category.tsx
  • src/utils/blog-format.ts
  • src/utils/blog.functions.ts
  • src/utils/blog.ts

Comment thread src/utils/blog-format.ts
Comment on lines +60 to +70
export function getDistinctAuthors(
posts: ReadonlyArray<{ authors: string[] }>,
): string[] {
const authors = new Set<string>()
for (const post of posts) {
for (const author of post.authors) {
authors.add(author)
}
}
return [...authors].sort((a, b) => a.localeCompare(b))
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Pin locale for deterministic sort order.

localeCompare without an explicit locale depends on the runtime's default locale, which can differ between server (SSR) and client (hydration), causing non-deterministic author ordering and potential hydration mismatches. formatAuthors in this same file already pins locale ('en-US') for this reason — apply the same here.

🌐 Proposed fix
-  return [...authors].sort((a, b) => a.localeCompare(b))
+  return [...authors].sort((a, b) => a.localeCompare(b, 'en-US'))
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function getDistinctAuthors(
posts: ReadonlyArray<{ authors: string[] }>,
): string[] {
const authors = new Set<string>()
for (const post of posts) {
for (const author of post.authors) {
authors.add(author)
}
}
return [...authors].sort((a, b) => a.localeCompare(b))
}
export function getDistinctAuthors(
posts: ReadonlyArray<{ authors: string[] }>,
): string[] {
const authors = new Set<string>()
for (const post of posts) {
for (const author of post.authors) {
authors.add(author)
}
}
return [...authors].sort((a, b) => a.localeCompare(b, 'en-US'))
}
🤖 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 `@src/utils/blog-format.ts` around lines 60 - 70, The author sorting in
getDistinctAuthors uses localeCompare without a fixed locale, so the order can
vary between SSR and hydration. Update the sort callback in getDistinctAuthors
to use the same pinned locale approach as formatAuthors by passing a fixed
locale like en-US to localeCompare, keeping author ordering deterministic across
runtimes.

@LadyBluenotes LadyBluenotes merged commit d17bbc7 into main Jul 2, 2026
7 checks passed
@LadyBluenotes LadyBluenotes deleted the perf/split-blog-content-bundle branch July 2, 2026 15:16
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