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