Skip to content

React — Performance & Large Datasets

This document contains the complete conversation from this topic, formatted for use in an Astro project. Mermaid diagrams are included where they improve architectural or process understanding.


1. How do you make parallel API calls in React?

Section titled “1. How do you make parallel API calls in React?”
  • Promise.all
  • Promise.allSettled
  • Concurrent requests
  • Request batching

I use Promise.all() when multiple APIs are independent and I need all responses:

const [users, orders, products] = await Promise.all([
fetch("/api/users"),
fetch("/api/orders"),
fetch("/api/products")
]);
  • Promise.all → concurrent requests; fails fast if any request rejects.
  • Promise.allSettled → waits for all requests and gives success/failure for each.
  • Concurrent requests → useful when APIs are independent; avoid unnecessary sequential calls.
  • Request batching → combine multiple requests into one API call when the backend supports it, reducing network overhead.
flowchart LR
    R["React Component"] --> A["API 1"]
    R --> B["API 2"]
    R --> C["API 3"]

    A --> P["Promise.all"]
    B --> P
    C --> P

    P --> UI["Update UI"]

Follow-up: What happens if one API fails inside Promise.all?

Section titled “Follow-up: What happens if one API fails inside Promise.all?”

Promise.all() rejects immediately when any promise fails, so you don’t get the successful results through the returned promise.

try {
const results = await Promise.all([
api1(),
api2(),
api3()
]);
} catch (error) {
// One failure causes Promise.all to reject
}

If I want partial results, I use Promise.allSettled():

const results = await Promise.allSettled([
api1(),
api2(),
api3()
]);

This lets me process successful APIs while handling failed ones independently.

flowchart TD
    A["Multiple API Requests"] --> B{"Which behavior is needed?"}

    B -->|"All must succeed"| C["Promise.all"]
    B -->|"Partial success is acceptable"| D["Promise.allSettled"]

    C --> E["Reject on first rejection"]
    D --> F["Wait for every promise"]
    F --> G["Handle success and failure individually"]

2. How would you improve page load performance?

Section titled “2. How would you improve page load performance?”
  • Lazy Loading
  • Code Splitting
  • Tree Shaking
  • CDN
  • Compression
  • Browser Caching

I would optimize it at multiple levels:

  • Lazy Loading – Load components, images, and routes only when needed.
  • Code Splitting – Split large JavaScript bundles using dynamic imports so the initial bundle is smaller.
  • Tree Shaking – Remove unused JavaScript/CSS code during the build.
  • CDN – Serve static assets from edge locations closer to users.
  • Compression – Enable Brotli/Gzip for JS, CSS, HTML, and API responses.
  • Browser Caching – Cache static assets using proper Cache-Control headers and hashed filenames.
const Dashboard = lazy(() => import("./Dashboard"));
flowchart LR
    A["User"] --> B["Browser"]
    B --> C["CDN"]
    C --> D["Compressed Assets"]
    D --> E["Cached Assets"]

    B --> F["Lazy Loaded Components"]
    F --> G["Code-Split Bundles"]

    G --> H["Smaller Initial Load"]
    H --> I["Faster Page Load"]

“I would first identify the bottleneck using Lighthouse and Chrome DevTools. Then I would reduce the initial JavaScript using code splitting and lazy loading, remove unused code through tree shaking, optimize static assets through a CDN and compression, and configure browser caching. I would monitor metrics such as LCP, FCP, and INP to validate the improvements.”


I would use caching at multiple levels:

  • Browser Cache – Cache static JS, CSS, and images using Cache-Control and hashed filenames.
  • HTTP/API Cache – Cache GET API responses using HTTP cache headers like ETag and Cache-Control.
  • In-memory Cache – Keep frequently used data in React state, Context, or libraries like TanStack Query.
  • LocalStorage/SessionStorage – Store small, non-sensitive data that should survive page refreshes.
  • Service Worker / PWA Cache – Cache assets and selected API responses for offline or faster loading.
  • CDN Cache – Cache static assets and sometimes API responses closer to users.
flowchart TD
    U["User"] --> B["Browser"]

    B --> BC["Browser Cache"]
    B --> SW["Service Worker Cache"]
    B --> API["API Request"]

    API --> HTTP["HTTP/API Cache"]
    HTTP --> CDN["CDN Cache"]
    CDN --> SERVER["Backend"]

    B --> MEM["In-Memory / Query Cache"]

Static assets can use long-lived caching when filenames contain content hashes:

Cache-Control: public, max-age=31536000, immutable

API responses may use shorter caching periods depending on how frequently the data changes.

“I choose the caching strategy based on data freshness and sensitivity. Static assets can have long TTLs, while frequently changing API data should have a short TTL or use stale-while-revalidate. For server state, a library such as TanStack Query can simplify cache management.”


4. How would you reduce bundle size for a React application?

Section titled “4. How would you reduce bundle size for a React application?”

I would focus on:

  • Code Splitting – Split bundles using React.lazy() and dynamic import().
  • Tree Shaking – Remove unused code by using ES modules and production builds.
  • Lazy Loading – Load heavy components, routes, and libraries only when required.
  • Dependency Optimization – Remove unnecessary packages and replace large libraries with lighter alternatives.
  • Bundle Analysis – Use tools such as webpack-bundle-analyzer or Vite’s bundle analysis to identify large dependencies.
  • Asset Optimization – Compress images and use WebP/AVIF; optimize fonts.
  • Minification & Compression – Minify JS/CSS and enable Brotli/Gzip.
  • Avoid duplicate dependencies – Check for multiple versions of the same library.
flowchart TD
    A["React Application"] --> B["Analyze Bundle"]

    B --> C["Large Dependencies"]
    B --> D["Unused Code"]
    B --> E["Large Assets"]
    B --> F["Duplicate Dependencies"]

    C --> G["Replace / Remove"]
    D --> H["Tree Shaking"]
    E --> I["Optimize Images / Fonts"]
    F --> J["Deduplicate Dependencies"]

    G --> K["Smaller Bundle"]
    H --> K
    I --> K
    J --> K

    K --> L["Faster Initial Load"]

“First, I analyze the bundle to identify the largest dependencies. Then I apply code splitting and lazy loading, remove unused dependencies through tree shaking, optimize assets, and enable compression. I also monitor bundle size in CI/CD so it doesn’t regress.”


5. How would you handle large datasets (100k+ records) in the UI?

Section titled “5. How would you handle large datasets (100k+ records) in the UI?”
  • Pagination
  • Virtualization
  • Infinite Scrolling

I wouldn’t load all 100k records into the browser. I’d handle it at both backend and frontend levels:

  • Pagination – Fetch only a limited number of records per request, e.g. 50–100.
  • Virtualization – For large lists already loaded in memory, render only visible rows using libraries such as react-window.
  • Infinite Scrolling – Fetch the next batch as the user scrolls, useful for feeds/search results.
  • Server-side filtering/sorting – Push expensive operations to the backend/database.
  • Debouncing – Debounce search/filter requests to avoid excessive API calls.

Pagination fetches a limited number of records per request.

For example:

Page 1 → 50 records
Page 2 → 50 records
Page 3 → 50 records
...
const fetchUsers = async (page) => {
const response = await fetch(
`/api/users?page=${page}&size=50`
);
return response.json();
};
  • Low network usage
  • Lower browser memory consumption
  • Simple implementation
  • Good for tables and admin screens
  • Easy to support page navigation
  • Data tables
  • Search results
  • Admin dashboards
  • Reports

Virtualization renders only the rows currently visible in the viewport instead of creating DOM nodes for every record.

For example, even if thousands of records are loaded, the browser may render only the visible 20–50 rows.

flowchart TD
    A["Large Dataset"] --> B["Virtualized List"]

    B --> C["Visible Rows"]
    B --> D["Rows Outside Viewport"]

    C --> E["Rendered in DOM"]
    D --> F["Not Rendered"]
100,000 records
|
v
Virtualized List
|
+----> Visible rows → Render
|
+----> Off-screen rows → Do not render

Libraries such as react-window can be used:

import { FixedSizeList } from "react-window";
<FixedSizeList
height={600}
itemCount={users.length}
itemSize={50}
width="100%"
>
{Row}
</FixedSizeList>
  • Dramatically reduces DOM nodes
  • Improves scrolling performance
  • Reduces rendering cost
  • Useful for large lists and grids
  • Large tables
  • Logs
  • File lists
  • Search results
  • Chat/message lists

Infinite scrolling loads the next batch when the user approaches the bottom of the list.

sequenceDiagram
    participant U as User
    participant R as React
    participant A as API
    participant D as Database

    U->>R: Scrolls near bottom
    R->>A: Request next batch
    A->>D: Fetch records
    D-->>A: Return records
    A-->>R: Return next batch
    R-->>U: Append records
const loadMore = async () => {
const nextPage = page + 1;
const response = await fetch(
`/api/users?page=${nextPage}&size=50`
);
const data = await response.json();
setUsers((current) => [...current, ...data]);
setPage(nextPage);
};

In production, an IntersectionObserver is often preferable to manually listening for scroll events.

  • Good user experience
  • No explicit page navigation
  • Loads data progressively
  • Suitable for feed-like experiences
  • Social feeds
  • Activity feeds
  • Product feeds
  • Search results
  • Content streams

For very large datasets, filtering and sorting should generally happen on the server.

Instead of:

100,000 records
Browser
Filter / Sort

Prefer:

User filter
React
Backend API
Database
Small result set
React
flowchart LR
    U["User"] --> R["React"]
    R --> API["Backend API"]
    API --> DB["Database"]

    DB --> API
    API --> R
GET /api/users?search=John&page=0&size=50&sort=name

This avoids transferring unnecessary records to the browser.


When users type into a search field, avoid making an API request for every keystroke.

Without debouncing:

J → API call
Jo → API call
Joh → API call
John → API call

With debouncing:

John
Wait 300–500ms
API call

This reduces unnecessary network traffic and backend load.


5.6 Pagination vs Virtualization vs Infinite Scrolling

Section titled “5.6 Pagination vs Virtualization vs Infinite Scrolling”
Strategy Network Efficiency DOM Performance UX Best Use Case
Pagination Excellent Excellent Good Tables / Admin
Virtualization Depends on data loading Excellent Excellent Large lists
Infinite Scrolling Good Good Excellent Feeds
Server-side Filtering Excellent Excellent Excellent Search / Reports

These techniques can also be combined.

For example:

Server-side pagination
+
Virtualized table
+
Server-side filtering/sorting

This is a strong approach for enterprise applications.


Section titled “5.7 Recommended Approach for 100k+ Records”
flowchart TD
    A["100k+ Records"] --> B{"What type of UI?"}

    B -->|"Table / Report"| C["Server-side Pagination"]
    B -->|"Large List"| D["Pagination + Virtualization"]
    B -->|"Feed / Stream"| E["Infinite Scrolling"]

    C --> F["Server-side Filtering & Sorting"]
    D --> F
    E --> F

    F --> G["Debounced Search"]
    G --> H["Small API Responses"]
    H --> I["Fast React UI"]

For a typical enterprise React application, I would prefer:

  • Server-side pagination
  • Server-side filtering
  • Server-side sorting
  • Virtualization for large rendered lists
  • Infinite scrolling only when the UX requires it
  • Debounced search
  • Cursor-based pagination where appropriate

A concise senior-level answer for this entire performance topic:

“For frontend performance, I first measure the actual bottleneck using Lighthouse and Chrome DevTools. For page load, I use code splitting, lazy loading, tree shaking, compression, CDN delivery, and browser caching. For API calls, I run independent requests concurrently with Promise.all and use Promise.allSettled when partial failures are acceptable. For large datasets, I avoid loading everything into the browser and use server-side pagination, filtering, and sorting. I combine pagination with virtualization for large tables or lists and use infinite scrolling for feed-based experiences. I also debounce search requests and monitor performance metrics and bundle size continuously.”


mindmap
  root((React Performance))
    API Calls
      Promise.all
      Promise.allSettled
      Concurrent Requests
      Request Batching
    Page Load
      Lazy Loading
      Code Splitting
      Tree Shaking
      CDN
      Compression
      Browser Cache
    Frontend Caching
      HTTP Cache
      In-Memory Cache
      LocalStorage
      Service Worker
      CDN
    Bundle Size
      Analyze Bundle
      Remove Dependencies
      Tree Shaking
      Dynamic Imports
      Asset Optimization
    Large Datasets
      Pagination
      Virtualization
      Infinite Scrolling
      Server-side Filtering
      Server-side Sorting
      Debouncing
Problem Preferred Approach
Independent API calls Promise.all
Partial API failures acceptable Promise.allSettled
Slow initial page Lazy loading + code splitting
Unused JS Tree shaking
Static asset delivery CDN + browser caching
Large JS/CSS/assets Compression + optimization
Repeated API data HTTP/query caching
100k+ table records Server-side pagination
Large rendered list Virtualization
Feed-like UI Infinite scrolling
Expensive search Debounce + server-side filtering
Large dataset sorting Server-side sorting

“My overall approach is to minimize what the browser downloads, minimize what it renders, and avoid unnecessary network calls. I use concurrent API calls where appropriate, cache reusable data, split and lazy-load bundles, and for large datasets I keep the heavy work on the server while using pagination or virtualization on the client.”