How to Fix: Ensure the component tree is only rendered once during SSG/revalidations #67680. Why hasn’t it been merged in the release?

7 min read

Next.js App Router SSG revalidation can render your component tree twice, and that is exactly why you may see duplicated fetches, repeated logs, doubled CPU work, or side effects firing more than expected during static generation and ISR. The GitHub issue #67680 describes a real production concern: during SSG and revalidation, the tree should ideally be rendered once, but under current internals it can be evaluated multiple times before the final output is committed.

What is happening in this bug

When a route is statically rendered in the App Router, Next.js may execute parts of the React Server Component tree more than once during build-time generation or ISR revalidation. This is not the same as React Strict Mode in development. It can happen in production-oriented rendering paths as the framework resolves data dependencies, builds the RSC payload, and prepares the final HTML and cache artifacts.

In practice, this shows up as:

  • duplicate console output from server components,
  • the same expensive function being called twice,
  • database or API reads happening more than expected,
  • unexpected side effects if rendering code is not pure.

If your page contains non-idempotent code in the render path, revalidation can amplify the problem.

Understanding the Root Cause

The root issue is architectural: rendering in the App Router is not always a single-pass operation. Next.js needs to coordinate several concerns at once:

  • generate the React Server Components payload,
  • produce the final HTML shell,
  • track cache usage and static/dynamic boundaries,
  • resolve async data dependencies for streaming and serialization,
  • store results for static output and future revalidation.

Because of this pipeline, the framework may evaluate the component tree once to collect enough information for static generation and then again to finalize output. During ISR, the revalidation worker can repeat that process when rebuilding the cached page.

Technically, the problem becomes visible when developers assume that a server component render is equivalent to a single execution. In reality, server rendering must be treated as pure and replayable. If a component performs work with side effects inside the render path, repeated evaluation becomes a bug in user code even if the framework behavior feels surprising.

This is why duplicated work often appears around:

  • direct database writes in a component,
  • incrementing counters during render,
  • logging or analytics calls from server component execution,
  • custom fetch wrappers without memoization,
  • expensive CPU-bound transforms not wrapped in caching.

The linked issue exists because developers want stronger guarantees that the tree is rendered only once during SSG and revalidation. That request is reasonable, but implementing it safely inside Next.js touches render orchestration, cache semantics, and RSC/HTML generation internals, which is why a fix is not always quick to ship.

Step-by-Step Solution

Until the framework guarantees a single render pass, the safest solution is to make your render path idempotent, cache-aware, and free of side effects.

1. Remove side effects from Server Component rendering

Do not write to a database, mutate global state, or trigger analytics directly inside a page or layout render.

Problematic:

export default async function Page() {
  await db.logs.create({ data: { event: 'rendered-page' } })

  return <div>Hello</div>
}

Safer: move writes to a Server Action, route handler, cron job, or explicit mutation endpoint.

export default async function Page() {
  return <div>Hello</div>
}

2. Cache expensive reads with React cache or Next.js caching primitives

If your data-fetching logic is expensive, wrap it so repeated tree evaluation does not duplicate the work.

import { cache } from 'react'

const getProduct = cache(async (id: string) => {
  return db.product.findUnique({ where: { id } })
})

export default async function Page() {
  const product = await getProduct('123')
  return <div>{product?.name}</div>
}

This makes repeated calls in the same render lifecycle far less costly.

3. Use fetch caching correctly

When using fetch in App Router, rely on built-in caching where appropriate.

async function getData() {
  const res = await fetch('https://example.com/api/products', {
    next: { revalidate: 60 }
  })

  if (!res.ok) {
    throw new Error('Failed to fetch data')
  }

  return res.json()
}

export default async function Page() {
  const data = await getData()
  return <div>{data.title}</div>
}

If the same request is encountered during the same render pipeline, Next.js can often dedupe it. But this works best when your fetch calls are stable and not hidden behind mutable wrappers.

4. Isolate non-repeatable work outside the render tree

If you absolutely must perform logic once per regeneration, do it in infrastructure designed for that purpose rather than in component execution.

Good alternatives include:

  • Route Handlers for explicit server-side work,
  • Server Actions for user-triggered mutations,
  • scheduled jobs for revalidation-related bookkeeping,
  • background workers for post-render processing.

5. Avoid assuming console logs represent a single production render

Many developers use logs to judge whether rendering happened once. In this bug, duplicated logs are only a symptom. The real issue is duplicated work. Make sure the code remains safe even if evaluation happens more than once.

export default async function Page() {
  console.log('Rendering page')
  return <div>Rendered</div>
}

This log may appear multiple times during SSG or revalidation. That alone is not dangerous. What matters is whether the render path has side effects.

6. Add a stable memoization layer for custom data utilities

If your code does not use fetch directly, memoize the underlying function.

import { cache } from 'react'

export const getSettings = cache(async () => {
  const settings = await db.settings.findFirst()
  return settings
})

This is especially important for:

  • ORM queries,
  • CMS SDK calls,
  • GraphQL clients,
  • custom REST wrappers.

7. Audit for hidden global mutations

Some bugs are caused by seemingly harmless module-level state.

let counter = 0

export default async function Page() {
  counter++
  return <div>Counter: {counter}</div>
}

This is unsafe in server rendering because execution can be replayed, parallelized, or reused across requests depending on runtime behavior.

Replace mutable globals with deterministic data sources.

Common Edge Cases

Database reads are safe, but writes are not

Repeated reads are usually acceptable if cached. Repeated writes can corrupt data or generate duplicate records.

Third-party SDKs may bypass Next.js fetch deduplication

If a CMS or GraphQL client performs its own network calls internally, Next.js may not dedupe them automatically. Add React cache around the SDK call.

Mixed static and dynamic rendering can confuse expectations

If part of the route becomes dynamic because of cookies, headers, or uncached fetches, the rendering behavior can differ from pure SSG routes. Review whether the route is truly static.

Development behavior is different from production behavior

React Strict Mode in development can also cause double invocation patterns. Do not conflate that with this issue. Test with a production build when validating ISR behavior.

next build
next start

Revalidation timing can hide the problem

If your page only revalidates every few minutes, duplicated work may seem random. The bug becomes easier to spot with short revalidation intervals and timestamped logs.

Streaming boundaries can make duplicate execution harder to trace

Nested async components, Suspense boundaries, and layouts can make it look like only one component is re-rendering, when the real duplicated work is happening higher or lower in the tree.

Why it has not been merged into a release

There are usually three practical reasons issues like this one do not land immediately in stable releases:

  1. The fix is deeper than it looks. Ensuring a single render for SSG and ISR is not a small patch if the current pipeline depends on multiple evaluation stages for RSC payload generation, cache collection, and HTML output.
  2. It can affect framework correctness. Any optimization here risks breaking caching, streaming, partial prerendering, or static/dynamic route detection.
  3. The team may be waiting for a safer internal refactor. Some bugs are acknowledged but deferred until related rendering infrastructure is stable enough to change.

So the answer to “why hasn’t it been merged in the release?” is usually not that the issue was ignored. It is more often that the change is high-risk in core rendering code, needs broad regression coverage, or is tied to larger internal work.

If you need a production-safe path today, the correct strategy is not to wait for a merge. It is to design server components as replay-safe and cache expensive operations explicitly.

FAQ

Is this just React Strict Mode double rendering?

No. Strict Mode can double-invoke in development, but this issue is about SSG and ISR revalidation behavior in Next.js App Router rendering paths. You should still test in a production build to distinguish the two.

Can Next.js fetch deduplication solve everything?

No. It helps for identical fetch requests, but it does not automatically protect database writes, custom SDK calls, mutable globals, or side effects embedded in component rendering.

What is the best workaround until the framework changes?

Keep the render tree pure, wrap expensive reads in cache(), use built-in fetch caching, and move all mutations out of the component render path.

Bottom line: treat App Router server rendering during SSG and revalidation as replayable. If your code is pure and memoized, this issue becomes mostly a performance concern instead of a correctness bug.

Leave a Reply

Your email address will not be published. Required fields are marked *