Our cosmic journey through React is nearing its end, but before we set off for further explorations, we need to make sure our spaceship is running at maximum capacity. In this module, we will focus on profiling the performance of React applications - that is, identifying and analyzing "bottlenecks" in our code.
Performance profiling is the process of identifying places in an application that consume the most resources (CPU time, memory) and may lead to slow user interface behavior. It is like diagnosing the engines of a spaceship to check which parts are not running efficiently and can be optimized.
In the context of React applications, profiling allows you to:
React DevTools is a browser extension that allows analyzing the performance of React applications. It includes a "Profiler" tab, which is the simplest and most powerful tool for profiling React applications.
Installing React DevTools
Starting profiling:
Analyzing results:
Chrome DevTools includes a Performance panel that allows for more general page performance profiling:
Starting profiling:
Analyze results:
This panel is particularly useful for identifying:
React provides a built-in API for profiling in development mode:
1// Import the Profiler component
2import { Profiler } from 'react';
3
4// Callback function called after rendering
5function onRenderCallback(
6 id, // identifier of the profiled component tree
7 phase, // "mount" (first render) or "update" (re-render)
8 actualDuration, // time spent rendering
9 baseDuration, // estimated render time of the entire subtree without optimizations
10 startTime, // when React started rendering
11 commitTime, // when React finished rendering
12 interactions // set of "interactions" tracked for this render
13) {
14 console.log({
15 id,
16 phase,
17 actualDuration,
18 baseDuration,
19 startTime,
20 commitTime
21 });
22}
23
24// Using the Profiler component in the application
25function App() {
26 return (
27 <Profiler id="MissionControl" onRender={onRenderCallback}>
28 <MissionControl />
29 </Profiler>
30 );
31}You can nest Profiler components to profile different parts of the application:
1function Dashboard() {
2 return (
3 <Profiler id="Dashboard" onRender={onRenderCallback}>
4 <h1>Mission Control Panel</h1>
5
6 <Profiler id="Navigation" onRender={onRenderCallback}>
7 <Navigation />
8 </Profiler>
9
10 <Profiler id="Stats" onRender={onRenderCallback}>
11 <MissionStats />
12 </Profiler>
13
14 <Profiler id="Crew" onRender={onRenderCallback}>
15 <CrewStatus />
16 </Profiler>
17 </Profiler>
18 );
19}This method of profiling is particularly useful when:
Problem: Components re-render even when their data hasn't changed.
How to profile:
Example of the problem:
1function ParentComponent() {
2 const [count, setCount] = useState(0);
3
4 // This function is created anew on every render
5 const handleClick = () => {
6 console.log('Button clicked');
7 };
8
9 return (
10 <div>
11 <button onClick={() => setCount(count + 1)}>
12 Increase counter: {count}
13 </button>
14
15 {/* ChildComponent will re-render on every count change,
16 even though handleClick has the same functionality */}
17 <ChildComponent onClick={handleClick} />
18 </div>
19 );
20}Solution:
1function ParentComponent() {
2 const [count, setCount] = useState(0);
3
4 // Function memoization prevents creating a new reference on every render
5 const handleClick = useCallback(() => {
6 console.log('Button clicked');
7 }, []);
8
9 return (
10 <div>
11 <button onClick={() => setCount(count + 1)}>
12 Increase counter: {count}
13 </button>
14
15 {/* ChildComponent wrapped in React.memo won't re-render
16 when its props don't change */}
17 <MemoizedChildComponent onClick={handleClick} />
18 </div>
19 );
20}
21
22const MemoizedChildComponent = React.memo(ChildComponent);Problem: Performing complex calculations during rendering causes UI delays.
How to profile:
Example of the problem:
1function DataAnalytics({ data }) {
2 // Heavy computations performed on every render
3 const processedData = processLargeDataset(data);
4 const statistics = calculateStatistics(processedData);
5
6 return (
7 <div>
8 <h2>Mission Data Analysis</h2>
9 <DataVisualisation stats={statistics} />
10 </div>
11 );
12}Solution:
1function DataAnalytics({ data }) {
2 // Memoizing heavy computations - they only run when data changes
3 const processedData = useMemo(() => {
4 return processLargeDataset(data);
5 }, [data]);
6
7 const statistics = useMemo(() => {
8 return calculateStatistics(processedData);
9 }, [processedData]);
10
11 return (
12 <div>
13 <h2>Mission Data Analysis</h2>
14 <DataVisualisation stats={statistics} />
15 </div>
16 );
17}Problem: Too frequent state updates cause cascading re-renders.
How to profile:
Example of the problem:
1function SpaceshipSensors() {
2 const [temperature, setTemperature] = useState(0);
3 const [pressure, setPressure] = useState(0);
4 const [oxygen, setOxygen] = useState(0);
5 const [radiation, setRadiation] = useState(0);
6
7 // Fetching sensor data every 100ms
8 useEffect(() => {
9 const interval = setInterval(() => {
10 // Each update causes a separate render
11 setTemperature(getTemperature());
12 setPressure(getPressure());
13 setOxygen(getOxygen());
14 setRadiation(getRadiation());
15 }, 100);
16
17 return () => clearInterval(interval);
18 }, []);
19
20 return (
21 <div>
22 <h2>Sensor Readings</h2>
23 <p>Temperature: {temperature}°C</p>
24 <p>Pressure: {pressure} hPa</p>
25 <p>Oxygen level: {oxygen}%</p>
26 <p>Radiation level: {radiation} μSv/h</p>
27 </div>
28 );
29}Solution:
1function SpaceshipSensors() {
2 // Storing related data together in a single state object
3 const [sensorData, setSensorData] = useState({
4 temperature: 0,
5 pressure: 0,
6 oxygen: 0,
7 radiation: 0
8 });
9
10 useEffect(() => {
11 const interval = setInterval(() => {
12 // One state update instead of four
13 setSensorData({
14 temperature: getTemperature(),
15 pressure: getPressure(),
16 oxygen: getOxygen(),
17 radiation: getRadiation()
18 });
19 }, 100);
20
21 return () => clearInterval(interval);
22 }, []);
23
24 return (
25 <div>
26 <h2>Sensor Readings</h2>
27 <p>Temperature: {sensorData.temperature}°C</p>
28 <p>Pressure: {sensorData.pressure} hPa</p>
29 <p>Oxygen level: {sensorData.oxygen}%</p>
30 <p>Radiation level: {sensorData.radiation} μSv/h</p>
31 </div>
32 );
33}Problem: Rendering long lists or complex visualizations can slow down the interface.
How to profile:
Example of using React DevTools to profile list performance:
Solution:
The Browser Performance API allows creating custom time markers:
1function ExpensiveComponent({ data }) {
2 useEffect(() => {
3 // Start measurement
4 performance.mark('expensive-calculation-start');
5
6 // Perform expensive calculations
7 const result = performExpensiveCalculation(data);
8
9 // End measurement
10 performance.mark('expensive-calculation-end');
11
12 // Create measurement between markers
13 performance.measure(
14 'expensive-calculation',
15 'expensive-calculation-start',
16 'expensive-calculation-end'
17 );
18
19 // Log results
20 console.log(performance.getEntriesByName('expensive-calculation'));
21 }, [data]);
22
23 //
24}These markers will be visible in the Performance panel in Chrome DevTools.
For monitoring performance in production, you can use:
Example of collecting data from React Profiler and sending it to a server:
1function onRenderCallback(id, phase, actualDuration, baseDuration, startTime, commitTime) {
2 // We only collect data from the update phase that takes longer than 10ms
3 if (phase === 'update' && actualDuration > 10) {
4 sendToAnalyticsServer({
5 componentId: id,
6 renderTime: actualDuration,
7 timestamp: Date.now()
8 });
9 }
10}
11
12function App() {
13 return (
14 <Profiler id="App" onRender={onRenderCallback}>
15 <AppContent />
16 </Profiler>
17 );
18}Let's analyze the process of profiling and optimizing the performance of a React application using the example of a space mission control panel:
Users report that the control panel is slow, especially when displaying telemetry data and the event list.
We use React DevTools Profiler to identify the source of the problem:
TelemetryChart component renders very slowly (>100ms)EventsList component re-renders every time new telemetry data arrivescalculateTrends function takes a lot of CPU timeBased on the profiling results, we introduce the following optimizations:
1// Before optimization
2function TelemetryChart({ data }) {
3 // Heavy data processing on every render
4 const chartData = processChartData(data);
5
6 return <LineChart data={chartData} />;
7}
8
9// After optimization
10const TelemetryChart = React.memo(function TelemetryChart({ data }) {
11 // Memoized data processing
12 const chartData = useMemo(() => processChartData(data), [data]);
13
14 return <LineChart data={chartData} />;
15});1// Before optimization
2function EventsList({ events }) {
3 return (
4 <div>
5 {events.map(event => (
6 <EventItem key={event.id} event={event} />
7 ))}
8 </div>
9 );
10}
11
12// After optimization
13const EventsList = React.memo(function EventsList({ events }) {
14 return (
15 <div>
16 {events.map(event => (
17 <MemoizedEventItem key={event.id} event={event} />
18 ))}
19 </div>
20 );
21});
22
23const MemoizedEventItem = React.memo(EventItem);1// Before optimization
2function MissionControlPanel({ telemetryData }) {
3 const trends = calculateTrends(telemetryData); // Blocks the main thread
4
5 return (
6 <div>
7 <TrendsDisplay trends={trends} />
8 {/* other components */}
9 </div>
10 );
11}
12
13// After optimization
14function MissionControlPanel({ telemetryData }) {
15 const [trends, setTrends] = useState(null);
16
17 useEffect(() => {
18 if (!telemetryData) return;
19
20 // Create a web worker
21 const worker = new Worker('./trendsWorker.js');
22
23 // Listen for response
24 worker.addEventListener('message', (event) => {
25 setTrends(event.data);
26 worker.terminate();
27 });
28
29 // Send data for processing
30 worker.postMessage(telemetryData);
31 }, [telemetryData]);
32
33 return (
34 <div>
35 {trends ? <TrendsDisplay trends={trends} /> : <LoadingSpinner />}
36 {/* other components */}
37 </div>
38 );
39}After introducing optimizations, we profile the application again:
TelemetryChart rendering time dropped from >100ms to ~15msEventsList component no longer renders on telemetry data updatescalculateTrendsThanks to these optimizations, the user interface runs smoothly, and the control panel responds immediately to user interactions.
Profile in a production-like environment
npm run build and serve the built files)Establish baselines
Profile regularly
Focus on user perception
Document profiling results
Performance profiling is an essential element of creating high-quality React applications. Just as space engineers monitor every aspect of a ship's operation, we should continuously analyze the performance of our applications to ensure users have a smooth and responsive experience.
Key points to remember:
With these profiling techniques and tools, you will be able to create performant React applications that provide an excellent experience for users, even in the most complex cosmic control panel interfaces.