Skip to content

⚡ Performance Optimization: Client-Side Responsiveness via Selective Rendering #570

Description

@arii
Category Value
Labels performance, frontend, react, memoization
Project HRM Core Client
Assignee @frontend-lead

Description

Our application receives high-frequency updates from the server via WebSockets (UnifiedStateMessage). When the root state object changes in a parent component (e.g., the useWebSocket hook updates the main state context), all consumer components re-render, even if the specific data they display has not changed. This "ripple effect" leads to excessive CPU cycles, slow frame rates, and a "janky" user experience, which is unacceptable for a real-time monitoring dashboard.

The objective is to strictly enforce selective rendering using React's optimization tools.

Goal: Decouple Component Re-renders from Global State Updates

We need to ensure that components only re-render when the exact data they consume changes. This requires refactoring both how the state is consumed (Custom Hooks) and how components are defined (React.memo).

I. Component-Level Optimization (React.memo)

We must apply memoization to all presentational components that consume high-frequency, dynamic state. These components currently re-render unnecessarily because their parent component re-renders.

Tasks:

  • A. Memoize High-Frequency Components: Wrap the following components (and their children) in React.memo to prevent re-rendering when their props are shallowly equal:

    • components/HrTile.tsx: This receives the heart rate, which is updated frequently.
    • components/TimerDisplay.tsx: This receives timer/Tabata state, which updates every second.
    • components/SpotifyDisplay.tsx: This updates with song progress, which is frequent.
    • Example Implementation:
      // File: components/HrTile.tsx
      import React, { memo } from 'react';
      
      const HrTile: React.FC<HrTileProps> = ({ heartRate, zone }) => {
        // ... rendering logic
      };
      
      export default memo(HrTile); // <-- The Fix
  • B. Profile and Verify: Use the React DevTools Profiler to ensure these components only re-render when the props passed to them change, confirming that memoization is effective.

II. Custom Hook Optimization (Stable Data Extraction)

Memoization on components only works if the props passed to them are stable. If the state hook returns a new object reference every time (even if the internal values are the same), memoization fails. We must optimize the custom data hooks.

Tasks:

  • A. Refactor State Consumption with useMemo: In any custom hook that consumes the main WebSocket state (e.g., in an abstraction over useWebSocket.ts) and extracts specific values, use React.useMemo to return a stable object/value reference.

  • B. Create Fine-Grained Hooks: Abstract the state access into highly specific hooks. Instead of a component calling const { heartRate, tabataState, spotify } = useHrmState();, encourage the use of dedicated, small hooks.

    // Example Refactoring:
    // New hook to extract only HR data, memoized by value (not object reference)
    export const useHeartRateData = () => {
      const { heartRate, zone } = useHrmState(); 
      // Returns a stable object or primitive if the value hasn't changed.
      return useMemo(() => ({ heartRate, zone }), [heartRate, zone]);
    }
    
    // Component now uses:
    // const { heartRate, zone } = useHeartRateData(); 
    // This component will only re-render if heartRate OR zone value changes.
  • C. Stabilize Callbacks: Use React.useCallback for any event handlers or functions (like the broadcastState function equivalents on the client for command sending) that are passed down to memoized children. Passing a new function reference every time bypasses React.memo's shallow comparison.

III. Data Structure Review

  • A. Prop Atomicity: Review the props passed to high-frequency components. Ensure that props are as atomic as possible. Instead of passing the entire tabataState object to <TimerDisplay />, consider passing only the necessary primitives like phase and remainingTime. This makes the memoized comparison faster and more reliable.

Next Step: I recommend starting by implementing Task II.B by creating the initial fine-grained data-extraction hooks (e.g., useHeartRateData(), useTabataTimer()) to abstract state from the main component hierarchy. In which existing client-side directory (e.g., hooks/, utils/) should these new selective-rendering hooks reside?

Metadata

Metadata

Assignees

No one assigned

    Projects

    Status
    Done

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions