Back to Articles
React TypeScript Performance Guide for 60-240 FPS UI

React TypeScript Performance Guide for 60-240 FPS UI

Comprehensive practices for building 60-240 FPS UI with React and TypeScript


Table of Contents

  1. React Rendering Fundamentals
  2. React 19 Compiler
  3. Component Optimization
  4. Memoization Patterns
  5. State Management
  6. Context Optimization
  7. Virtualization
  8. Concurrent Features
  9. Event Handling
  10. CSS & Styling Performance
  11. Image Optimization
  12. Code Splitting
  13. TypeScript Patterns
  14. Web Workers
  15. Profiling & Debugging
  16. Quick Reference

1. React Rendering Fundamentals

The Render Cycle

┌─────────────────────────────────────────────────────────────────────────────┐
│                          REACT RENDERING CYCLE                              │
├───────────────┬───────────────────┬─────────────────────────────────────────┤
│    TRIGGER    │      RENDER       │              COMMIT                     │
│ (State/Props) │ (Virtual DOM)     │            (Real DOM)                   │
├───────────────┼───────────────────┼─────────────────────────────────────────┤
│ • setState    │ • Call component  │ • Apply DOM changes                     │
│ • Props       │   functions       │ • Run useLayoutEffect                   │
│   change      │ • Diff old vs new │ • Paint to screen                       │
│ • Parent      │ • Calculate       │ • Run useEffect                         │
│   re-render   │   minimal updates │                                         │
└───────────────┴───────────────────┴─────────────────────────────────────────┘

Frame Budget

Target FPSFrame BudgetJS Budget (approx)
60 FPS16.67ms~10ms
120 FPS8.33ms~5ms
240 FPS4.17ms~3ms

What Triggers Re-renders

// 1. State change
const [count, setCount] = useState(0);
setCount(1);  // → Re-render

// 2. Props change
<Child data={newData} />  // → Child re-renders

// 3. Parent re-render
function Parent() {
  const [, forceUpdate] = useState({});
  // When Parent re-renders, Child re-renders too
  return <Child />;
}

// 4. Context change
const ThemeContext = createContext('light');
// All consumers re-render when context value changes

Virtual DOM Reconciliation

// React compares virtual DOM trees
// Key rules for efficient reconciliation:

// 1. Same type → update props
<div className="old" />  →  <div className="new" />  // Update class

// 2. Different type → unmount and remount
<div />  →  <span />  // Complete replacement

// 3. Keys identify elements in lists
{items.map(item => <Item key={item.id} />)}  // Track by ID

2. React 19 Compiler

Automatic Optimization

React 19’s compiler automatically handles what you previously did manually:

// ❌ Pre-React 19: Manual memoization everywhere
const MemoizedComponent = React.memo(function Component({ data }: Props) {
  const processed = useMemo(() => expensiveOp(data), [data]);
  const handler = useCallback(() => doAction(data), [data]);

  return (
    <div onClick={handler}>
      {processed.map(item => <Item key={item.id} item={item} />)}
    </div>
  );
});

// ✅ React 19: Just write simple code
function Component({ data }: Props) {
  const processed = expensiveOp(data);
  const handler = () => doAction(data);

  return (
    <div onClick={handler}>
      {processed.map(item => <Item key={item.id} item={item} />)}
    </div>
  );
}
// Compiler automatically:
// - Memoizes component
// - Memoizes expensive calculations
// - Stabilizes function references

When Manual Optimization Still Matters

// ✅ Still use useMemo for:

// 1. Third-party libraries requiring stable references
const mapLayer = useMemo(
  () => createMapboxLayer(geoData),
  [geoData]
);

// 2. Truly expensive O(n²) or worse operations
const relationships = useMemo(() => {
  return items.flatMap(item =>
    items.map(other => computeRelationship(item, other))
  );
}, [items]);

// 3. Objects passed to non-React APIs
const chartConfig = useMemo(
  () => ({ width: 800, height: 600, theme: currentTheme }),
  [currentTheme]
);

// 4. Reference equality for effect dependencies
const options = useMemo(
  () => ({ retry: 3, timeout: 5000 }),
  []
);
useEffect(() => {
  fetchData(options);
}, [options]);

Checking Compiler Status

// Check if compiler is optimizing
if (process.env.NODE_ENV === 'development') {
  // React DevTools shows "compiled" badge
}

// babel.config.js for React 19
module.exports = {
  plugins: [
    ['babel-plugin-react-compiler', {
      // Configuration
    }]
  ]
};

3. Component Optimization

Isolate State to Smallest Scope

// ❌ BAD: State at top level causes cascade re-renders
function ProductPage({ product }: Props) {
  const [quantity, setQuantity] = useState(1);  // Changes often

  return (
    <div>
      <ProductHeader product={product} />      {/* Re-renders! */}
      <ProductGallery images={product.images} /> {/* Re-renders! */}
      <QuantitySelector
        value={quantity}
        onChange={setQuantity}
      />
      <ProductReviews productId={product.id} /> {/* Re-renders! */}
    </div>
  );
}

// ✅ GOOD: State isolated in child
function ProductPage({ product }: Props) {
  return (
    <div>
      <ProductHeader product={product} />      {/* Never re-renders */}
      <ProductGallery images={product.images} /> {/* Never re-renders */}
      <QuantitySection product={product} />    {/* Only this re-renders */}
      <ProductReviews productId={product.id} /> {/* Never re-renders */}
    </div>
  );
}

function QuantitySection({ product }: Props) {
  const [quantity, setQuantity] = useState(1);  // Isolated here

  return (
    <>
      <QuantitySelector value={quantity} onChange={setQuantity} />
      <AddToCartButton product={product} quantity={quantity} />
    </>
  );
}

React.memo for Expensive Children

// ✅ Wrap components that receive stable props but have expensive renders
interface ProductCardProps {
  product: Product;
  onAddToCart: (id: string) => void;
}

const ProductCard = memo(function ProductCard({
  product,
  onAddToCart
}: ProductCardProps) {
  // Expensive rendering
  return (
    <div className="product-card">
      <img src={product.image} alt={product.name} />
      <h3>{product.name}</h3>
      <p>{product.description}</p>
      <button onClick={() => onAddToCart(product.id)}>
        Add to Cart
      </button>
    </div>
  );
});

Custom Comparison Function

// ✅ For complex props, provide custom comparison
interface ChartProps {
  data: DataPoint[];
  config: ChartConfig;
}

const Chart = memo(
  function Chart({ data, config }: ChartProps) {
    return <ExpensiveChartRender data={data} config={config} />;
  },
  (prevProps, nextProps) => {
    // Return true if props are equal (skip re-render)
    return (
      prevProps.data.length === nextProps.data.length &&
      prevProps.config.theme === nextProps.config.theme &&
      prevProps.data === nextProps.data  // Reference equality for immutable data
    );
  }
);

Component Composition Pattern

// ✅ Use children to avoid re-rendering static content
function ExpensiveLayout({ children }: { children: ReactNode }) {
  // This component might re-render...
  const [, forceUpdate] = useState({});

  return (
    <div className="layout">
      <Header />
      {children}  {/* Children are passed by reference, not re-created */}
      <Footer />
    </div>
  );
}

// Usage
function App() {
  return (
    <ExpensiveLayout>
      <StaticContent />  {/* This won't re-render when layout does */}
    </ExpensiveLayout>
  );
}

4. Memoization Patterns

useMemo for Expensive Calculations

// ✅ Memoize truly expensive operations
function SearchResults({ items, query }: Props) {
  const filteredItems = useMemo(() => {
    console.log('Filtering...');  // Only logs when items or query change
    return items.filter(item =>
      item.name.toLowerCase().includes(query.toLowerCase())
    );
  }, [items, query]);

  const sortedItems = useMemo(() => {
    console.log('Sorting...');
    return [...filteredItems].sort((a, b) => a.name.localeCompare(b.name));
  }, [filteredItems]);

  return (
    <ul>
      {sortedItems.map(item => (
        <li key={item.id}>{item.name}</li>
      ))}
    </ul>
  );
}

When NOT to useMemo

// ❌ Don't memoize simple operations
const fullName = useMemo(
  () => `${firstName} ${lastName}`,  // String concat is cheap
  [firstName, lastName]
);

// ✅ Just compute directly
const fullName = `${firstName} ${lastName}`;

// ❌ Don't memoize primitive values
const isAdult = useMemo(() => age >= 18, [age]);

// ✅ Just compute directly
const isAdult = age >= 18;

useCallback for Stable References

// ✅ Stabilize callbacks passed to memoized children
function ParentList({ items }: Props) {
  const [selectedId, setSelectedId] = useState<string | null>(null);

  const handleSelect = useCallback((id: string) => {
    setSelectedId(id);
  }, []);

  const handleDelete = useCallback((id: string) => {
    // Delete logic
  }, []);

  return (
    <ul>
      {items.map(item => (
        <MemoizedItem
          key={item.id}
          item={item}
          isSelected={selectedId === item.id}
          onSelect={handleSelect}
          onDelete={handleDelete}
        />
      ))}
    </ul>
  );
}

const MemoizedItem = memo(function Item({
  item,
  isSelected,
  onSelect,
  onDelete
}: ItemProps) {
  return (
    <li className={isSelected ? 'selected' : ''}>
      <span onClick={() => onSelect(item.id)}>{item.name}</span>
      <button onClick={() => onDelete(item.id)}>Delete</button>
    </li>
  );
});

Memoization Cost Analysis

// useMemo has overhead:
// 1. Store previous deps in memory
// 2. Compare deps on every render
// 3. Store previous result

// Only beneficial when:
// - Computation is expensive (> 1ms)
// - Dependencies rarely change
// - Result is passed to memoized children

// Measure before adding useMemo!
console.time('computation');
const result = expensiveOperation(data);
console.timeEnd('computation');  // Check if > 1ms

5. State Management

Performance Comparison

SolutionRe-render ScopeBundle SizeBest For
useStateComponent + children0KBLocal state
useReducerComponent + children0KBComplex local state
ContextAll consumers0KBTheme, auth
ZustandSelected slices only~1KBApp state
JotaiPer-atom~2KBFine-grained
Redux ToolkitSelected slices~10KBLarge apps
TanStack QueryPer-query~12KBServer state

Zustand with Selectors

import { create } from 'zustand';
import { shallow } from 'zustand/shallow';

interface Store {
  user: User | null;
  cart: CartItem[];
  orders: Order[];
  addToCart: (item: CartItem) => void;
  removeFromCart: (id: string) => void;
  setUser: (user: User | null) => void;
}

const useStore = create<Store>((set) => ({
  user: null,
  cart: [],
  orders: [],

  addToCart: (item) => set((state) => ({
    cart: [...state.cart, item]
  })),

  removeFromCart: (id) => set((state) => ({
    cart: state.cart.filter(item => item.id !== id)
  })),

  setUser: (user) => set({ user }),
}));

// ✅ Select only what you need
function CartIcon() {
  // Only re-renders when cart length changes
  const itemCount = useStore((state) => state.cart.length);
  return <Badge count={itemCount} />;
}

function CartTotal() {
  // Only re-renders when total changes
  const total = useStore((state) =>
    state.cart.reduce((sum, item) => sum + item.price * item.quantity, 0)
  );
  return <span>${total.toFixed(2)}</span>;
}

// ✅ Multiple selectors with shallow comparison
function CartSummary() {
  const { itemCount, total } = useStore(
    (state) => ({
      itemCount: state.cart.length,
      total: state.cart.reduce((sum, item) => sum + item.price, 0),
    }),
    shallow  // Compare object values, not reference
  );

  return (
    <div>
      {itemCount} items - ${total.toFixed(2)}
    </div>
  );
}

TanStack Query for Server State

import {
  useQuery,
  useMutation,
  useQueryClient,
  QueryClient,
  QueryClientProvider
} from '@tanstack/react-query';

// Setup
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 5 * 60 * 1000,  // 5 minutes
      gcTime: 10 * 60 * 1000,    // 10 minutes (garbage collection)
      retry: 3,
      refetchOnWindowFocus: false,
    },
  },
});

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <MyApp />
    </QueryClientProvider>
  );
}

// ✅ Fetch with caching
function ProductList() {
  const { data, isLoading, error } = useQuery({
    queryKey: ['products'],
    queryFn: fetchProducts,
  });

  if (isLoading) return <Skeleton />;
  if (error) return <Error error={error} />;

  return (
    <ul>
      {data?.map(product => (
        <ProductItem key={product.id} product={product} />
      ))}
    </ul>
  );
}

// ✅ Mutations with optimistic updates
function AddToCartButton({ product }: Props) {
  const queryClient = useQueryClient();

  const mutation = useMutation({
    mutationFn: addToCart,

    // Optimistic update
    onMutate: async (newItem) => {
      await queryClient.cancelQueries({ queryKey: ['cart'] });
      const previous = queryClient.getQueryData(['cart']);
      queryClient.setQueryData(['cart'], (old: CartItem[]) => [...old, newItem]);
      return { previous };
    },

    onError: (err, newItem, context) => {
      queryClient.setQueryData(['cart'], context?.previous);
    },

    onSettled: () => {
      queryClient.invalidateQueries({ queryKey: ['cart'] });
    },
  });

  return (
    <button
      onClick={() => mutation.mutate(product)}
      disabled={mutation.isPending}
    >
      {mutation.isPending ? 'Adding...' : 'Add to Cart'}
    </button>
  );
}

State Colocation

// ✅ Keep state as close to usage as possible

// Form state - local to form
function ContactForm() {
  const [formData, setFormData] = useState({
    name: '',
    email: '',
    message: ''
  });

  return <form>...</form>;
}

// UI state - local to component
function Dropdown() {
  const [isOpen, setIsOpen] = useState(false);
  return <div>...</div>;
}

// Shared state - lifted to common ancestor or global store
function App() {
  return (
    <CartProvider>
      <Header />  {/* Uses cart */}
      <Products />  {/* Uses cart */}
      <Checkout />  {/* Uses cart */}
    </CartProvider>
  );
}

6. Context Optimization

The Problem with Context

// ❌ BAD: Single context with everything
interface AppState {
  user: User | null;
  theme: Theme;
  cart: CartItem[];
  notifications: Notification[];
}

const AppContext = createContext<AppState>(initialState);

// When ANYTHING changes, ALL consumers re-render!
function CartIcon() {
  const { cart } = useContext(AppContext);  // Re-renders on theme change too!
  return <Badge count={cart.length} />;
}

Split Contexts by Update Frequency

// ✅ GOOD: Separate contexts by how often they change

// Rarely changes
const UserContext = createContext<User | null>(null);
const ThemeContext = createContext<Theme>(defaultTheme);

// Changes often
const CartContext = createContext<CartState>(initialCart);
const NotificationContext = createContext<Notification[]>([]);

// Compose providers
function AppProviders({ children }: { children: ReactNode }) {
  return (
    <UserProvider>
      <ThemeProvider>
        <CartProvider>
          <NotificationProvider>
            {children}
          </NotificationProvider>
        </CartProvider>
      </ThemeProvider>
    </UserProvider>
  );
}

Memoize Context Value

// ❌ BAD: New object every render
function CartProvider({ children }: { children: ReactNode }) {
  const [items, setItems] = useState<CartItem[]>([]);

  return (
    <CartContext.Provider value={{ items, setItems }}>  {/* New object! */}
      {children}
    </CartContext.Provider>
  );
}

// ✅ GOOD: Memoized value
function CartProvider({ children }: { children: ReactNode }) {
  const [items, setItems] = useState<CartItem[]>([]);

  const value = useMemo(
    () => ({ items, setItems }),
    [items]
  );

  return (
    <CartContext.Provider value={value}>
      {children}
    </CartContext.Provider>
  );
}

Split State and Dispatch

// ✅ Separate read and write contexts
interface CartState {
  items: CartItem[];
  total: number;
}

type CartDispatch = (action: CartAction) => void;

const CartStateContext = createContext<CartState | null>(null);
const CartDispatchContext = createContext<CartDispatch | null>(null);

function CartProvider({ children }: { children: ReactNode }) {
  const [state, dispatch] = useReducer(cartReducer, initialState);

  return (
    <CartStateContext.Provider value={state}>
      <CartDispatchContext.Provider value={dispatch}>
        {children}
      </CartDispatchContext.Provider>
    </CartStateContext.Provider>
  );
}

// Components that only dispatch don't re-render on state changes
function AddToCartButton({ product }: Props) {
  const dispatch = useContext(CartDispatchContext);  // Stable reference
  return (
    <button onClick={() => dispatch({ type: 'ADD', product })}>
      Add
    </button>
  );
}

// Components that read state re-render as expected
function CartTotal() {
  const state = useContext(CartStateContext);
  return <span>${state?.total.toFixed(2)}</span>;
}

Context Selectors (use-context-selector)

import { createContext, useContextSelector } from 'use-context-selector';

const StoreContext = createContext<Store>(initialStore);

// ✅ Only re-renders when selected value changes
function UserName() {
  const name = useContextSelector(
    StoreContext,
    (state) => state.user?.name
  );
  return <span>{name}</span>;
}

function CartCount() {
  const count = useContextSelector(
    StoreContext,
    (state) => state.cart.length
  );
  return <Badge count={count} />;
}

7. Virtualization

import { useVirtualizer } from '@tanstack/react-virtual';

interface Item {
  id: string;
  name: string;
  description: string;
}

function VirtualList({ items }: { items: Item[] }) {
  const parentRef = useRef<HTMLDivElement>(null);

  const virtualizer = useVirtualizer({
    count: items.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 50,  // Estimated row height
    overscan: 5,  // Extra items to render above/below
  });

  return (
    <div
      ref={parentRef}
      style={{
        height: '400px',
        overflow: 'auto',
      }}
    >
      <div
        style={{
          height: `${virtualizer.getTotalSize()}px`,
          width: '100%',
          position: 'relative',
        }}
      >
        {virtualizer.getVirtualItems().map((virtualRow) => (
          <div
            key={virtualRow.key}
            style={{
              position: 'absolute',
              top: 0,
              left: 0,
              width: '100%',
              height: `${virtualRow.size}px`,
              transform: `translateY(${virtualRow.start}px)`,
            }}
          >
            <ItemRow item={items[virtualRow.index]} />
          </div>
        ))}
      </div>
    </div>
  );
}

Variable Height Items

function VariableHeightList({ items }: { items: Item[] }) {
  const parentRef = useRef<HTMLDivElement>(null);

  const virtualizer = useVirtualizer({
    count: items.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 100,  // Initial estimate

    // Measure actual size
    measureElement: (element) => {
      return element.getBoundingClientRect().height;
    },
  });

  return (
    <div ref={parentRef} style={{ height: '400px', overflow: 'auto' }}>
      <div
        style={{
          height: `${virtualizer.getTotalSize()}px`,
          position: 'relative',
        }}
      >
        {virtualizer.getVirtualItems().map((virtualRow) => (
          <div
            key={virtualRow.key}
            ref={virtualizer.measureElement}
            data-index={virtualRow.index}
            style={{
              position: 'absolute',
              top: 0,
              left: 0,
              width: '100%',
              transform: `translateY(${virtualRow.start}px)`,
            }}
          >
            <DynamicHeightItem item={items[virtualRow.index]} />
          </div>
        ))}
      </div>
    </div>
  );
}

Virtual Grid

function VirtualGrid({ items, columns = 4 }: Props) {
  const parentRef = useRef<HTMLDivElement>(null);
  const rowCount = Math.ceil(items.length / columns);

  const rowVirtualizer = useVirtualizer({
    count: rowCount,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 200,
    overscan: 2,
  });

  return (
    <div ref={parentRef} style={{ height: '600px', overflow: 'auto' }}>
      <div
        style={{
          height: `${rowVirtualizer.getTotalSize()}px`,
          position: 'relative',
        }}
      >
        {rowVirtualizer.getVirtualItems().map((virtualRow) => (
          <div
            key={virtualRow.key}
            style={{
              position: 'absolute',
              top: 0,
              left: 0,
              width: '100%',
              height: `${virtualRow.size}px`,
              transform: `translateY(${virtualRow.start}px)`,
              display: 'grid',
              gridTemplateColumns: `repeat(${columns}, 1fr)`,
              gap: '16px',
            }}
          >
            {Array.from({ length: columns }).map((_, colIndex) => {
              const itemIndex = virtualRow.index * columns + colIndex;
              if (itemIndex >= items.length) return null;
              return (
                <GridItem
                  key={items[itemIndex].id}
                  item={items[itemIndex]}
                />
              );
            })}
          </div>
        ))}
      </div>
    </div>
  );
}

Infinite Scroll

function InfiniteList() {
  const parentRef = useRef<HTMLDivElement>(null);

  const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteQuery({
    queryKey: ['items'],
    queryFn: ({ pageParam = 0 }) => fetchItems(pageParam),
    getNextPageParam: (lastPage) => lastPage.nextCursor,
  });

  const allItems = data?.pages.flatMap(page => page.items) ?? [];

  const virtualizer = useVirtualizer({
    count: hasNextPage ? allItems.length + 1 : allItems.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 50,
    overscan: 5,
  });

  // Fetch more when approaching end
  useEffect(() => {
    const [lastItem] = [...virtualizer.getVirtualItems()].reverse();

    if (!lastItem) return;

    if (
      lastItem.index >= allItems.length - 1 &&
      hasNextPage &&
      !isFetchingNextPage
    ) {
      fetchNextPage();
    }
  }, [
    hasNextPage,
    fetchNextPage,
    allItems.length,
    isFetchingNextPage,
    virtualizer.getVirtualItems(),
  ]);

  return (
    <div ref={parentRef} style={{ height: '400px', overflow: 'auto' }}>
      <div
        style={{
          height: `${virtualizer.getTotalSize()}px`,
          position: 'relative',
        }}
      >
        {virtualizer.getVirtualItems().map((virtualRow) => {
          const isLoaderRow = virtualRow.index > allItems.length - 1;

          return (
            <div
              key={virtualRow.key}
              style={{
                position: 'absolute',
                top: 0,
                left: 0,
                width: '100%',
                height: `${virtualRow.size}px`,
                transform: `translateY(${virtualRow.start}px)`,
              }}
            >
              {isLoaderRow ? (
                hasNextPage ? <Spinner /> : 'Nothing more to load'
              ) : (
                <ItemRow item={allItems[virtualRow.index]} />
              )}
            </div>
          );
        })}
      </div>
    </div>
  );
}

8. Concurrent Features

useTransition for Non-Blocking Updates

import { useState, useTransition } from 'react';

function SearchPage() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState<Item[]>([]);
  const [isPending, startTransition] = useTransition();

  const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
    const value = e.target.value;

    // Urgent: Update input immediately
    setQuery(value);

    // Non-urgent: Can be interrupted
    startTransition(() => {
      const filtered = filterItems(value);  // Expensive
      setResults(filtered);
    });
  };

  return (
    <div>
      <input value={query} onChange={handleChange} />

      {isPending && <Spinner />}

      <ul style={{ opacity: isPending ? 0.7 : 1 }}>
        {results.map(item => (
          <li key={item.id}>{item.name}</li>
        ))}
      </ul>
    </div>
  );
}

useDeferredValue for Expensive Children

import { useDeferredValue, Suspense, memo } from 'react';

function SearchPage() {
  const [query, setQuery] = useState('');
  const deferredQuery = useDeferredValue(query);

  const isStale = query !== deferredQuery;

  return (
    <div>
      <input
        value={query}
        onChange={(e) => setQuery(e.target.value)}
      />

      <Suspense fallback={<Skeleton />}>
        <div style={{ opacity: isStale ? 0.7 : 1 }}>
          <SearchResults query={deferredQuery} />
        </div>
      </Suspense>
    </div>
  );
}

// Memoized to avoid re-render with same query
const SearchResults = memo(function SearchResults({ query }: { query: string }) {
  const results = useSearchResults(query);  // Could be expensive
  return (
    <ul>
      {results.map(item => (
        <li key={item.id}>{item.name}</li>
      ))}
    </ul>
  );
});

Suspense for Data Fetching

import { Suspense } from 'react';
import { useSuspenseQuery } from '@tanstack/react-query';

function ProductPage({ id }: { id: string }) {
  return (
    <div>
      <Suspense fallback={<HeaderSkeleton />}>
        <ProductHeader id={id} />
      </Suspense>

      <Suspense fallback={<DetailsSkeleton />}>
        <ProductDetails id={id} />
      </Suspense>

      <Suspense fallback={<ReviewsSkeleton />}>
        <ProductReviews id={id} />
      </Suspense>
    </div>
  );
}

function ProductDetails({ id }: { id: string }) {
  const { data } = useSuspenseQuery({
    queryKey: ['product', id],
    queryFn: () => fetchProduct(id),
  });

  return (
    <div>
      <h1>{data.name}</h1>
      <p>{data.description}</p>
      <span>${data.price}</span>
    </div>
  );
}

Parallel Data Loading

// ✅ Load data in parallel using Suspense
function Dashboard() {
  return (
    <div className="dashboard">
      {/* These load in parallel, not sequentially */}
      <Suspense fallback={<StatsSkeleton />}>
        <Stats />
      </Suspense>

      <Suspense fallback={<ChartSkeleton />}>
        <SalesChart />
      </Suspense>

      <Suspense fallback={<TableSkeleton />}>
        <RecentOrders />
      </Suspense>
    </div>
  );
}

9. Event Handling

Passive Event Listeners

// ✅ Passive listeners don't block scrolling
useEffect(() => {
  const handleScroll = () => {
    // Handle scroll
  };

  window.addEventListener('scroll', handleScroll, { passive: true });
  return () => window.removeEventListener('scroll', handleScroll);
}, []);

Debouncing

import { useMemo, useCallback, useEffect } from 'react';
import { debounce } from 'lodash-es';

function SearchInput({ onSearch }: Props) {
  const [query, setQuery] = useState('');

  // Memoize debounced function
  const debouncedSearch = useMemo(
    () => debounce((q: string) => onSearch(q), 300),
    [onSearch]
  );

  // Cleanup on unmount
  useEffect(() => {
    return () => {
      debouncedSearch.cancel();
    };
  }, [debouncedSearch]);

  const handleChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
    const value = e.target.value;
    setQuery(value);
    debouncedSearch(value);
  }, [debouncedSearch]);

  return <input value={query} onChange={handleChange} />;
}

Throttling

import { useMemo, useEffect } from 'react';
import { throttle } from 'lodash-es';

function ScrollTracker() {
  const throttledHandler = useMemo(
    () => throttle(() => {
      const scrollY = window.scrollY;
      // Track scroll position
    }, 100),  // Max once per 100ms
    []
  );

  useEffect(() => {
    window.addEventListener('scroll', throttledHandler, { passive: true });
    return () => {
      throttledHandler.cancel();
      window.removeEventListener('scroll', throttledHandler);
    };
  }, [throttledHandler]);

  return null;
}

requestAnimationFrame for Visual Updates

function SmoothScroll() {
  const [scrollY, setScrollY] = useState(0);
  const rafRef = useRef<number>();
  const scrollRef = useRef(0);

  useEffect(() => {
    const handleScroll = () => {
      scrollRef.current = window.scrollY;

      // Batch visual updates with RAF
      if (rafRef.current === undefined) {
        rafRef.current = requestAnimationFrame(() => {
          setScrollY(scrollRef.current);
          rafRef.current = undefined;
        });
      }
    };

    window.addEventListener('scroll', handleScroll, { passive: true });
    return () => {
      window.removeEventListener('scroll', handleScroll);
      if (rafRef.current) {
        cancelAnimationFrame(rafRef.current);
      }
    };
  }, []);

  return <div style={{ transform: `translateY(${scrollY * 0.5}px)` }} />;
}

Event Delegation

// ✅ Single handler for many elements
function List({ items }: Props) {
  const handleClick = useCallback((e: MouseEvent<HTMLUListElement>) => {
    const target = e.target as HTMLElement;
    const itemId = target.closest('[data-item-id]')?.getAttribute('data-item-id');

    if (itemId) {
      handleItemClick(itemId);
    }
  }, []);

  return (
    <ul onClick={handleClick}>
      {items.map(item => (
        <li key={item.id} data-item-id={item.id}>
          {item.name}
        </li>
      ))}
    </ul>
  );
}

10. CSS & Styling Performance

Zero-Runtime CSS Solutions

SolutionRuntime CostType SafetyBest For
Tailwind CSSZeroNo (but IDE support)Most projects
CSS ModulesZeroNoComponent isolation
vanilla-extractZeroYesType-safe styles
LinariaZeroPartialCSS-in-JS syntax
styled-componentsHighPartialAvoid if perf-critical
EmotionHighPartialAvoid if perf-critical
// ✅ Zero runtime, styles purged at build
function Button({ primary, children }: Props) {
  return (
    <button
      className={cn(
        'px-4 py-2 rounded font-medium transition-colors duration-200',
        primary
          ? 'bg-blue-500 hover:bg-blue-600 text-white'
          : 'bg-gray-200 hover:bg-gray-300 text-gray-800'
      )}
    >
      {children}
    </button>
  );
}

// Utility for conditional classes
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';

function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs));
}

CSS Modules

// Button.module.css
.button {
  padding: 0.5rem 1rem;
  border-radius: 0.25rem;
  font-weight: 500;
}

.primary {
  background-color: #3b82f6;
  color: white;
}

// Button.tsx
import styles from './Button.module.css';

function Button({ primary, children }: Props) {
  return (
    <button className={cn(styles.button, primary && styles.primary)}>
      {children}
    </button>
  );
}

Avoid Inline Style Objects

// ❌ BAD: New object every render
<div style={{ transform: `translateX(${x}px)` }} />

// ✅ GOOD: CSS custom properties
<div
  style={{ '--x': `${x}px` } as CSSProperties}
  className="transform translate-x-[var(--x)]"
/>

// ✅ BEST: CSS transitions
<div className={cn(
  'transition-transform duration-300',
  isOpen ? 'translate-x-0' : '-translate-x-full'
)} />

GPU-Accelerated Properties

/* ✅ GPU composited (fast) */
.animate {
  transform: translateX(100px);
  opacity: 0.5;
  filter: blur(2px);
}

/* ❌ Triggers layout (slow) */
.animate {
  left: 100px;
  width: 200px;
  height: 200px;
}

/* ✅ Use will-change sparingly */
.will-animate {
  will-change: transform;
}

240fps-Specific Optimizations

For 240Hz displays (gaming monitors, high-refresh devices), every millisecond counts. These advanced techniques are essential for sub-4ms frame budgets.

CSS Containment

/* Isolate components from parent reflows */
.isolated-component {
  contain: layout paint;
}

/* Skip rendering off-viewport content entirely */
.lazy-section {
  content-visibility: auto;
  contain-intrinsic-size: 0 500px; /* Placeholder size */
}

/* Full isolation for maximum performance */
.perf-critical {
  contain: strict;
}

will-change Lifecycle Management

// ✅ GOOD: Add before animation, remove after
function AnimatedCard({ isAnimating }: Props) {
  const ref = useRef<HTMLDivElement>(null);

  useEffect(() => {
    const element = ref.current;
    if (!element) return;

    if (isAnimating) {
      element.style.willChange = 'transform, opacity';
    }

    return () => {
      element.style.willChange = 'auto';
    };
  }, [isAnimating]);

  return <div ref={ref}>...</div>;
}

// ❌ BAD: Permanent will-change (wastes GPU memory)
.always-ready {
  will-change: transform; /* Don't do this */
}

OffscreenCanvas for Heavy Drawing

// Move canvas rendering to Web Worker
function HeavyVisualization({ data }: Props) {
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const workerRef = useRef<Worker>();

  useEffect(() => {
    const canvas = canvasRef.current;
    if (!canvas) return;

    const offscreen = canvas.transferControlToOffscreen();
    workerRef.current = new Worker(
      new URL('./canvas-worker.ts', import.meta.url),
      { type: 'module' }
    );

    workerRef.current.postMessage({ canvas: offscreen, data }, [offscreen]);

    return () => workerRef.current?.terminate();
  }, [data]);

  return <canvas ref={canvasRef} width={800} height={600} />;
}

// canvas-worker.ts
self.onmessage = (e: MessageEvent) => {
  const { canvas, data } = e.data;
  const ctx = canvas.getContext('2d');
  // All rendering happens off main thread
  renderVisualization(ctx, data);
};

Forced Synchronous Layout Prevention

// ❌ BAD: Layout thrashing (read-write-read pattern)
function BadResize() {
  elements.forEach(el => {
    const width = el.offsetWidth;      // Read (forces layout)
    el.style.width = `${width * 2}px`; // Write (invalidates layout)
    // Next read forces ANOTHER layout!
  });
}

// ✅ GOOD: Batch reads, then batch writes
function GoodResize() {
  // Phase 1: Read all
  const widths = elements.map(el => el.offsetWidth);

  // Phase 2: Write all
  elements.forEach((el, i) => {
    el.style.width = `${widths[i] * 2}px`;
  });
}

// ✅ BEST: Use requestAnimationFrame for DOM writes
function BestResize() {
  const widths = elements.map(el => el.offsetWidth);

  requestAnimationFrame(() => {
    elements.forEach((el, i) => {
      el.style.width = `${widths[i] * 2}px`;
    });
  });
}

Passive Event Listeners (Critical for 240fps)

// ✅ Passive listeners don't block compositor
useEffect(() => {
  const handleScroll = () => {
    // Handle scroll - cannot call preventDefault()
  };

  const handleWheel = () => {
    // Handle wheel - cannot call preventDefault()
  };

  window.addEventListener('scroll', handleScroll, { passive: true });
  window.addEventListener('wheel', handleWheel, { passive: true });
  window.addEventListener('touchmove', handleTouch, { passive: true });

  return () => {
    window.removeEventListener('scroll', handleScroll);
    window.removeEventListener('wheel', handleWheel);
    window.removeEventListener('touchmove', handleTouch);
  };
}, []);

11. Image Optimization

import Image from 'next/image';

function ProductImage({ src, alt }: Props) {
  return (
    <Image
      src={src}
      alt={alt}
      width={400}
      height={300}
      priority  // Preload for above-the-fold
      placeholder="blur"
      blurDataURL={generateBlurHash(src)}
    />
  );
}

// Responsive images
function ResponsiveHero() {
  return (
    <Image
      src="/hero.jpg"
      alt="Hero"
      fill
      sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
      style={{ objectFit: 'cover' }}
    />
  );
}

Native Lazy Loading

// ✅ Browser-native lazy loading
<img
  src={url}
  alt={alt}
  loading="lazy"        // Lazy load
  decoding="async"      // Non-blocking decode
  width={200}           // Prevents layout shift
  height={200}
/>

// ✅ With srcSet for responsive
<img
  src={url}
  srcSet={`
    ${url}?w=400 400w,
    ${url}?w=800 800w,
    ${url}?w=1200 1200w
  `}
  sizes="(max-width: 600px) 400px, (max-width: 1200px) 800px, 1200px"
  alt={alt}
  loading="lazy"
/>

Image Preloading

// Preload critical images
function preloadImage(src: string) {
  const link = document.createElement('link');
  link.rel = 'preload';
  link.as = 'image';
  link.href = src;
  document.head.appendChild(link);
}

// In component
useEffect(() => {
  preloadImage('/hero.jpg');
}, []);

// Or in HTML head
<link rel="preload" as="image" href="/hero.jpg" />

12. Code Splitting

Route-Based Splitting

import { lazy, Suspense } from 'react';
import { Routes, Route } from 'react-router-dom';

// Lazy load route components
const Home = lazy(() => import('./pages/Home'));
const Products = lazy(() => import('./pages/Products'));
const ProductDetail = lazy(() => import('./pages/ProductDetail'));
const Admin = lazy(() => import('./pages/Admin'));

function App() {
  return (
    <Suspense fallback={<PageSkeleton />}>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/products" element={<Products />} />
        <Route path="/products/:id" element={<ProductDetail />} />
        <Route path="/admin/*" element={<Admin />} />
      </Routes>
    </Suspense>
  );
}

Component-Level Splitting

// Lazy load heavy components
const Chart = lazy(() => import('./components/Chart'));
const PDFViewer = lazy(() => import('./components/PDFViewer'));
const RichTextEditor = lazy(() => import('./components/RichTextEditor'));

function Dashboard() {
  const [showChart, setShowChart] = useState(false);

  return (
    <div>
      <button onClick={() => setShowChart(true)}>
        Show Analytics
      </button>

      {showChart && (
        <Suspense fallback={<ChartSkeleton />}>
          <Chart />
        </Suspense>
      )}
    </div>
  );
}

Named Exports with Lazy

// For named exports
const Modal = lazy(() =>
  import('./components').then(module => ({
    default: module.Modal
  }))
);

Preloading on Hover

function NavLink({ to, children }: Props) {
  const preloadComponent = () => {
    // Preload when user hovers
    import(`./pages/${to}`);
  };

  return (
    <Link
      to={to}
      onMouseEnter={preloadComponent}
      onFocus={preloadComponent}
    >
      {children}
    </Link>
  );
}

13. TypeScript Patterns

Performance-Friendly Types

// ✅ Use const assertions for literal types
const ACTIONS = {
  ADD: 'ADD',
  REMOVE: 'REMOVE',
  UPDATE: 'UPDATE',
} as const;

type Action = typeof ACTIONS[keyof typeof ACTIONS];

// ✅ Prefer interfaces for objects (faster compilation)
interface User {
  id: string;
  name: string;
  email: string;
}

// ✅ Use type for unions/intersections
type Status = 'idle' | 'loading' | 'success' | 'error';

// ✅ Generic constraints for better inference
function useQuery<T extends object>(
  key: string,
  fetcher: () => Promise<T>
): QueryResult<T> {
  // Implementation
}

Discriminated Unions for State

// ✅ Type-safe state handling
type AsyncState<T> =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: T }
  | { status: 'error'; error: Error };

function UserProfile() {
  const [state, setState] = useState<AsyncState<User>>({ status: 'idle' });

  // TypeScript narrows the type based on status
  if (state.status === 'loading') {
    return <Spinner />;
  }

  if (state.status === 'error') {
    return <Error error={state.error} />;  // error is available
  }

  if (state.status === 'success') {
    return <Profile user={state.data} />;  // data is available
  }

  return <button onClick={loadUser}>Load Profile</button>;
}

Strict Event Typing

// ✅ Type events precisely
function Form() {
  const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    const formData = new FormData(e.currentTarget);
    // Process form
  };

  const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
    const value = e.target.value;
    // Handle change
  };

  return (
    <form onSubmit={handleSubmit}>
      <input onChange={handleChange} />
    </form>
  );
}

Component Props Patterns

// ✅ Extend native element props
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
  variant?: 'primary' | 'secondary';
  loading?: boolean;
}

function Button({ variant = 'primary', loading, children, ...props }: ButtonProps) {
  return (
    <button {...props} disabled={loading || props.disabled}>
      {loading ? <Spinner /> : children}
    </button>
  );
}

// ✅ Polymorphic components
interface BoxProps<T extends ElementType> {
  as?: T;
  children: ReactNode;
}

type PolymorphicProps<T extends ElementType> = BoxProps<T> &
  Omit<ComponentPropsWithoutRef<T>, keyof BoxProps<T>>;

function Box<T extends ElementType = 'div'>({
  as,
  children,
  ...props
}: PolymorphicProps<T>) {
  const Component = as || 'div';
  return <Component {...props}>{children}</Component>;
}

// Usage
<Box as="section" className="container">Content</Box>
<Box as="article">Article content</Box>

14. Web Workers

Basic Worker Setup

// worker.ts
self.onmessage = (e: MessageEvent<{ data: number[] }>) => {
  const result = heavyComputation(e.data.data);
  self.postMessage(result);
};

function heavyComputation(data: number[]): number {
  return data.reduce((a, b) => a + b, 0);
}

// Component
function DataProcessor() {
  const [result, setResult] = useState<number | null>(null);
  const workerRef = useRef<Worker | null>(null);

  useEffect(() => {
    workerRef.current = new Worker(
      new URL('./worker.ts', import.meta.url),
      { type: 'module' }
    );

    workerRef.current.onmessage = (e: MessageEvent<number>) => {
      setResult(e.data);
    };

    return () => workerRef.current?.terminate();
  }, []);

  const processData = (data: number[]) => {
    workerRef.current?.postMessage({ data });
  };

  return (
    <button onClick={() => processData([1, 2, 3, 4, 5])}>
      Process
    </button>
  );
}
// worker.ts
import { expose } from 'comlink';

const api = {
  async processData(data: number[]): Promise<number> {
    // Heavy computation
    return data.reduce((a, b) => a + b, 0);
  },

  async sortItems(items: Item[]): Promise<Item[]> {
    return items.sort((a, b) => a.name.localeCompare(b.name));
  }
};

expose(api);

export type WorkerApi = typeof api;

// Component
import { wrap, type Remote } from 'comlink';
import type { WorkerApi } from './worker';

function useWorker() {
  const workerRef = useRef<Remote<WorkerApi>>();

  useEffect(() => {
    const worker = new Worker(
      new URL('./worker.ts', import.meta.url),
      { type: 'module' }
    );

    workerRef.current = wrap<WorkerApi>(worker);

    return () => worker.terminate();
  }, []);

  return workerRef.current;
}

function Component() {
  const worker = useWorker();
  const [result, setResult] = useState<number | null>(null);

  const handleClick = async () => {
    if (worker) {
      const sum = await worker.processData([1, 2, 3, 4, 5]);
      setResult(sum);
    }
  };

  return <button onClick={handleClick}>Process</button>;
}

Worker Pool

// workerPool.ts
import { wrap, type Remote } from 'comlink';
import type { WorkerApi } from './worker';

class WorkerPool {
  private workers: Remote<WorkerApi>[] = [];
  private index = 0;

  constructor(size = navigator.hardwareConcurrency || 4) {
    for (let i = 0; i < size; i++) {
      const worker = new Worker(
        new URL('./worker.ts', import.meta.url),
        { type: 'module' }
      );
      this.workers.push(wrap<WorkerApi>(worker));
    }
  }

  getWorker(): Remote<WorkerApi> {
    const worker = this.workers[this.index];
    this.index = (this.index + 1) % this.workers.length;
    return worker;
  }

  async processInParallel<T, R>(
    items: T[],
    fn: (worker: Remote<WorkerApi>, item: T) => Promise<R>
  ): Promise<R[]> {
    return Promise.all(
      items.map((item, i) => fn(this.workers[i % this.workers.length], item))
    );
  }

  terminate() {
    this.workers = [];
  }
}

export const workerPool = new WorkerPool();

15. Profiling & Debugging

React DevTools Profiler

// Enable Profiler in production (optional)
// Build with: REACT_APP_ENABLE_PROFILER=true

import { Profiler, ProfilerOnRenderCallback } from 'react';

const onRender: ProfilerOnRenderCallback = (
  id,
  phase,
  actualDuration,
  baseDuration,
  startTime,
  commitTime
) => {
  console.log({
    id,
    phase,
    actualDuration: `${actualDuration.toFixed(2)}ms`,
    baseDuration: `${baseDuration.toFixed(2)}ms`,
  });
};

function App() {
  return (
    <Profiler id="App" onRender={onRender}>
      <MainContent />
    </Profiler>
  );
}

why-did-you-render

// wdyr.ts - import at top of index.tsx
import React from 'react';

if (process.env.NODE_ENV === 'development') {
  const whyDidYouRender = require('@welldone-software/why-did-you-render');
  whyDidYouRender(React, {
    trackAllPureComponents: true,
    trackHooks: true,
    logOnDifferentValues: true,
  });
}

// Mark specific components
MyComponent.whyDidYouRender = true;

// Or in the component
function MyComponent(props: Props) {
  // Component code
}
MyComponent.whyDidYouRender = {
  logOnDifferentValues: true,
  customName: 'MyComponent'
};

Performance Marks

// Custom performance marks
function ExpensiveComponent() {
  useEffect(() => {
    performance.mark('expensive-start');

    return () => {
      performance.mark('expensive-end');
      performance.measure('expensive-render', 'expensive-start', 'expensive-end');

      const measure = performance.getEntriesByName('expensive-render')[0];
      console.log(`Render took: ${measure.duration.toFixed(2)}ms`);
    };
  }, []);

  return <div>...</div>;
}

Core Web Vitals

import { onLCP, onFID, onCLS, onINP, onTTFB } from 'web-vitals';

// Report to analytics
onLCP(console.log);  // Largest Contentful Paint
onFID(console.log);  // First Input Delay (deprecated)
onINP(console.log);  // Interaction to Next Paint (replacement)
onCLS(console.log);  // Cumulative Layout Shift
onTTFB(console.log); // Time to First Byte

// Send to analytics
function sendToAnalytics(metric: Metric) {
  const body = JSON.stringify({
    name: metric.name,
    value: metric.value,
    id: metric.id,
  });

  navigator.sendBeacon('/analytics', body);
}

onINP(sendToAnalytics);

Bundle Analysis

# Next.js
ANALYZE=true next build

# Vite
npx vite-bundle-visualizer

# webpack
npx webpack-bundle-analyzer stats.json

16. Quick Reference

Performance Checklist

  • State at lowest possible level
  • memo() on expensive pure components
  • Stable keys in lists (not index)
  • Context split by update frequency
  • Virtualization for long lists (> 100 items)
  • Code splitting for routes
  • Images optimized and lazy-loaded
  • Zero-runtime CSS (Tailwind/CSS Modules)
  • Heavy computation in Web Workers
  • Profiled with React DevTools

240fps Additional Checks

  • CSS containment (contain: layout paint) on isolated components
  • content-visibility: auto for long scrolling content
  • will-change managed properly (add before animation, remove after)
  • No forced synchronous layout (batch reads, then writes)
  • Passive event listeners for scroll/touch/wheel
  • OffscreenCanvas for heavy canvas operations
  • All DOM updates inside requestAnimationFrame

Cheat Sheet

// Stable callback
const handleClick = useCallback((id: string) => {
  doSomething(id);
}, []);

// Memoized expensive computation
const processed = useMemo(() => {
  return expensiveOperation(data);
}, [data]);

// Memoized component
const MemoizedChild = memo(Child);

// Non-blocking state update
const [isPending, startTransition] = useTransition();
startTransition(() => {
  setExpensiveState(newValue);
});

// Deferred value
const deferredQuery = useDeferredValue(query);

// Virtual list
const virtualizer = useVirtualizer({
  count: items.length,
  estimateSize: () => 50,
});

Common Pitfalls

PitfallImpactFix
State in parentCascade re-rendersMove state down
Object/array in propsChild always re-rendersuseMemo or lift out
Index as keyWrong state after reorderUse stable ID
Single large ContextAll consumers re-renderSplit contexts
Runtime CSS-in-JSStyle recalculationUse Tailwind/CSS Modules
No virtualizationDOM thrashingUse TanStack Virtual
Sync heavy workFrozen UIWeb Worker or useTransition
Full-size imagesSlow LCPUse next/image or srcSet

Frame Budget

60 FPS (16.67ms)              240 FPS (4.17ms)
├── JavaScript:  5-6ms         ├── JavaScript:  1-1.5ms
├── Style calc:  1-2ms         ├── Style calc:  0.5ms
├── Layout:      2-3ms         ├── Layout:      0.5-1ms
├── Paint:       2-3ms         ├── Paint:       0.5-1ms
└── Composite:   2-3ms         └── Composite:   0.5-1ms

Core Web Vitals Targets (2025)

MetricGoodNeeds WorkPoor
LCP< 2.5s2.5-4s> 4s
INP< 200ms200-500ms> 500ms
CLS< 0.10.1-0.25> 0.25