I just made my dashboard go from 2-3 second loading
spinner to instant. Here's the exact technique.
The problem: every time a user opened the dashboard,
they'd see a spinner while we fetched their plan,
repos, and docs from the API. Even on fast connections
it felt slow.
The solution: stale-while-revalidate with localStorage
Here's the pattern:
- On load, check localStorage for cached data
- If found, show it IMMEDIATELY (no spinner)
- Always fetch fresh data in background
- Update the UI silently when fresh data arrives
The user sees their data in milliseconds. If data
changed since last visit they see the update
a second later with zero disruption.
The cache utility (TypeScript):
\`typescript
interface CacheEntry {
data: T
timestamp: number
userId: string
version: string
}
export function getCached(
key: string,
userId: string,
ttlMs: number
): T | null {
try {
const raw = localStorage.getItem(`pushpen_\${key})
if (!raw) return null
const entry: CacheEntry<T> = JSON.parse(raw)
if (entry.userId !== userId) return null
if (Date.now() - entry.timestamp > ttlMs) return null
return entry.data
} catch { return null }
}
\\
Usage in the dashboard:
\`typescript
async function fetchUserPlan(userId: string) {
// Show cached data instantly
const cached = getCached('user_plan', userId, 300000)
if (cached) {
setUserPlan(cached)
setLoading(false)
}
// Always fetch fresh in background
const res = await fetch('/api/user/plan')
const data = await res.json()
setUserPlan(data)
setCached('user_plan', userId, data)
}
`\
TTLs I use:
- User plan: 5 minutes
- Connected repos: 3 minutes
- Docs list: 2 minutes
- Templates: 10 minutes
Also preloaded the interactive demo iframe 2 seconds
after page load so clicking play is instant.
The result: dashboard feels native-app fast.
I built this for Pushpen (pushpen.dev) — an AI tool
that auto-generates GitHub docs on every push.
What performance tricks have worked for your SaaS?
Top comments (0)