Performance Optimization
React 19 Performance Patterns
Code Splitting with Lazy Loading
import { lazy, Suspense } from "react";
// Lazy load heavy components const HeavyEditor = lazy(() => import("./HeavyEditor")); const DataVisualization = lazy(() => import("./DataVisualization"));
function Dashboard() { return ( <Suspense fallback={<LoadingSkeleton />}> <HeavyEditor /> </Suspense> ); }
Strategic Memoization
import { memo, useMemo, useCallback, useDeferredValue } from "react";
// Memoize expensive computations const expensiveResult = useMemo(() => { return processLargeDataset(data); }, [data]);
// Memoize callbacks passed to children const handleClick = useCallback((id: number) => { updateItem(id); }, [updateItem]);
// Defer non-critical updates const deferredSearchTerm = useDeferredValue(searchTerm);
// Memoize components that receive stable props const MemoizedList = memo(function List({ items }: { items: Item[] }) { return items.map(item => <ListItem key={item.id} item={item} />); });
Avoid Re-renders
// BAD: Creates new object every render <Component style={{ color: "red" }} />
// GOOD: Define outside or memoize const style = { color: "red" }; <Component style={style} />
// BAD: Creates new function every render <Button onClick={() => handleClick(id)} />
// GOOD: Use useCallback or define handler const handleButtonClick = useCallback(() => handleClick(id), [id]); <Button onClick={handleButtonClick} />
TanStack Query Optimization
Caching Configuration
import { queryOptions } from "@tanstack/react-query";
// Configure stale time based on data freshness needs export const segmentsQueryOptions = () => queryOptions({ queryKey: ["segments"], queryFn: () => getSegmentsFn(), staleTime: 5 * 60 * 1000, // 5 minutes gcTime: 30 * 60 * 1000, // 30 minutes cache });
// For frequently changing data export const statsQueryOptions = () => queryOptions({ queryKey: ["stats"], queryFn: () => getStatsFn(), staleTime: 30 * 1000, // 30 seconds refetchInterval: 60 * 1000, // Auto-refetch every minute });
Prefetching
// Prefetch on route load export const Route = createFileRoute("/courses/$courseId")({ loader: async ({ params, context }) => { await context.queryClient.ensureQueryData( courseQueryOptions(params.courseId) ); }, component: CoursePage, });
// Prefetch on hover function CourseCard({ course }) { const queryClient = useQueryClient();
const handleMouseEnter = () => { queryClient.prefetchQuery(courseQueryOptions(course.id)); };
return (
<Link to={/courses/${course.id}} onMouseEnter={handleMouseEnter}>
{course.title}
</Link>
);
}
Optimistic Updates
const mutation = useMutation({ mutationFn: updateSegmentFn, onMutate: async (newData) => { await queryClient.cancelQueries({ queryKey: ["segments"] }); const previous = queryClient.getQueryData(["segments"]);
queryClient.setQueryData(["segments"], (old) =>
old.map((s) => (s.id === newData.id ? { ...s, ...newData } : s))
);
return { previous };
}, onError: (err, newData, context) => { queryClient.setQueryData(["segments"], context.previous); }, onSettled: () => { queryClient.invalidateQueries({ queryKey: ["segments"] }); }, });
Database Query Optimization
Select Only Required Columns
// BAD: Fetches all columns const users = await database.select().from(users);
// GOOD: Select only what you need const users = await database .select({ id: users.id, name: users.name, email: users.email }) .from(users);
Use Indexes Effectively
// Ensure indexes exist for frequently queried columns // In schema definition: export const users = pgTable("users", { id: serial("id").primaryKey(), email: varchar("email", { length: 255 }).notNull().unique(), createdAt: timestamp("created_at").defaultNow(), }, (table) => ({ emailIdx: index("email_idx").on(table.email), createdAtIdx: index("created_at_idx").on(table.createdAt), }));
Avoid N+1 Queries
// BAD: N+1 query pattern const segments = await getSegments(); for (const segment of segments) { segment.attachments = await getAttachments(segment.id); }
// GOOD: Single query with join const segmentsWithAttachments = await database .select() .from(segments) .leftJoin(attachments, eq(segments.id, attachments.segmentId));
// Or use Drizzle's with clause const result = await database.query.segments.findMany({ with: { attachments: true, module: true, }, });
Pagination
export async function getPaginatedUsers(page: number, limit: number) { const offset = (page - 1) * limit;
const [data, [{ count }]] = await Promise.all([
database
.select()
.from(users)
.orderBy(desc(users.createdAt))
.limit(limit)
.offset(offset),
database.select({ count: sqlcount(*) }).from(users),
]);
return { data, pagination: { page, limit, total: Number(count), totalPages: Math.ceil(Number(count) / limit), }, }; }
Bundle Optimization
Vite Configuration
// vite.config.ts export default defineConfig({ build: { rollupOptions: { output: { manualChunks: { // Split vendor chunks "react-vendor": ["react", "react-dom"], "query-vendor": ["@tanstack/react-query"], "ui-vendor": ["@radix-ui/react-dialog", "@radix-ui/react-dropdown-menu"], }, }, }, // Enable minification minify: "terser", terserOptions: { compress: { drop_console: true, // Remove console.logs in production }, }, }, });
Dynamic Imports for Large Libraries
// Dynamically import heavy libraries async function processMarkdown(content: string) { const { marked } = await import("marked"); return marked(content); }
// For React components const RichTextEditor = lazy(() => import("./RichTextEditor")); const ChartComponent = lazy(() => import("recharts").then(mod => ({ default: mod.ResponsiveContainer })));
Image & Media Optimization
Lazy Loading Images
function OptimizedImage({ src, alt }: { src: string; alt: string }) { return ( <img src={src} alt={alt} loading="lazy" decoding="async" /> ); }
Video Loading Strategy
function VideoPlayer({ videoKey }: { videoKey: string }) { const [videoUrl, setVideoUrl] = useState<string | null>(null);
// Load video URL only when component is visible useEffect(() => { const observer = new IntersectionObserver((entries) => { if (entries[0].isIntersecting) { loadVideoUrl(videoKey).then(setVideoUrl); observer.disconnect(); } });
observer.observe(containerRef.current);
return () => observer.disconnect();
}, [videoKey]);
return videoUrl ? <video src={videoUrl} controls /> : <VideoSkeleton />; }
Tailwind CSS Optimization
Purge Unused CSS
Tailwind v4 automatically purges unused styles in production. Ensure your content paths are correct:
// tailwind.config.ts export default { content: [ "./src//*.{js,ts,jsx,tsx}", "./src/components//*.{js,ts,jsx,tsx}", ], };
Use CSS Variables for Theming
/* Avoid redundant utility classes */ :root { --primary: theme(colors.blue.600); --primary-hover: theme(colors.blue.700); }
Memory Leak Prevention
Cleanup Effects
useEffect(() => { const controller = new AbortController();
fetchData({ signal: controller.signal }) .then(setData) .catch((err) => { if (err.name !== "AbortError") { setError(err); } });
return () => controller.abort(); }, []);
Event Listener Cleanup
useEffect(() => { const handleResize = () => setWidth(window.innerWidth); window.addEventListener("resize", handleResize); return () => window.removeEventListener("resize", handleResize); }, []);
Performance Checklist
-
Heavy components are lazy loaded with Suspense
-
TanStack Query has appropriate staleTime/gcTime
-
Database queries select only needed columns
-
Joins are used instead of N+1 queries
-
Large lists are paginated
-
Bundle chunks are optimized for caching
-
Images use loading="lazy"
-
Effects clean up subscriptions and abort controllers
-
Memoization is used strategically (not everywhere)
-
Prefetching is used for predictable navigation