Skip to content

ADR-012: Blog Performance Optimizations

Accepted

Note: This ADR documents the implementation of patterns defined in ADR 020: Page Performance Patterns. Refer to ADR 020 for comprehensive performance guidelines.

The initial blog implementation had several performance bottlenecks that would scale poorly as content grows:

  1. Redundant Sorting: Each blog post page fetched and sorted ALL posts independently (O(n log n) per page)
  2. Double Rendering: post.render() was called twice—once for content, once for headings
  3. Implicit Image Decoding: Cover images didn’t explicitly set decoding="async"
  4. Per-Page Metadata Calculation: Reading time calculated from raw markdown on every page

For a blog with 100 posts, this meant:

  • 100 separate sort operations (instead of 1)
  • 200 markdown parse operations (instead of 100)
  • Unnecessary blocking on image decode

Implement build-time performance optimizations following Astro’s static generation best practices:

1. Compute Navigation Once in getStaticPaths

Section titled “1. Compute Navigation Once in getStaticPaths”

Before:

// In [slug].astro page component
const allPosts = (await getCollection("blog")).sort(...);
const currentIndex = allPosts.findIndex((p) => p.slug === post.slug);
const prevPost = currentIndex > 0 ? allPosts[currentIndex - 1] : null;
const nextPost = currentIndex < allPosts.length - 1 ? allPosts[currentIndex + 1] : null;

After:

// In getStaticPaths (runs once)
export async function getStaticPaths() {
const posts = await getCollection("blog");
const sortedPosts = posts.sort(...);
return sortedPosts.map((post, index) => ({
params: { slug: post.slug },
props: {
post,
prevPost: index > 0 ? sortedPosts[index - 1] : null,
nextPost: index < sortedPosts.length - 1 ? sortedPosts[index + 1] : null,
},
}));
}

Impact: Reduces O(n²) to O(n log n) for entire build

Before:

// In [slug].astro
const { Content } = await post.render();
// In BlogLayout.astro
const { headings } = await post.render(); // DUPLICATE!

After:

// In [slug].astro
const { Content, headings } = await post.render();
// Pass to layout
<BlogLayout post={post} headings={headings}>
<Content />
</BlogLayout>
// In BlogLayout.astro - receive as prop
const { post, headings } = Astro.props;

Impact: Eliminates 50% of markdown parsing operations

Before:

<Image src={cover} loading="eager" />

After:

<Image src={cover} loading="eager" decoding="async" />

Impact: Prevents image decode from blocking main thread (LCP improvement)

Current: formatPostMetadata(date, post.body, updated) processes raw markdown per page

Decision: Keep as-is for now because:

  • Static builds = one-time cost
  • Only becomes bottleneck at 1000+ posts
  • Precomputing would require content schema changes or build hooks

Future: If blog exceeds 500 posts, consider:

  • Precomputing in content collection config
  • Caching in build hook
  • Storing in frontmatter
  1. Leverage Static Generation: Astro builds pages once—optimize the build, not runtime
  2. Minimal Code Changes: Works within existing architecture
  3. Measurable Impact: Each optimization has clear performance benefit
  4. Scalability: Handles 100s of posts efficiently; 1000s with future optimizations
MetricBeforeAfterImprovement
Sort operations (100 posts)100199% reduction
Markdown parses (100 posts)20010050% reduction
Image decode blockingImplicitAsyncLCP improvement

Alternative 1: Client-side navigation

// Fetch prev/next via API

❌ Breaks static generation, adds runtime overhead

Alternative 2: Precompute everything in content schema

// Store metadata in frontmatter

❌ Over-engineering for current scale, harder to maintain

Alternative 3: Cache sorted posts globally

// Use module-level cache

❌ Unnecessary complexity when getStaticPaths already runs once

  • Build Performance: 50-99% reduction in redundant operations
  • Scalability: Handles 100s of posts efficiently
  • Lighthouse Scores: Async decoding improves LCP
  • Maintainability: Cleaner separation of concerns (data prep in getStaticPaths)
  • Zero Runtime Cost: All optimizations are build-time only
  • Slightly more complex getStaticPaths (but clearer intent)
  • headings must be passed as prop (explicit dependency)
  • None identified for current scale (< 500 posts)
  • User Rules: ✅ Follows minimal, focused edits principle
  • User Rules: ✅ Leverages Astro’s static generation patterns
  • User Rules: ✅ Optimizes build performance without runtime overhead
  • Performance: ✅ Targets 95+ Lighthouse scores
  • Scalability: ✅ Handles growth to 100s of posts
  • Move sorting to getStaticPaths in /src/pages/blog/[slug].astro
  • Pass prevPost and nextPost as props
  • Render markdown once, pass headings to layout
  • Update BlogLayout.astro to accept headings prop
  • Add explicit decoding="async" to cover image
  • Monitor build times as content grows
  • Consider precomputing reading time if blog exceeds 500 posts
  • src/pages/blog/[slug].astro - Primary optimizations
  • src/pages/blog/index.astro - Uses centralized blog utilities
  • src/layouts/BlogLayout.astro - Receives headings as prop
  • src/utils/blog.ts - Centralized blog post queries and sorting (eliminates duplication)
  • src/utils/formatDate.ts - Reading time calculation (future optimization)
  1. Precompute Reading Time:

    // In content config or build hook
    const readingTime = estimateReadingTime(post.body);
  2. Pagination for Blog Index:

    // Already implemented in /blog/index.astro
  3. Incremental Builds:

    • Explore Astro’s experimental incremental static regeneration
    • Only rebuild changed posts

Track build times in CI/CD:

Terminal window
pnpm run build --verbose
# Monitor: "Generating static routes" duration
  • Not machine-checkable: the compute-in-getStaticPaths pattern is a review concern; aggregate regressions surface via the JS bundle budget and the Lighthouse workflow rather than a static check.
  • Graduation log: (empty at creation; entries added when a check changes status)

Date: 2025-10-01 (footer backfilled 2026-07-05 from git history; this record predates the footer convention)
Participants: Template maintainers
Outcome: Accepted