Initial commit
This commit is contained in:
653
skills/skill/references/performance.md
Normal file
653
skills/skill/references/performance.md
Normal file
@@ -0,0 +1,653 @@
|
||||
# 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 (
|
||||
<Link
|
||||
to={`/todo/${todo.id}`}
|
||||
onMouseEnter={prefetchTodo}
|
||||
onFocus={prefetchTodo}
|
||||
>
|
||||
{todo.title}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 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 <Outlet />;
|
||||
}
|
||||
```
|
||||
|
||||
### 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 <div>{/* render items */}</div>;
|
||||
}
|
||||
```
|
||||
|
||||
## 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 <div>{completedTodos?.map(/* ... */)}</div>;
|
||||
}
|
||||
|
||||
// ✅ Transform with select - memoized automatically
|
||||
function TodoList() {
|
||||
const { data: completedTodos } = useQuery({
|
||||
queryKey: ['todos'],
|
||||
queryFn: fetchTodos,
|
||||
select: (todos) => todos.filter(todo => todo.completed),
|
||||
});
|
||||
|
||||
return <div>{completedTodos?.map(/* ... */)}</div>;
|
||||
}
|
||||
```
|
||||
|
||||
### 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 (
|
||||
<div>
|
||||
{data?.items.map(todo => <TodoItem key={todo.id} todo={todo} />)}
|
||||
<button onClick={() => setPage(p => p - 1)} disabled={page === 1}>
|
||||
Previous
|
||||
</button>
|
||||
<button onClick={() => setPage(p => p + 1)}>
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 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 (
|
||||
<div ref={parentRef} style={{ height: '500px', overflow: 'auto' }}>
|
||||
<div style={{ height: `${virtualizer.getTotalSize()}px` }}>
|
||||
{virtualizer.getVirtualItems().map((virtualRow) => (
|
||||
<div
|
||||
key={virtualRow.index}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: `${virtualRow.size}px`,
|
||||
transform: `translateY(${virtualRow.start}px)`,
|
||||
}}
|
||||
>
|
||||
{allItems[virtualRow.index] ? (
|
||||
<Item item={allItems[virtualRow.index]} />
|
||||
) : (
|
||||
'Loading...'
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## 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 <div>{/* render */}</div>;
|
||||
}
|
||||
```
|
||||
|
||||
### 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 <div>{/* render */}</div>;
|
||||
}
|
||||
```
|
||||
|
||||
## 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 <div>{/* render */}</div>;
|
||||
}
|
||||
```
|
||||
|
||||
## 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 (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<YourApp />
|
||||
<Suspense fallback={null}>
|
||||
<ReactQueryDevtools />
|
||||
</Suspense>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Monitoring Performance
|
||||
|
||||
### Using DevTools
|
||||
|
||||
```tsx
|
||||
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<YourApp />
|
||||
<ReactQueryDevtools
|
||||
initialIsOpen={false}
|
||||
position="bottom-right"
|
||||
/>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 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
|
||||
Reference in New Issue
Block a user