Engineering Note
Performance

How I Think About Frontend Performance

Frontend Performance as a Systems Problem

9 min read
IntermediatePerformance

Introduction

Frontend performance issues are rarely isolated to the frontend. They are often the result of how the entire system is designed, including API responses, data flow, caching, state management, and component structure.

In real-world applications, slow UI interactions are usually symptoms of deeper architectural problems rather than simple rendering inefficiencies. A page may feel slow because the frontend is doing too much work, but the cause can also be repeated network calls, oversized payloads, or poor data ownership.

This note focuses on practical engineering decisions behind frontend performance as a system problem, especially the patterns that reduce redundant work across APIs, state, rendering, and data flow.

The Problem

A common mistake is treating frontend performance as a purely visual or rendering issue. Developers often try to optimize components while ignoring how data is fetched, shaped, cached, and updated across the system.

Common Failures

  • Repeated API calls fetch the same data multiple times
  • Large payloads slow down parsing, rendering, and hydration
  • State updates trigger unnecessary re-renders across the UI
  • UI components become tightly coupled with business logic

User Impact

  • Pages feel slower even when the backend is working correctly
  • Interactions lag because too much state changes at once
  • Large lists, dashboards, and feeds become heavy to use
  • Users experience delays that are difficult to explain from the UI alone

These issues accumulate over time. A single extra request or one unnecessary render may not matter, but repeated across a large product, they create noticeable performance bottlenecks.

System Design / Approach

The correct approach is to treat performance as a system problem. Instead of only optimizing individual components, the goal is to reduce unnecessary work across the entire data flow.

1. Reduce Redundant Data Fetching

The frontend should avoid repeatedly requesting the same data when it can be cached, reused, prefetched, or shared safely between screens.

2. Shape APIs Around UI Needs

API responses should provide the data needed by the screen without forcing the frontend to receive, filter, or transform unnecessary payloads.

3. Control State Updates Carefully

State should be scoped to the smallest useful boundary so updates do not trigger unrelated parts of the interface to re-render.

Implementation

Step 1: Reduce API Overfetching

Avoid fetching unnecessary data. Keep API responses minimal and tailored to the screen that consumes them.

notes-api.ts
const data = await fetch("/api/notes?page=1&fields=id,title,excerpt");

Smaller payloads reduce network transfer, parsing cost, memory usage, and rendering work on the frontend.

Step 2: Cache Repeated Requests

Caching prevents duplicate API calls and reduces waiting time for data that does not need to be fetched from the server repeatedly.

cache.ts
const cached = cache.get(key);

if (cached) {
  return cached;
}

Caching reduces network overhead and makes repeated navigation, filters, and dashboard views feel more responsive.

Step 3: Control State Updates

Avoid unnecessary state updates that trigger re-renders. State should only change when the next value is meaningfully different from the previous value.

state-update.ts
setState((prev) => {
  if (prev.id === next.id && prev.status === next.status) {
    return prev;
  }

  return next;
});

Controlled state updates reduce unnecessary rendering and keep large interfaces responsive under frequent changes.

Step 4: Decouple UI from Heavy Logic

UI components should not own expensive business logic, data transformation, or repeated filtering work. Move heavy logic into services, selectors, API layers, or memoized helpers.

selector.ts
const visibleNotes = useMemo(() => {
  return filterNotes(notes, activeFilters);
}, [notes, activeFilters]);

Decoupling keeps components focused on rendering and makes performance problems easier to isolate.

Trade-offs

Approach Benefit Cost
Caching Faster repeated access and fewer duplicate network requests Stale data risk if invalidation is not handled carefully
Memoization Reduces repeated computation during rendering Adds memory usage and dependency management overhead
API Optimization Less data transfer and faster frontend rendering Adds backend design work and endpoint-specific planning

Real-World Impact

Faster Screens

Pages load and update faster because the frontend receives less unnecessary data and performs less repeated work.

Smoother Interactions

Inputs, filters, tables, and dashboards feel smoother because state updates and rendering are more controlled.

Cleaner Architecture

Performance improves alongside maintainability because UI, data, and business logic responsibilities are separated clearly.

Key Takeaways

Frontend performance is often a system-level issue, not just a UI problem

Unnecessary re-renders are usually caused by poor state and component structure

Optimizing data flow is more impactful than micro-optimizing components

Caching and memoization should be applied selectively, not everywhere

Backend latency and API design directly affect frontend performance

Future Improvements

Introduce request-level caching to reduce repeated API calls

Implement component-level code splitting for faster initial load

Use virtualization techniques for rendering large datasets

Adopt React Server Components to shift work to the server

Add performance monitoring using tools like Web Vitals or React Profiler