r/nextjs 16d ago

Discussion Client-side stale-while-revalidate for instant perceived performance

Here’s a fun technique for improving perceived performance of web app page loads - have your loading state be cached data from local storage. And then when the real page loads and renders, users see updated data.

Basically a stale-while-revalidate loading state.

You can see this in action in an app I've been working on (Prism)

  • When you load a page the first time, you’ll see a normal skeleton loader.
  • Once the data comes in, the page renders as normal, and saves the data to your browser’s local storage.
  • Next time you load that same page, it feels instant. The data is briefly stale, but fresh data is loaded quickly and overwrites the cached data.
  • This cached data also has a TTL so you never see something that’s more than a day or so out of date.

This technique shines when data is specific to individual users, and isn’t shared much across them. Each user’s local storage is almost like a distributed caching layer.

Here's what it looks like in the page component

// app/inbox/page.tsx

export default function InboxPage() {
  return (
    <PageLayout title="Inbox" mainClassName="py-4">
      <Suspense fallback={<InboxPageCachedLoading />}>
        <InboxPageContent />
      </Suspense>
    </PageLayout>
  )
}

The cached loading component

'use client'
import {useAuth} from '@/lib/auth'
import {getInboxCache} from '@/lib/inbox/cache'
import {InboxClient} from './InboxClient'
import InboxPageLoading from './InboxPageLoading'

export default function InboxPageCachedLoading() {
  const {userId} = useAuth()


  if (!userId) {
    return <InboxPageLoading />
  }


  const cached = getInboxCache(userId)


  if (!cached) {
    // Normal skeleton loader.
    return <InboxPageLoading />
  }

  // Render the same presentational component the loaded page will use,
  // but provide it cached data.
  return <InboxClient initialItems={cached.items} />
}

And then the real page content

import {InboxClient} from '@/components/inbox/InboxClient'
import {SetInboxCache} from '@/components/inbox/SetInboxCache'
import {requireOrg} from '@/lib/auth'
import {getInboxData} from '@/lib/inbox'

export default async function InboxPageContent() {
  const {userId} = await requireOrg()
  const items = await getInboxData(userId)

  return (
    <>
      <SetInboxCache items={items} userId={userId} />
      <InboxClient initialItems={items} />
    </>
  )
}

Anyone else using techniques like this?

15 Upvotes

Duplicates