Performance is crucial for user experience. In this comprehensive guide, I'll share advanced techniques for optimizing React applications based on real-world projects.
React performance issues typically stem from:
const ExpensiveComponent = React.memo(({ data, onUpdate }) => { return ( <div> {data.map(item => ( <ComplexItem key={item.id} item={item} onUpdate={onUpdate} /> ))} </div> ) }, (prevProps, nextProps) => { // Custom comparison function return prevProps.data.length === nextProps.data.length && prevProps.data.every((item, index) => item.id === nextProps.data[index].id ) })
const DataVisualization = ({ rawData, filters }) => { const processedData = useMemo(() => { return rawData .filter(item => filters.includes(item.category)) .map(item => ({ ...item, computed: expensiveCalculation(item) })) }, [rawData, filters]) return <Chart data={processedData} /> }
const TodoList = ({ todos, onToggle, onDelete }) => { const handleToggle = useCallback((id) => { onToggle(id) }, [onToggle]) const handleDelete = useCallback((id) => { onDelete(id) }, [onDelete]) return ( <div> {todos.map(todo => ( <TodoItem key={todo.id} todo={todo} onToggle={handleToggle} onDelete={handleDelete} /> ))} </div> ) }
import { lazy, Suspense } from 'react' // In a Next.js App Router project, routing is handled by the file system. // This is a conceptual example for a client-side routing library. const Dashboard = lazy(() => import('./Dashboard')) const Profile = lazy(() => import('./Profile')) const Settings = lazy(() => import('./Settings')) function App() { return ( <Suspense fallback={<div>Loading...</div>}> {/* In Next.js, these would be pages like app/dashboard/page.tsx, app/profile/page.tsx, etc. */} {/* This section conceptually represents how different routes might load components dynamically. */} <div> {/* Example: <Dashboard /> or <Profile /> based on the current route */} </div> </Suspense> ) }
const HeavyModal = lazy(() => import('./HeavyModal')) const App = () => { const [showModal, setShowModal] = useState(false) return ( <div> <button onClick={() => setShowModal(true)}> Open Modal </button> {showModal && ( <Suspense fallback={<div>Loading modal...</div>}> <HeavyModal onClose={() => setShowModal(false)} /> </Suspense> )} </div> ) }
import { FixedSizeList as List } from 'react-window' const VirtualizedList = ({ items }) => { const Row = ({ index, style }) => ( <div style={style}> <ListItem item={items[index]} /> </div> ) return ( <List height={600} itemCount={items.length} itemSize={50} width="100%" > {Row} </List> ) }
Keep state as close to where it's used as possible:
// Instead of lifting state too high const App = () => { const [userPreferences, setUserPreferences] = useState({}) const [dashboardData, setDashboardData] = useState([]) return ( <div> <Header preferences={userPreferences} /> <Dashboard data={dashboardData} /> <Footer /> </div> ) } // Better: Keep state local const Dashboard = () => { const [data, setData] = useState([]) // Dashboard-specific logic } const Header = () => { const [preferences, setPreferences] = useState({}) // Header-specific logic }
Split contexts to prevent unnecessary re-renders:
// Separate contexts for different concerns const UserContext = createContext() const ThemeContext = createContext() const DataContext = createContext() // Use multiple providers const App = () => ( <UserProvider> <ThemeProvider> <DataProvider> <AppContent /> </DataProvider> </ThemeProvider> </UserProvider> )
Import only what you need:
// Instead of import * as _ from 'lodash' // Use import debounce from 'lodash/debounce' import throttle from 'lodash/throttle'
const loadChartLibrary = async () => { const { Chart } = await import('chart.js') return Chart } const ChartComponent = ({ data }) => { const [Chart, setChart] = useState(null) useEffect(() => { loadChartLibrary().then(setChart) }, []) if (!Chart) return <div>Loading chart...</div> return <Chart data={data} /> }
Use the Profiler to identify performance bottlenecks:
import { Profiler } from 'react' const onRenderCallback = (id, phase, actualDuration) => { console.log('Component:', id, 'Phase:', phase, 'Duration:', actualDuration) } const App = () => ( <Profiler id="App" onRender={onRenderCallback}> <MainContent /> </Profiler> )
import { getCLS, getFID, getFCP, getLCP, getTTFB } from 'web-vitals' getCLS(console.log) getFID(console.log) getFCP(console.log) getLCP(console.log) getTTFB(console.log)
const DataFetcher = ({ children, url }) => { const [data, setData] = useState(null) const [loading, setLoading] = useState(true) useEffect(() => { fetchData(url).then(data => { setData(data) setLoading(false) }) }, [url]) return children({ data, loading }) } // Usage <DataFetcher url="/api/users"> {({ data, loading }) => loading ? <Spinner /> : <UserList users={data} /> } </DataFetcher>
const Accordion = ({ children }) => { const [openIndex, setOpenIndex] = useState(null) return ( <div className="accordion"> {React.Children.map(children, (child, index) => React.cloneElement(child, { isOpen: index === openIndex, onToggle: () => setOpenIndex(index === openIndex ? null : index) }) )} </div> ) } Accordion.Item = ({ title, children, isOpen, onToggle }) => ( <div className="accordion-item"> <button onClick={onToggle}>{title}</button> {isOpen && <div>{children}</div>} </div> )
type Result<T, E = Error> = | { success: true; data: T } | { success: false; error: E } async function fetchUser(id: string): Promise<Result<User, string>> { try { const user = await api.get(`/users/${id}`) return { success: true, data: user } } catch (error) { return { success: false, error: 'Failed to fetch user' } } } // Usage const result = await fetchUser('123') if (result.success) { console.log(result.data.name) // Type-safe access } else { console.error(result.error) }
abstract class Option<T> { abstract isSome(): this is Some<T> abstract isNone(): this is None<T> abstract map<U>(fn: (value: T) => U): Option<U> abstract flatMap<U>(fn: (value: T) => Option<U>): Option<U> abstract getOrElse(defaultValue: T): T } class Some<T> extends Option<T> { constructor(private value: T) { super() } isSome(): this is Some<T> { return true } isNone(): this is None<T> { return false } map<U>(fn: (value: T) => U): Option<U> { return new Some(fn(this.value)) } flatMap<U>(fn: (value: T) => Option<U>): Option<U> { return fn(this.value) } getOrElse(_defaultValue: T): T { return this.value } } class None<T> extends Option<T> { isSome(): this is Some<T> { return false } isNone(): this is None<T> { return true } map<U>(_fn: (value: T) => U): Option<U> { return new None<U>() } flatMap<U>(_fn: (value: T) => Option<U>): Option<U> { return new None<U>() } getOrElse(defaultValue: T): T { return defaultValue } } // Usage function findUser(id: string): Option<User> { const user = database.find(id) return user ? new Some(user) : new None() } const userOption = findUser('123') const userName = userOption .map(user => user.name) .map(name => name.toUpperCase()) .getOrElse('Unknown User')
These advanced TypeScript patterns can significantly improve your code quality by:
The key is to use these patterns judiciously—not every situation requires advanced types. Start with simpler patterns and gradually incorporate more complex ones as your needs grow.
Remember, the goal is to make your code more robust and maintainable, not to show off TypeScript's complexity.
Explore how to seamlessly integrate AI capabilities into your web applications using modern frameworks and APIs. Learn about best practices, common pitfalls, and real-world implementation strategies.
Read More