Why Your React App Feels Slow (Even When Lighthouse Says It’s Fast)
A high Lighthouse score does not always mean users experience a fast product. Learn the frontend performance problems that quietly hurt real users slow interactions, oversized JavaScript, rendering waterfalls, and layout shifts and how to fix them in a React application.
Amit Srivastava
Published on July 27, 2026
Why Your React App Feels Slow (Even When Lighthouse Says It's Fast)
Speed isn't about how fast a page loads. It's whether a user can tap, type, scroll, and finish a task without waiting or wondering if the app even registered their action.
You shipped a React app, ran Lighthouse, got a score in the 90s. Users still complain: the dashboard takes too long to respond, a button looked like it did nothing, the page jumps around while they're trying to click, it feels slow on mobile.
The gap is that Lighthouse measures a controlled page load. Real users go through a whole journey: opening the page on a mid-range phone, waiting for JS to become interactive, filtering a table, opening a modal, switching tabs, submitting a form. A lab score doesn't capture any of that.
This post covers the usual reasons React apps feel slow in practice, and what actually fixes them.
Nobody cares about First Contentful Paint or bundle size directly. What they're really asking is: can I do what I came here to do, right now? That comes down to whether content shows up quickly, whether they can interact with it immediately, whether the interface responds instantly to their action, and whether the layout stays put while they use it.
A page can paint fast and still lock up on click. It can have skeleton loaders and still shift once real data arrives. It can ship a small initial bundle and still download a ton of code on the next navigation. Performance isn't one number — it's the sum of every wait a user notices.
React apps ship a lot of JS because they're interactive, and every interactive component adds more for the browser to parse, compile, and run. A dashboard page like this looks harmless:
import { DataTable } from "@/components/data-table";import { RevenueChart } from "@/components/revenue-chart";import { ExportButton } from "@/components/export-button";import { DateRangePicker } from "@/components/date-range-picker";export default function DashboardPage() { return ( <> <DateRangePicker /> <RevenueChart />
But the table probably pulls in a sorting library, the chart a visualization package, the export button spreadsheet-generation code, the date picker a full calendar library. If all of that ships up front, the browser has a lot of work to do before it can respond to anything.
Don't lazy-load everything — that just trades one problem for a pile of delayed requests. Ask whether the user needs it immediately, whether it's above the fold, whether the library is unusually heavy, and whether it can wait until a click. Below-the-fold charts, PDF exporters, rich text editors, advanced filter panels — good candidates.
A page can render visually while the main thread is still busy. The user sees "Save Changes," clicks it, and nothing happens for 400ms. That's a trust problem more than a performance one.
Interaction to Next Paint (INP) is the metric that catches this — how quickly the page visibly responds after an interaction. It usually breaks on expensive synchronous work:
function handleSearch(query: string) { const filteredUsers = users .filter((user) => user.name.toLowerCase().includes(query.toLowerCase())) .sort((a, b) => a.name.localeCompare(b.name)); setResults(filteredUsers);}
Fine for a small list. With thousands of records and a heavy table render, every keystroke can lag.
The input update is urgent and should happen right away. Filtering and re-rendering the list can run at lower priority. That one distinction goes a long way toward making an interface feel responsive.
Rerenders themselves aren't the bug — React is built to rerender on state changes. The problem is when one small state update drags a large chunk of the page along with it.
Take a pricing page with a search field, filters, a product table, and a heavy analytics chart. If the search text lives at the page level, every keystroke can rerender all of it, chart included.
This usually does more than sprinkling useMemo and useCallback everywhere. Reach for memoization after you've measured an expensive rerender, not as a default habit.
Tapping a button just as an image or banner loads in and shoves it away — that's Cumulative Layout Shift, and it's one of the fastest ways to make an interface feel unreliable. Usual culprits: images without explicit dimensions, ads or banners inserted after load, web fonts changing text size once they load, data-driven sections popping in without reserved space, skeletons that don't match the shape of the real content.
A waterfall happens when one request can't start until another finishes: load the page, fetch the current user, fetch their org, fetch the org's projects, fetch the selected project's analytics. Each step waits on the last one, and a handful of fast requests turns into a slow page.
In Next.js, server components help a lot here — fetch data close to the route, stream what's ready, and keep client-side JS focused on interaction rather than initial load. The biggest wins are often architectural: cut the wait instead of dressing it up.
Lighthouse is useful but partial. Test what users actually do: sign in on a slow mobile connection, search a large dataset, open a modal with a form, switch dashboard tabs, submit a checkout form, navigate from a list into a detail page. Use browser performance tools for long tasks, the React DevTools Profiler for expensive updates, and real user monitoring for actual Core Web Vitals.
A decent rule of thumb: if a performance problem can't be felt, it's probably not the first thing to fix. If users can feel it, it matters, even when the benchmark looks fine.
Before calling a page fast, check whether critical content is visible quickly on a real mobile device, whether every click, tap, and keystroke gets quick feedback, whether heavy features (charts, editors, exporters, rarely-used panels) load only when needed, whether large tables and lists are virtualized, whether the layout stays stable as content, images, and fonts load in, whether independent requests are parallelized, and whether you've profiled real user journeys rather than just the initial load.
Most of this comes down to removing friction from the moments that matter: a response when the user clicks, a scroll that keeps up, a page that feels stable instead of assembling itself in front of them. That's not a final optimization pass — it's part of the product.