Why Your App Feels Slow Even When Queries are Fast
Ever optimized a database query to milliseconds, yet users still complain the app feels slow? Let’s break down why that happens — and what you, as a developer, can do to fix it.
1. The Illusion of "Fast Queries"
You run a query in your database, and it comes back in 5ms. Fantastic, right?
But when you check the actual user experience, the request takes 2 seconds.
Where does the time go? Somewhere in between:
- Network latency
- Serialization and deserialization
- API response shaping
- Inefficient frontend rendering
- Chatty requests (too many round-trips)
Let’s dig into each with examples.
2. Network Latency
Imagine your backend is in Frankfurt, but your user is in Bangalore. The round-trip time (RTT) might be 200–300ms just because of distance.
If your frontend makes 10 sequential requests, that’s already 2–3 seconds gone — before rendering anything.
👉 Example:
// Bad: Sequential requests
async function loadUserDashboard(userId) {
const profile = await fetch(`/api/user/${userId}`);
const orders = await fetch(`/api/orders/${userId}`);
const notifications = await fetch(`/api/notifications/${userId}`);
return { profile, orders, notifications };
}
This looks innocent, but each request waits for the previous one.
✅ Fix: Make requests in parallel.
// Good: Parallel requests
async function loadUserDashboard(userId) {
const [profile, orders, notifications] = await Promise.all([
fetch(`/api/user/${userId}`),
fetch(`/api/orders/${userId}`),
fetch(`/api/notifications/${userId}`)
]);
return { profile, orders, notifications };
}
3. Serialization & Deserialization
You query data in PostgreSQL → serialize to JSON → send over HTTP → parse in frontend.
Each step adds overhead, especially if your payload is heavy.
👉 Example: Returning a giant user object when only two fields are needed.
{
"id": 123,
"name": "Tony",
"email": "[email protected]",
"created_at": "...",
"preferences": { "theme": "dark", "notifications": true },
"friends": [ ...2000 items... ],
"posts": [ ...3000 items... ]
}
The query was instant, but sending MBs of JSON takes time.
✅ Fix: Shape your response properly.
-- Instead of SELECT *
SELECT id, name, email FROM users WHERE id = 123;
And in the API:
// Only send what’s required for UI
return {
id: user.id,
name: user.name,
email: user.email
};
4. Inefficient Frontend Rendering
Even if the backend is blazing fast, the browser might choke.
Example: Rendering a table with 10,000 rows directly into the DOM.
// naive rendering
data.forEach(item => {
const row = document.createElement("tr");
row.innerHTML = `<td>${item.name}</td><td>${item.email}</td>`;
table.appendChild(row);
});
This works, but on large datasets, the UI freezes.
✅ Fix: Virtualize rendering (render only what’s visible).
Libraries like react-window
or vue-virtual-scroller
help here.
import { FixedSizeList as List } from 'react-window';
<List
height={600}
itemCount={data.length}
itemSize={35}
width={800}
>
{({ index, style }) => (
<div style={style}>{data[index].name}</div>
)}
</List>
5. Too Many Round-Trips (Chatty APIs)
One common anti-pattern is fetching related data piece by piece.
👉 Example:
const posts = await fetch(`/api/posts`);
for (const post of posts) {
post.comments = await fetch(`/api/posts/${post.id}/comments`);
}
If you have 50 posts, that’s 51 API calls!
✅ Fix: Use batch APIs or GraphQL.
query {
posts {
id
title
comments {
id
text
}
}
}
One round-trip, all the data you need.
6. Backend Bottlenecks Beyond Queries
Sometimes, queries are fast but your business logic layer slows things down.
- Heavy loops in Node.js that block the event loop
- Large object transformations in Python
- Inefficient ORM usage (N+1 queries sneak in easily)
👉 Example: ORM N+1 problem (simplified in pseudo-code):
// Loads 1 user, then loads orders in a loop
const user = await db.users.findById(1);
for (const orderId of user.orderIds) {
const order = await db.orders.findById(orderId); // N+1 queries!
}
✅ Fix: Use joins or eager loading.
// Fetch all orders in one go
const userWithOrders = await db.users.findById(1, { include: 'orders' });
7. Perceived Performance (What the User Sees)
Sometimes the actual work is fast, but you’re not showing progress.
- Blank screens while loading
- Spinners everywhere instead of skeleton screens
- No optimistic UI updates
👉 Example: Instead of making the user wait for confirmation, update the UI instantly and sync later.
// Optimistic update
function likePost(postId) {
setLiked(true); // Update UI immediately
fetch(`/api/posts/${postId}/like`, { method: 'POST' });
}
The app feels fast, even if the request takes 500ms.
8. Bringing It All Together
So why does your app feel slow even when queries are fast?
Because speed is holistic. It’s not just about database queries. It’s about:
- Reducing network round-trips
- Keeping payloads lean
- Rendering efficiently on frontend
- Avoiding chatty APIs
- Showing progress (optimistic UI, skeletons)
👉 As developers, we need to think end-to-end performance, not just database optimization.
Final Words
Next time someone says “the query is fast, but the app feels slow,” you know where to look.
- Check network latency
- Watch API payload sizes
- Profile frontend rendering
- Avoid chatty round-trips
- Improve perceived performance
Performance is not a single bottleneck — it’s a chain. And your app is only as fast as its slowest link.
🔥 Enjoyed this breakdown?
We share practical developer insights like this every week on my X account.
👉 Follow us on X for more content like this.
If you found this helpful, show some support by sharing it with your dev friends. Let’s make the web faster, one line of code at a time. 🚀
Member discussion