Skip to content

Rendering Modes ​

EreoJS supports multiple rendering modes, allowing you to choose the best approach for each route based on your requirements for performance, SEO, and interactivity.

Overview ​

ModeWhen RendersUse Case
SSREvery requestDynamic content, personalization
SSGBuild timeStatic content, blogs, docs
CSRClient-sideDashboards, authenticated apps
StreamingProgressiveLarge pages, slow data sources

Server-Side Rendering (SSR) ​

SSR renders the page on every request. The server fetches data, renders HTML, and sends the complete page to the browser.

tsx
// routes/posts/[id].tsx
export const config = {
  render: 'ssr'
}

export const loader = createLoader(async ({ params }) => {
  const post = await db.posts.find(params.id)
  return { post }
})

export default function Post({ loaderData }) {
  return <article>{loaderData.post.content}</article>
}

Benefits:

  • Always fresh content
  • Good for personalized or frequently changing data
  • SEO-friendly

Trade-offs:

  • Server compute on every request
  • Slower TTFB than SSG

When to Use SSR ​

  • User-specific content (dashboards, profiles)
  • Frequently updated data (news, stock prices)
  • Content that must be current (inventory, pricing)
  • Pages requiring authentication context

Static Site Generation (SSG) ​

SSG renders pages at build time. The HTML is generated once and served from a CDN for all subsequent requests.

tsx
// routes/about.tsx
export const config = {
  render: 'ssg'
}

export const loader = createLoader(async () => {
  const team = await getTeamMembers()
  return { team }
})

export default function About({ loaderData }) {
  return <TeamGrid members={loaderData.team} />
}

Benefits:

  • Fastest possible load times
  • Reduced server load
  • Can be served from edge CDN

Trade-offs:

  • Content only updates on rebuild
  • Not suitable for personalized content

Dynamic SSG Routes ​

Generate pages for dynamic routes at build time:

tsx
// routes/posts/[slug].tsx
export const config = {
  render: 'ssg'
}

// Tell EreoJS which paths to generate
export async function getStaticPaths() {
  const posts = await db.posts.findMany()
  return posts.map(post => ({
    params: { slug: post.slug }
  }))
}

export const loader = createLoader(async ({ params }) => {
  const post = await db.posts.findBySlug(params.slug)
  return { post }
})

Incremental Static Regeneration ​

Combine SSG with revalidation for the best of both worlds:

tsx
export const config = {
  render: 'ssg',
  cache: {
    revalidate: 60 // Regenerate every 60 seconds
  }
}

The page serves from cache but regenerates in the background when stale.

Client-Side Rendering (CSR) ​

CSR sends a minimal HTML shell, then renders the page entirely in the browser.

tsx
// routes/dashboard.tsx
export const config = {
  render: 'csr'
}

// Loader runs on client
export const loader = createLoader(async () => {
  const response = await fetch('/api/dashboard')
  return response.json()
})

export default function Dashboard({ loaderData }) {
  return <DashboardContent data={loaderData} />
}

Benefits:

  • No server rendering overhead
  • Rich interactivity
  • Smaller initial payload

Trade-offs:

  • No SEO (initially empty HTML)
  • Slower perceived load (blank page then content)
  • Requires JavaScript

When to Use CSR ​

  • Authenticated dashboards (no SEO needed)
  • Complex interactive applications
  • When data is entirely client-fetched

Streaming SSR ​

Streaming sends HTML progressively as it's rendered. This provides fast time-to-first-byte while allowing slow data to load.

tsx
// routes/posts/[id].tsx
export const config = {
  render: 'streaming'
}

export const loader = createLoader(async ({ params }) => {
  // Fast data - rendered immediately
  const post = await db.posts.find(params.id)

  // Slow data - streamed later
  const comments = defer(db.comments.findByPost(params.id))
  const recommendations = defer(getRecommendations(params.id))

  return { post, comments, recommendations }
})

export default function Post({ loaderData }) {
  return (
    <article>
      {/* Rendered immediately */}
      <h1>{loaderData.post.title}</h1>
      <p>{loaderData.post.content}</p>

      {/* Streamed when ready */}
      <Suspense fallback={<CommentsSkeleton />}>
        <Await resolve={loaderData.comments}>
          {(comments) => <Comments data={comments} />}
        </Await>
      </Suspense>

      <Suspense fallback={<RecommendationsSkeleton />}>
        <Await resolve={loaderData.recommendations}>
          {(recs) => <Recommendations items={recs} />}
        </Await>
      </Suspense>
    </article>
  )
}

Benefits:

  • Fast TTFB
  • Progressive content loading
  • Better perceived performance
  • SEO-friendly (critical content renders first)

Trade-offs:

  • More complex component structure
  • Requires Suspense boundaries
  • Some overhead in streaming setup

How Streaming Works ​

┌─────────────────────────────────────────────────────────┐
│ Time: 0ms                                               │
│ Server: Start rendering, send HTML shell                │
│ Browser: Receives <html><head>...<body>                 │
└─────────────────────────────────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────────┐
│ Time: 50ms                                              │
│ Server: Post data ready, send article content           │
│ Browser: Displays article with loading placeholders     │
└─────────────────────────────────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────────┐
│ Time: 200ms                                             │
│ Server: Comments ready, stream comment chunk            │
│ Browser: Comments replace skeleton                      │
└─────────────────────────────────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────────┐
│ Time: 500ms                                             │
│ Server: Recommendations ready, stream final chunk       │
│ Browser: Page complete                                  │
└─────────────────────────────────────────────────────────┘

Hybrid Rendering ​

Different routes can use different rendering modes:

tsx
// routes/index.tsx - SSG for landing page
export const config = { render: 'ssg' }

// routes/blog/[slug].tsx - SSG with revalidation for blog posts
export const config = {
  render: 'ssg',
  cache: { revalidate: 3600 }
}

// routes/dashboard/index.tsx - CSR for authenticated dashboard
export const config = { render: 'csr' }

// routes/search.tsx - SSR for dynamic search
export const config = { render: 'ssr' }

// routes/products/[id].tsx - Streaming for product pages
export const config = { render: 'streaming' }

Per-Request Mode Selection ​

Choose rendering mode dynamically:

tsx
export const config = {
  render: ({ request }) => {
    // Bots get SSR for SEO
    const userAgent = request.headers.get('User-Agent')
    if (isBot(userAgent)) {
      return 'ssr'
    }

    // Logged-in users get streaming
    if (request.headers.get('Cookie')?.includes('session')) {
      return 'streaming'
    }

    // Default to SSG
    return 'ssg'
  }
}

Comparison Table ​

FeatureSSRSSGCSRStreaming
SEOExcellentExcellentPoorExcellent
TTFBMediumFastFastFast
Time to InteractiveMediumFastSlowMedium
Server LoadHighNoneNoneMedium
Content FreshnessReal-timeBuild-timeReal-timeReal-time
PersonalizationYesNoYesYes
CachingComplexEasyN/AComplex

Choosing a Rendering Mode ​

Is the content the same for all users?
├─ Yes → Does it change frequently?
│        ├─ No → SSG
│        └─ Yes → SSG + revalidation or SSR
└─ No → Is SEO important?
         ├─ Yes → Does it have slow data sources?
         │        ├─ Yes → Streaming
         │        └─ No → SSR
         └─ No → CSR

Best Practices ​

  1. Default to SSR - Start with SSR, optimize to SSG/streaming as needed
  2. Use SSG for static content - Marketing pages, docs, blog posts
  3. Stream slow data - Don't block on non-critical data
  4. CSR sparingly - Only when SEO doesn't matter
  5. Monitor TTFB - Streaming helps when SSR is slow
  6. Test with slow connections - Streaming benefits are most visible on slow networks

Anti-Patterns ​

Using CSR for SEO-critical pages ​

CSR pages have empty HTML until JavaScript loads — search engines may not index them properly. Use SSR or SSG for any page that needs to appear in search results.

SSG for frequently changing data ​

If your data changes every few minutes, SSG with long revalidation intervals serves stale content. Use SSR or SSG with short revalidate instead.

Streaming everything ​

Streaming adds complexity (Suspense boundaries, loading states). Only use streaming for routes with genuinely slow data sources. For fast queries, SSR is simpler and equally performant.

Mixing rendering modes in a layout ​

A layout's rendering mode affects all child routes. If the layout is SSG, child SSR routes may behave unexpectedly. Keep layout rendering modes compatible with their children.

Decision Tree ​

Need SEO? ─── No → CSR (dashboards, authenticated apps)
  │
  Yes
  │
Content changes often? ─── No → SSG (blogs, docs, marketing)
  │
  Yes
  │
Has slow data sources? ─── Yes → Streaming SSR
  │
  No → SSR

Released under the MIT License.