# Performance Optimization
Optimize TanStack Query for better performance, reduced network requests, and improved user experience.
## Query Configuration
### staleTime
Control how long data is considered fresh:
```tsx
// ❌ Default - data stale immediately
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
staleTime: 0, // default
});
// ✅ Optimized - data fresh for 5 minutes
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
staleTime: 1000 * 60 * 5, // 5 minutes
});
// ✅ Static data - never stale
useQuery({
queryKey: ['config'],
queryFn: fetchConfig,
staleTime: Infinity,
});
```
### gcTime (formerly cacheTime)
Control how long unused data stays in cache:
```tsx
// Default - 5 minutes
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
gcTime: 1000 * 60 * 5, // default
});
// Extended cache for frequently accessed data
useQuery({
queryKey: ['user-profile'],
queryFn: fetchUserProfile,
gcTime: 1000 * 60 * 30, // 30 minutes
});
// Immediate cleanup for sensitive data
useQuery({
queryKey: ['payment-info'],
queryFn: fetchPaymentInfo,
gcTime: 0, // Remove immediately when unused
});
```
### Disable Unnecessary Refetching
```tsx
// Disable all automatic refetching
useQuery({
queryKey: ['static-data'],
queryFn: fetchStaticData,
refetchOnWindowFocus: false,
refetchOnReconnect: false,
refetchOnMount: false,
});
// Global defaults
const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
refetchOnReconnect: false,
},
},
});
```
## Query Deduplication
TanStack Query automatically deduplicates identical requests:
```tsx
// These three components all request the same data
function Component1() {
useQuery({ queryKey: ['user', userId], queryFn: fetchUser });
}
function Component2() {
useQuery({ queryKey: ['user', userId], queryFn: fetchUser });
}
function Component3() {
useQuery({ queryKey: ['user', userId], queryFn: fetchUser });
}
// Result: Only ONE network request is made
// All three components share the same cached data
```
## Structural Sharing
TanStack Query preserves referential equality when data hasn't changed:
```tsx
const { data } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
// Structural sharing is enabled by default
structuralSharing: true,
});
// If server returns identical data, React won't re-render
// because data reference hasn't changed
```
Disable for very large datasets:
```tsx
useQuery({
queryKey: ['large-dataset'],
queryFn: fetchLargeDataset,
structuralSharing: false, // Skip structural sharing for performance
});
```
## Prefetching
Load data before it's needed:
### Hover Prefetch
```tsx
const queryClient = useQueryClient();
function TodoListItem({ todo }) {
const prefetchTodo = () => {
queryClient.prefetchQuery({
queryKey: ['todo', todo.id],
queryFn: () => fetchTodo(todo.id),
staleTime: 1000 * 60 * 5,
});
};
return (
{todo.title}
);
}
```
### Route-Based Prefetch
```tsx
// In router loader or component
async function todoLoader({ params }) {
await queryClient.prefetchQuery({
queryKey: ['todo', params.id],
queryFn: () => fetchTodo(params.id),
});
}
// Or in a parent component
function TodoLayout() {
const navigate = useNavigate();
useEffect(() => {
// Prefetch common routes
queryClient.prefetchQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
});
}, []);
return ;
}
```
### Predictive Prefetch
```tsx
function PaginatedList({ page }) {
const { data } = useQuery({
queryKey: ['items', page],
queryFn: () => fetchItems(page),
});
// Prefetch next page
useEffect(() => {
if (page < totalPages) {
queryClient.prefetchQuery({
queryKey: ['items', page + 1],
queryFn: () => fetchItems(page + 1),
});
}
}, [page]);
return
{/* render items */}
;
}
```
## Data Transformation
### Use select for Transformation
```tsx
// ❌ Transform in component - runs on every render
function TodoList() {
const { data } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
});
const completedTodos = data?.filter(todo => todo.completed);
return {completedTodos?.map(/* ... */)}
;
}
// ✅ Transform with select - memoized automatically
function TodoList() {
const { data: completedTodos } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
select: (todos) => todos.filter(todo => todo.completed),
});
return {completedTodos?.map(/* ... */)}
;
}
```
### Select is Memoized
```tsx
// select function only runs when data changes
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
select: (todos) => {
console.log('Transforming...'); // Only logs when data changes
return todos.map(todo => ({
...todo,
displayName: `${todo.id}: ${todo.title}`,
}));
},
});
```
## Pagination Optimization
### Offset Pagination
```tsx
function PaginatedTodos() {
const [page, setPage] = useState(1);
const { data } = useQuery({
queryKey: ['todos', page],
queryFn: () => fetchTodos(page),
staleTime: 1000 * 60 * 5, // Keep pages fresh
placeholderData: (previousData) => previousData, // Keep previous data while loading
});
return (
{data?.items.map(todo => )}
);
}
```
### Infinite Queries with Windowing
For very long lists, use virtual scrolling:
```tsx
import { useInfiniteQuery } from '@tanstack/react-query';
import { useVirtualizer } from '@tanstack/react-virtual';
function VirtualizedInfiniteList() {
const {
data,
fetchNextPage,
hasNextPage,
} = useInfiniteQuery({
queryKey: ['items'],
queryFn: ({ pageParam = 0 }) => fetchItems(pageParam),
initialPageParam: 0,
getNextPageParam: (lastPage) => lastPage.nextCursor,
});
const allItems = data?.pages.flatMap(page => page.items) ?? [];
const parentRef = useRef(null);
const virtualizer = useVirtualizer({
count: hasNextPage ? allItems.length + 1 : allItems.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 100,
});
useEffect(() => {
const lastItem = virtualizer.getVirtualItems()[virtualizer.getVirtualItems().length - 1];
if (!lastItem) return;
if (lastItem.index >= allItems.length - 1 && hasNextPage) {
fetchNextPage();
}
}, [hasNextPage, fetchNextPage, allItems.length, virtualizer.getVirtualItems()]);
return (
{virtualizer.getVirtualItems().map((virtualRow) => (
{allItems[virtualRow.index] ? (
) : (
'Loading...'
)}
))}
);
}
```
## Parallel Queries Optimization
### Using useQueries
```tsx
// ❌ Sequential queries
async function fetchAllData() {
const users = await fetchUsers();
const posts = await fetchPosts();
const comments = await fetchComments();
return { users, posts, comments };
}
// ✅ Parallel queries
function Dashboard() {
const results = useQueries({
queries: [
{ queryKey: ['users'], queryFn: fetchUsers },
{ queryKey: ['posts'], queryFn: fetchPosts },
{ queryKey: ['comments'], queryFn: fetchComments },
],
});
const [users, posts, comments] = results;
const isLoading = results.some(r => r.isLoading);
return {/* render */}
;
}
```
### Dynamic Parallel Queries
```tsx
function UserPosts({ userIds }) {
const queries = useQueries({
queries: userIds.map(id => ({
queryKey: ['user-posts', id],
queryFn: () => fetchUserPosts(id),
staleTime: 1000 * 60 * 5,
})),
});
return {/* render */}
;
}
```
## Memory Management
### Limit Cache Size
```tsx
const queryClient = new QueryClient({
defaultOptions: {
queries: {
gcTime: 1000 * 60 * 5, // 5 minutes
},
},
});
// Manually clear old queries
queryClient.clear(); // Clear all cache
// Remove specific queries
queryClient.removeQueries({ queryKey: ['old-data'] });
```
### Remove Queries on Unmount
```tsx
function ExpensiveComponent() {
const { data } = useQuery({
queryKey: ['expensive-data'],
queryFn: fetchExpensiveData,
gcTime: 0, // Remove immediately when component unmounts
});
return {/* render */}
;
}
```
## Network Optimization
### Batch Requests
If your API supports batching:
```tsx
// Collect query keys and batch them
const batchedQueryFn = async (keys) => {
const ids = keys.map(key => key[1]);
const results = await fetch(`/api/items?ids=${ids.join(',')}`);
return results.json();
};
// Use in queries
useQuery({
queryKey: ['item', itemId],
queryFn: () => batchedQueryFn([['item', itemId]]),
});
```
### Request Cancellation
```tsx
useQuery({
queryKey: ['search', searchTerm],
queryFn: async ({ signal }) => {
// AbortSignal automatically provided
const res = await fetch(`/api/search?q=${searchTerm}`, { signal });
return res.json();
},
});
// When searchTerm changes, previous request is cancelled
```
### Retry Configuration
```tsx
// ❌ Retry immediately 3 times
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
retry: 3,
});
// ✅ Exponential backoff
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
retry: 3,
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
});
```
## Dependent Queries
Avoid waterfalls by enabling queries in parallel when possible:
```tsx
// ❌ Waterfall - queries run sequentially
function UserDashboard({ userId }) {
const { data: user } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
});
const { data: posts } = useQuery({
queryKey: ['posts', user?.id],
queryFn: () => fetchPosts(user.id),
enabled: !!user?.id, // Waits for user
});
const { data: comments } = useQuery({
queryKey: ['comments', user?.id],
queryFn: () => fetchComments(user.id),
enabled: !!user?.id, // Also waits for user
});
}
// ✅ Optimized - posts and comments fetch in parallel after user loads
```
## Code Splitting
### Lazy Load Query Client
```tsx
import { lazy, Suspense } from 'react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
const ReactQueryDevtools = lazy(() =>
import('@tanstack/react-query-devtools').then(mod => ({
default: mod.ReactQueryDevtools,
}))
);
function App() {
return (
);
}
```
## Monitoring Performance
### Using DevTools
```tsx
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
function App() {
return (
);
}
```
### Custom Logger
```tsx
const queryClient = new QueryClient({
logger: {
log: (...args) => console.log(...args),
warn: (...args) => console.warn(...args),
error: (...args) => console.error(...args),
},
defaultOptions: {
queries: {
onSuccess: (data, query) => {
console.log(`Query ${query.queryKey} succeeded`, data);
},
onError: (error, query) => {
console.error(`Query ${query.queryKey} failed`, error);
},
},
},
});
```
### Performance Metrics
```tsx
useQuery({
queryKey: ['todos'],
queryFn: async () => {
const start = performance.now();
const data = await fetchTodos();
const duration = performance.now() - start;
console.log(`Query took ${duration}ms`);
return data;
},
});
```
## Best Practices
1. **Set Appropriate staleTime**
```tsx
// Static data
staleTime: Infinity
// Frequently changing
staleTime: 0
// Moderate
staleTime: 1000 * 60 * 5 // 5 minutes
```
2. **Use Prefetching**
- Hover intent
- Route prediction
- Next page in pagination
3. **Optimize with select**
```tsx
select: (data) => data.filter(/* ... */)
```
4. **Disable Unnecessary Refetching**
```tsx
refetchOnWindowFocus: false
refetchOnReconnect: false
```
5. **Use Structural Sharing**
- Enabled by default
- Disable for very large datasets
6. **Implement Virtual Scrolling**
- For long lists
- For infinite queries
7. **Monitor with DevTools**
- Watch for unnecessary refetches
- Check cache effectiveness
- Identify slow queries
8. **Batch Parallel Queries**
- Use useQueries
- Reduce waterfalls
9. **Clean Up Unused Cache**
```tsx
gcTime: 1000 * 60 * 5
```
10. **Use Request Cancellation**
- Automatically handled by TanStack Query
- Ensures old requests don't override new ones