The Frontend Works. So Why Does It Still Feel Slow?
Everything looks fine in the code. The API is fast. There are no errors. But something still feels wrong.
Imagine you’re working on a trading dashboard.
Everything seems ready.
The charts are displaying correctly. Market prices are updating. The API responses are fast. Even the initial page load looks good.
You open the application and start testing.
You type a few letters into the instrument search field.
For a moment, nothing happens.
You keep typing.
The input suddenly catches up, and the results appear.
You try switching to another market.
Again, there’s a small delay.
Nothing is broken. There are no obvious errors.
But the application doesn’t feel right.
And that’s where an interesting frontend problem begins.
The application works. It just doesn’t feel fast.
The API Wasn’t the Problem
The first instinct might be to check the network requests.
Maybe the search endpoint is slow.
Maybe we’re fetching too much data.
Maybe something is wrong with the backend.
So we open Chrome DevTools and look at the Network tab.
The requests look fine.
Some responses arrive in around 100 milliseconds.
That’s not bad.
But if the backend is responding quickly, why does the interface still feel slow?
Because an API response is only one part of what happens between a user action and a visible update.
After receiving data, the browser might still need to process it, execute JavaScript, update components, calculate layout, and paint the result.
And all of that takes time.
A fast API doesn’t guarantee a fast user experience.
It’s a simple idea, but one that’s easy to forget when we’re focused on building features.
What Was Actually Happening?
Let’s return to our trading dashboard.
Imagine it has hundreds of instruments, live price updates, filtering, and pagination.
Every time a new price arrives, the application updates its state.
That’s expected.
But suppose those updates also cause a large part of the component tree to render again.
Now the browser is doing extra work continuously.
Meanwhile, the user is trying to type into the search field.
The input needs to respond to their keyboard events, but the main thread is busy processing other JavaScript work.
The result?
The search field feels delayed.
The important part is that none of the individual features is necessarily broken.
WebSocket updates work.
Search works.
Filtering works.
Pagination works.
But the way they interact creates a performance problem.
This is something I find particularly interesting about frontend engineering.
The biggest challenges often appear not when building individual features, but when those features start working together.
The Temptation to Optimize Everything
Once we notice unnecessary rendering, it’s tempting to start adding optimizations everywhere.
useMemo here.
useCallback there.
React.memo around every component.
Problem solved?
Not necessarily.
These tools can be useful, but they aren’t magic performance switches.
If a component receives constantly changing props, memoizing it might not help much.
If the application is processing a large dataset on every keystroke, wrapping a few functions in useCallback won't fix the expensive calculation.
And if we’re rendering hundreds of rows that the user can’t even see, we may need to rethink the rendering strategy itself.
I’ve learned to prefer a different question:
What exactly is making this interaction slow?
Not which optimization should we add.
Get Huseyn Ashurlu’s stories in your inbox
Join Medium for free to get updates from this writer.
Not which library should we install.
Just: where is the time going?
Finding the Real Bottleneck
For our example dashboard, I’d start with Chrome DevTools Performance and React DevTools Profiler.
I’d reproduce the delayed search interaction and inspect what happens.
Are there long JavaScript tasks blocking the main thread?
Are unrelated components rendering when the user types?
Is filtering the dataset expensive?
Are incoming market updates triggering more work than necessary?
The answers determine the solution.
If too many components subscribe to frequently changing data, we can make those subscriptions more targeted.
If filtering is expensive, we can avoid repeating unnecessary calculations or move sufficiently heavy work away from the main thread.
If the list contains hundreds of rendered elements, virtualization might help by rendering only what is needed near the visible area.
And if updates arrive faster than the screen needs to display them, we can consider controlling how often visual updates are applied, without losing important data.
Notice something?
We haven’t introduced a complicated architecture.
We’re simply trying to stop the application from doing work it doesn’t need to do.
Sometimes the best optimization is doing less.
What About Performance Metrics?
This is where metrics like Largest Contentful Paint (LCP) and Interaction to Next Paint (INP) become useful.
LCP helps us understand loading experience.
INP helps us understand how responsive a page is to interactions.
Both matter, but they answer different questions.
A page can have a good LCP while still having poor interaction responsiveness.
For example, a marketing page might load quickly, but clicking a navigation menu could feel delayed because the main thread is busy.
In my experience working on frontend performance, looking at these metrics together provides a much better picture than focusing only on loading speed.
But I wouldn’t rely on a single number.
I’d also use the application the way a real person would.
Type into the search field.
Open and close a modal.
Switch between tabs.
Scroll through a long list.
Because performance issues often become obvious when we stop looking at dashboards and start interacting with the actual product.
Back to Our Trading Dashboard
Remember the search field that felt slow?
Imagine we’ve profiled it and found that frequent price updates are causing unnecessary rendering across the dashboard.
We narrow the state subscriptions so components only receive the updates they need.
We make the instrument list more efficient.
We avoid recalculating filtered results when the relevant inputs haven’t changed.
Then we test again.
The user starts typing.
The input responds immediately.
Live prices continue updating.
Market switching feels smooth.
The application still does the same things it did before.
But now it feels different.
And that’s the part that matters.
We didn’t necessarily need faster servers or more powerful devices.
We needed a better understanding of what the frontend was doing.
The Lesson I Keep Coming Back To
As frontend engineers, it’s easy to focus on whether a feature works.
Does the API return the right data?
Does the button trigger the correct action?
Does the component render?
Those questions are important.
But there’s another question worth asking:
Does the experience feel right to the person using it?
Users don’t know how many React components we have.
They don’t care whether we used useMemo, virtualization, or some clever caching strategy.
They notice whether the interface responds when they need it.
And sometimes a tiny delay is enough to make an otherwise well-built application feel unreliable.
That’s why I believe good frontend engineering goes beyond writing working code.
It’s about understanding the entire experience, identifying what actually matters, and solving problems without adding unnecessary complexity.
Because the frontend isn’t truly fast just because our metrics look good. It needs to feel fast to the people using it.
Have you ever worked on an application where everything looked fast in DevTools, but the UI still felt slow? What turned out to be the problem?









