Memo
Overview
Learn the basics of function memoization in 10 minutes.
Table of Contents
Why Memoization?
The Problem: Repeated Expensive Work
// Expensive computation
function fibonacci(n: number): number {
if (n <= 1) return n
return fibonacci(n - 1) + fibonacci(n - 2)
}
fibonacci(40) // Takes ~1 second
fibonacci(40) // Takes ~1 second again! (wasteful)
fibonacci(40) // Takes ~1 second again! (wasteful)Problem: Same input, same output, but we're recomputing every time.
The Solution: Memoization
import { memoize } from '@ontologyio/receta/memo'
const memoFib = memoize(fibonacci)
memoFib(40) // Takes ~1 second (computed)
memoFib(40) // Takes <1ms (cached!)
memoFib(40) // Takes <1ms (cached!)Benefit: 1000x+ speedup for cached calls.
Installation
Receta is built on Remeda and installs it as a dependency. Add remeda yourself if your own code imports it:
# Using bun (recommended)
bun add @ontologyio/receta remeda
# Using npm
npm install @ontologyio/receta remeda
# Using yarn
yarn add @ontologyio/receta remeda
# Using pnpm
pnpm add @ontologyio/receta remedaTypeScript Configuration
Receta requires TypeScript 5.0+ with strict mode:
{
"compilerOptions": {
"strict": true,
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler"
}
}Your First Memoized Function
Step 1: Import
import { memoize } from '@ontologyio/receta/memo'Step 2: Wrap Your Function
// Original function
function getUserById(id: string): User {
console.log(`Fetching user ${id}`)
return database.users.find(u => u.id === id)!
}
// Memoized version
const memoGetUserById = memoize(getUserById)Step 3: Use It
const user1 = memoGetUserById('123')
// Console: "Fetching user 123"
const user2 = memoGetUserById('123')
// (no console output - cached!)
console.log(user1 === user2) // true (same object)How It Works
- First call: Function executes, result cached with argument as key
- Subsequent calls: If argument matches, return cached result
- Different argument: Execute function, cache new result
Common Use Cases
1. Expensive Computations
import { memoize } from '@ontologyio/receta/memo'
const factorial = memoize((n: number): number => {
if (n <= 1) return 1
return n * factorial(n - 1)
})
factorial(100) // Computed
factorial(100) // Cached (instant)When to use: Mathematical computations, parsing, transformations
2. API Calls
import { memoizeAsync, ttlCache } from '@ontologyio/receta/memo'
const fetchUser = memoizeAsync(
async (id: string) => {
const res = await fetch(`/api/users/${id}`)
return res.json()
},
{ cache: ttlCache(5 * 60 * 1000) } // 5 min cache
)
await fetchUser('123') // API call
await fetchUser('123') // Cached (no network!)When to use: API responses, external data, slow network calls
3. Database Queries
import { memoizeAsync, lruCache } from '@ontologyio/receta/memo'
const queryUsers = memoizeAsync(
async (filter: string) => {
return await db.query(`SELECT * FROM users WHERE ${filter}`)
},
{
keyFn: (filter) => filter,
cache: lruCache(50) // Keep last 50 queries
}
)
await queryUsers('active = true') // DB query
await queryUsers('active = true') // CachedWhen to use: Read-heavy queries, configuration data, lookups
4. Image Processing
import { memoizeBy, lruCache } from '@ontologyio/receta/memo'
interface ImageOptions {
url: string
width: number
height: number
}
const resizeImage = memoizeBy(
(opts: ImageOptions) => processImage(opts),
(opts) => `${opts.url}:${opts.width}x${opts.height}`,
{ cache: lruCache(100) }
)
resizeImage({ url: 'photo.jpg', width: 800, height: 600 }) // Processed
resizeImage({ url: 'photo.jpg', width: 800, height: 600 }) // CachedWhen to use: Image/video transformations, file processing
Quick Comparison
Without Memoization
// ❌ Problems:
// - Repeated work
// - Slow performance
// - Wasted resources
// - No request deduplication
async function getWeather(city: string) {
const res = await fetch(`/api/weather/${city}`)
return res.json()
}
// Multiple components requesting same data
const [a, b, c] = await Promise.all([
getWeather('NYC'), // API call
getWeather('NYC'), // API call (duplicate!)
getWeather('NYC') // API call (duplicate!)
]) // 3 API calls for same data!With Memoization
// ✅ Benefits:
// - Work done once
// - Fast subsequent calls
// - Efficient resource use
// - Request deduplication
import { memoizeAsync } from '@ontologyio/receta/memo'
const getWeather = memoizeAsync(async (city: string) => {
const res = await fetch(`/api/weather/${city}`)
return res.json()
})
const [a, b, c] = await Promise.all([
getWeather('NYC'), // API call
getWeather('NYC'), // Deduplicated!
getWeather('NYC') // Deduplicated!
]) // Only 1 API call!Understanding Cache Keys
Single Argument (Automatic)
import { memoize } from '@ontologyio/receta/memo'
// Cache key = first argument
const double = memoize((n: number) => n * 2)
double(5) // Cache key: 5
double(5) // Cache hit!Multiple Arguments (Manual Key)
import { memoizeBy } from '@ontologyio/receta/memo'
// Custom key function
const add = memoizeBy(
(a: number, b: number) => a + b,
(a, b) => `${a},${b}` // Cache key
)
add(2, 3) // Cache key: "2,3"
add(2, 3) // Cache hit!
add(3, 2) // Cache key: "3,2" (different key, computed)Object Arguments (Serialize)
import { memoizeBy } from '@ontologyio/receta/memo'
interface Options {
id: string
type: string
}
const fetchData = memoizeBy(
(opts: Options) => api.fetch(opts),
(opts) => JSON.stringify(opts) // Serialize to key
)
fetchData({ id: '1', type: 'user' }) // Computed
fetchData({ id: '1', type: 'user' }) // CachedCache Invalidation
Manual Clearing
import { memoize, clearCache } from '@ontologyio/receta/memo'
const getUser = memoize(fetchUser)
getUser('123') // Computed
getUser('123') // Cached
// Clear cache when data changes
clearCache(getUser)
getUser('123') // RecomputedAlternative Syntax
const getUser = memoize(fetchUser)
getUser('123') // Computed
getUser('123') // Cached
// Using .clear() method
getUser.clear()
getUser('123') // RecomputedTime-Based Expiration
import { memoize, ttlCache } from '@ontologyio/receta/memo'
const getWeather = memoize(fetchWeather, {
cache: ttlCache(5 * 60 * 1000) // 5 min TTL
})
getWeather('NYC') // Computed
getWeather('NYC') // Cached
// After 5 minutes...
getWeather('NYC') // Recomputed (expired)Performance Tips
✅ Good Candidates for Memoization
- Pure functions (same input → same output)
- Expensive computations (parsing, calculations)
- API calls (network latency)
- Database queries (I/O bound)
- Recursive algorithms (fibonacci, factorial)
❌ Poor Candidates
- Functions with side effects (logging, I/O)
- Random outputs (Math.random, Date.now)
- Always-unique arguments (no cache hits)
- Memory-intensive results (large objects)
- Real-time data (stock prices, live feeds)
Common Pitfalls
Pitfall 1: Memoizing Impure Functions
// ❌ Bad: Side effects
const log = memoize((msg: string) => {
console.log(msg) // Side effect!
return msg
})
log('hello') // Logs "hello"
log('hello') // Cached - doesn't log! (unexpected)Pitfall 2: Forgetting Multi-Arg Keys
// ❌ Bad: Only uses first argument
const add = memoize((a: number, b: number) => a + b)
add(2, 3) // 5, cache key: 2
add(2, 5) // 5 (WRONG! cached from key 2)
// ✅ Good: Custom key function
const add = memoizeBy(
(a: number, b: number) => a + b,
(a, b) => `${a},${b}`
)Pitfall 3: Unbounded Cache Growth
// ❌ Bad: Unbounded cache
const process = memoize(expensiveProcess)
for (let i = 0; i < 1000000; i++) {
process(i) // Cache grows to 1M entries! (OOM)
}
// ✅ Good: Use LRU cache
import { lruCache } from '@ontologyio/receta/memo'
const process = memoize(expensiveProcess, {
cache: lruCache(1000) // Max 1000 entries
})Next Steps
Now that you understand the basics:
- Learn cache strategies → Core Concepts
- See real-world examples → Practical Examples
- Understand TypeScript types → TypeScript Integration
- Handle errors properly → Error Handling
Quick Reference
import {
memoize, // Single-arg caching
memoizeBy, // Custom key extraction
memoizeAsync, // Async + deduplication
ttlCache, // Time-based expiration
lruCache, // Memory-limited
weakCache, // GC-friendly
clearCache // Invalidate cache
} from '@ontologyio/receta/memo'
// Basic
const fast = memoize(slow)
// Multi-arg
const add = memoizeBy((a: number, b: number) => a + b, (a, b) => `${a},${b}`)
// Async
const fetch = memoizeAsync(async (id: string) => await api.get(id))
// TTL
const weather = memoize(getWeather, { cache: ttlCache(60000) })
// LRU
const user = memoize(getUser, { cache: lruCache(100) })