Performance
Stop Computing the Same Thing Twice - Cache Your Function Results
January 14, 2026
You're rendering a feed of 50 notifications. Each shows "2 hours ago" or "yesterday" - formatted from a timestamp. You call formatRelativeTime(notification.createdAt) inside the map. Clean.
Except 30 of those notifications were created in the same hour. You're running formatRelativeTime 50 times when you only need ~15 unique results. On every scroll, every re-render - same work, again.
That's redundant computation. The fix is memoization - caching results so the same input returns the cached output instead of recomputing.
The problem: compute on every call
function NotificationFeed({
notifications,
}: {
notifications: Notification[];
}) {
return (
<ul>
{notifications.map((notif) => {
const label = formatRelativeTime(notif.createdAt); // 50 calls, many duplicates
return (
<li key={notif.id}>
<span>{label}</span>
{notif.message}
</li>
);
})}
</ul>
);
}Every render, every item - formatRelativeTime runs again. No memory of previous results.
The fix: a module-level Map
const relativeTimeCache = new Map<number, string>();
function cachedFormatRelativeTime(timestamp: number): string {
if (relativeTimeCache.has(timestamp)) {
return relativeTimeCache.get(timestamp)!;
}
const result = formatRelativeTime(timestamp);
relativeTimeCache.set(timestamp, result);
return result;
}
function NotificationFeed({
notifications,
}: {
notifications: Notification[];
}) {
return (
<ul>
{notifications.map((notif) => {
const label = cachedFormatRelativeTime(notif.createdAt); // Once per unique timestamp
return (
<li key={notif.id}>
<span>{label}</span>
{notif.message}
</li>
);
})}
</ul>
);
}Now each unique timestamp is formatted once. The Map works everywhere - components, event handlers, utilities. No React dependency.
For single-value functions (e.g. auth checks):
let isLoggedInCache: boolean | null = null;
function isLoggedIn(): boolean {
if (isLoggedInCache !== null) return isLoggedInCache;
isLoggedInCache = document.cookie.includes("auth=");
return isLoggedInCache;
}
function onAuthChange() {
isLoggedInCache = null; // Clear when auth changes
}When to use React's cache() instead
React 18+ has a built-in cache() for Server Components. It memoizes, but with different rules:
| Module-level Map | React cache() | |
|---|---|---|
| Where | Client, server, anywhere | Server Components only |
| Lifetime | Until you clear it | Per request (auto-invalidated) |
| Use case | Pure utils, repeated sync work | RSC data fetching, request-scoped dedup |
| Call site | Components, handlers, utilities | Must be called from a Server Component |
Use cache() when: You're in a Server Component and want to deduplicate fetches or heavy work across the same request. Multiple components might fetch the same user - cache() ensures the DB is hit once:
import { cache } from "react";
const getUser = cache(async (id: string) => {
const res = await db.query("SELECT * FROM users WHERE id = $1", [id]);
return res.rows[0];
});
async function ProfileHeader({ userId }: { userId: string }) {
const user = await getUser(userId);
return <h1>{user.name}</h1>;
}
async function ProfileSidebar({ userId }: { userId: string }) {
const user = await getUser(userId); // Same request → cached, no second query
return <aside>{user.bio}</aside>;
}React invalidates this cache on every request - no stale data between users. Perfect for server-side data fetching.
Use a Map when: You need caching in Client Components, event handlers, or shared utilities. The Map persists until you clear it, so you control invalidation (e.g. on auth change, logout, or when data goes stale).
Rule of thumb: Server Components doing request-scoped work (especially async) → cache(). Pure functions called repeatedly with the same inputs → module-level Map (and invalidate when the underlying data changes).
Related content
Lazy-Load vs Conditional Mount/Unmount - What Actually Changes
Lazy-loading defers code/data download; conditional rendering decides whether a component instance exists. The difference impacts network, effects, and state.
Read moreWhy Your Animations Stutter - Batch Your DOM Style Changes
Changing four style properties one by one triggers four reflows. One class toggle triggers one. Here's how to stop the browser from doing unnecessary work.
Read moreUse Set for Membership Checks - Not .includes()
You're filtering items by allowed IDs. Each check does array.includes() - O(n) per item. A Set gives you O(1). Same result, much faster.
Read more