How to Fix: Old Content Persists After Navigation in Next.js on Chrome

6 min read

Old page content flashing or persisting after navigation in Chrome is usually not a routing bug in your app logic. In this Next.js reproduction, it is caused by the interaction between client-side navigation, streamed rendering or delayed page replacement, and Chrome reusing previously painted UI until the next route fully commits. The result is a stale screen that appears to survive navigation even though the URL has already changed.

Understanding the Root Cause

This issue typically shows up when navigating between routes in a Next.js app where the next screen does not immediately replace the previous rendered tree. In Chrome, the browser may continue displaying the previous DOM until the new route finishes enough work to paint a replacement. If the transition is delayed by asynchronous rendering, suspense boundaries, client components, or layout reuse, the old content can appear to persist.

Technically, several things are happening:

  • Next.js preserves shared layouts across route transitions, which is usually good for performance, but it also means parts of the old UI remain mounted unless you explicitly reset them.
  • React suspense and concurrent rendering can delay when new content commits to the screen.
  • Chrome paint behavior may keep the old pixels visible until something new is ready to render, making the bug feel worse in Chrome than in other browsers.
  • If a route segment does not have a proper loading state or a unique remount boundary, the old route can remain visible longer than expected.

In practice, this is less about broken navigation and more about stale UI being visually retained during transition. The fix is to make route transitions explicitly render a loading state or force specific content areas to remount when the pathname changes.

Step-by-Step Solution

The most reliable fix is to ensure that the route content is replaced immediately during navigation instead of letting the old page remain on screen.

1. Add a route-level loading UI

If you are using the App Router, create a loading.js or loading.tsx file in the relevant route segment. This gives Next.js something to render immediately while the next page loads.

app/loading.tsx
export default function Loading() {
  return <div>Loading new page...</div>
}

If the issue happens only in a nested route, add the loading file at that segment level instead:

app/some-route/loading.tsx
export default function Loading() {
  return <div>Loading route content...</div>
}

2. Force route content to remount on pathname change

If shared layouts are causing stale content to hang around, wrap the main content in a component keyed by the current pathname.

'use client'

import { usePathname } from 'next/navigation'

export default function RouteContentWrapper({ children }) {
  const pathname = usePathname()

  return <div key={pathname}>{children}</div>
}

Then use it in your layout or page container:

import RouteContentWrapper from './RouteContentWrapper'

export default function Layout({ children }) {
  return (
    <RouteContentWrapper>
      {children}
    </RouteContentWrapper>
  )
}

This tells React to treat each pathname as a fresh subtree, preventing old route content from visually lingering.

3. Move long async work behind suspense boundaries

If your page waits on slow server or client work before anything changes on screen, add a Suspense fallback so users see replacement UI immediately.

import { Suspense } from 'react'
import SlowContent from './SlowContent'

export default function Page() {
  return (
    <Suspense fallback={<div>Loading content...</div>}>
      <SlowContent />
    </Suspense>
  )
}

4. Avoid preserving stale state in client components

Client components reused across pages can keep old state alive. Reset local state when route params or pathname change.

'use client'

import { useEffect, useState } from 'react'
import { usePathname } from 'next/navigation'

export default function Example() {
  const pathname = usePathname()
  const [data, setData] = useState(null)

  useEffect(() => {
    setData(null)
  }, [pathname])

  return <div>{data ? data : 'Loading...'}</div>
}

5. Test with production build, not only development mode

Some navigation artifacts behave differently between dev and production. Always verify with:

npm run build
npm run start

If the issue is significantly reduced in production, the problem may be amplified by development overlays, extra renders, or debug tooling rather than your actual release behavior.

6. Upgrade Next.js and React

If your reproduction uses an older version, upgrade first. Navigation and rendering bugs around App Router, Suspense, and streaming have been improved across releases.

npm install next@latest react@latest react-dom@latest

Then retest the reproduction in Chrome.

7. Provide a deliberate transition placeholder for UX stability

If the route change is expected to take noticeable time, render a shell that clearly replaces the old content instead of leaving the previous page visible.

'use client'

import { usePathname } from 'next/navigation'

export default function PageShell({ children }) {
  const pathname = usePathname()

  return (
    <section aria-live="polite">
      <div key={pathname}>{children}</div>
    </section>
  )
}

This does not just mask the issue. It creates a clearer rendering contract during navigation.

Common Edge Cases

  • Shared layout segments: If the stale content sits inside a layout that does not remount between routes, adding a key at the page level may not be enough. You may need to key the specific nested content region.
  • Client-side caches: Libraries like SWR, React Query, or custom stores may immediately rehydrate old data on the next page. In that case, the problem is stale cache reuse rather than browser painting alone.
  • CSS-based overlays: Fixed-position elements, animated containers, or delayed opacity transitions can make it look like the old page persists when the new page is actually mounted underneath.
  • Suspense fallback placement: If the fallback is too deep in the tree, the outer old content remains visible. Place boundaries high enough to replace the visible page region.
  • Strict Mode confusion: In development, React Strict Mode can trigger additional renders that make transition artifacts easier to notice. Do not diagnose only from dev behavior.
  • Scroll restoration interactions: Chrome may preserve scroll position while the next content is not yet painted, which can make the old screen feel stuck longer.

FAQ

Is this a Chrome bug or a Next.js bug?

It is usually a combination of Chrome paint behavior and how Next.js renders route transitions. Chrome is more likely to keep old pixels visible, but the practical fix is in your Next.js rendering strategy: add loading states, suspense fallbacks, or remount boundaries.

Why does the URL change before the content updates?

Next.js navigation can update browser history immediately while the new route tree is still being prepared. If nothing replaces the visible UI right away, the previous page remains on screen until the next render commits.

What is the safest fix for production apps?

The safest approach is to combine a route-level loading UI with carefully placed Suspense fallbacks. If a specific area still retains stale content, add a key based on pathname to force remounting of that subtree.

For reference, review the reproduction linked in the GitHub issue and compare your implementation against current Next.js routing guidance in the Next.js documentation. If you want to confirm whether the behavior is version-specific, also check open discussions and release notes in the Next.js GitHub repository.

Leave a Reply

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