©Code byMilanParmar

Why is my Lovable app so slow, and how do I fix it?

How to tell front-end slowness from database slowness, the seven causes I find most in generated code, the SQL fix for each, a symptom-to-cause table, what the Supabase free tier changes, and what fast enough looks like.

14 min read

A Lovable app is slow once real users arrive because the generated code was tested on ten rows. The usual causes are one query per row, select * with no limit, no index on the columns it filters by, and access policies that run a function on every row. Each is a one-line fix once you know which one you have.

The app felt fine in the preview, then slowed down as the tables grew. A list that took a moment with twenty rows takes seconds with five thousand. Founders tell me it got slow "for no reason", but the reason is always there in the network tab or the database. This guide is for the founder of a Lovable app on Supabase, or Lovable Cloud, and for the developer helping them. It goes from how to tell where the time goes, to the seven causes I find most, with the SQL or code fix for each. If the app doesn't slow down but stops after sign-in, that's a different problem: a Lovable app that hangs after login.

Is it the front end or the database? How to tell

Don't guess. Open the slow screen with Chrome's developer tools on the Network tab, reload, and type supabase.co into the filter. Three things tell you where the time goes:

  1. How many requests.A screen that shows a list and needs one or two requests is fine. A screen that fires fifty, each a few milliseconds apart, has an N+1 problem, whatever each one's timing says.
  2. How long each takes.Click a request and open the Timing tab. "Waiting for server response" is the database and the network. If one request waits two seconds, the query is slow. If every request waits 150 ms and there are forty of them, the query is fine and the code asks too often.
  3. How big the response is. The Size column. A list screen that downloads 4 MB of JSON is fetching the whole table and trimming it in the browser.

If the requests are few, quick, and small, and the screen is still slow, the time is in the browser: rendering, images, or the JavaScript bundle. Jump to the sections on filtering in the browser and on images.

The Supabase Query Performance report

For the database side, the Supabase dashboard has a Query Performance report under Reports. It lists queries by time consumed, with how often each ran, and Supabase's docs say that selecting a query and opening its indexes tab shows index suggestions from the index_advisor extension. The data comes from pg_stat_statements, which Supabase's docs describe as tracking planning and execution statistics of every SQL statement run on the database. You can query it yourself in the SQL editor:

SQL: the ten queries that cost the most time in total
select
  calls,
  round(mean_exec_time::numeric, 1) as avg_ms,
  round(total_exec_time::numeric) as total_ms,
  left(query, 120) as query
from extensions.pg_stat_statements
order by total_exec_time desc
limit 10;

Sort by total time, not average. A 4 ms query called 30,000 times a day costs more than a 2 second report run twice. Both columns are worth reading: the slow one is a missing index or a policy problem, the frequent one is an N+1 or a fetch on every keystroke.

If the app is on Lovable Cloud

You may not have the Supabase dashboard. Lovable's docs say you can ask it to find slow queries: it reads the query statistics, ranks queries by total execution time with call counts, and can apply a fix such as adding an index. Under More, then Database, then Manage, the settings show CPU, memory, and disk pressure for the last 24 hours. A new or recently restarted database may have no statistics yet, so use the app for a day first.

Lovable app slow: symptom, likely cause, and fix

Match what you saw in the network tab or the report to a row. The sections after this one explain each fix.

Symptom, likely cause, and the fix in a Lovable app on Supabase
What you seeLikely causeFix
Dozens of small requests per screenN+1: a query inside a loop or a mapOne query with the related rows embedded
One request waits seconds; response is hugeselect("*") with no limit, or the 1,000 row cap hiding dataName the columns, add range(), paginate
One request waits seconds; response is smallSequential scan: no index on the filtered column or foreign keycreate index, confirm with explain analyze
Every query on one table is slow, even by IDAn RLS policy that calls a function per row or joins another tableWrap the call in (select …), index the policy column, add to authenticated
Requests are quick; typing or scrolling stuttersFiltering or sorting a large array in the browser, re-rendersFilter in the query, memoize, virtualize long lists
A request on every keystroke in the search boxNo debounce on the search inputWait 300 ms after typing stops, cancel stale requests
Fast on your laptop, slow on phonesLarge images or a large JavaScript bundleResize images, lazy-load, split routes
Slow at busy times, errors about connectionsFree-tier compute or the connection limitFix the queries first, then upgrade compute
The first visit after a quiet week takes a minuteA free Supabase project was pausedRestore it; a paid plan can't be paused

N+1 queries: one request for every row

This is the most common cause I see in generated code, and the one that only shows up with real data. The app loads a list of orders, then for each order asks for its customer. Ten orders is eleven requests and feels fine. Five hundred orders is 501 requests, each paying the full network round trip. A Supabase collaborator wrote in a GitHub discussion that normal REST calls from the US take 100 to 200 ms, almost all of it network time, so 500 of them is over a minute even when every query is instant.

The pattern: a query inside a loop
const { data: orders } = await supabase.from("orders").select("*");

const withCustomers = await Promise.all(
  orders.map(async (order) => {
    const { data: customer } = await supabase
      .from("customers")
      .select("*")
      .eq("id", order.customer_id)
      .single();
    return { ...order, customer };
  }),
);

Supabase can return the related rows in the same query when a foreign key links the tables. One request, one round trip:

The fix: embed the related rows
const { data: orders } = await supabase
  .from("orders")
  .select("id, total, created_at, customers ( id, name, email )")
  .order("created_at", { ascending: false })
  .range(0, 19);

Look for the pattern wherever a query sits inside map, forEach, or a for loop, and wherever a component that renders inside a list does its own fetch in a useEffect. That second form is the same bug hidden one file away. A DEV Community guide from October 2026 describes a January 2026 r/lovable thread where a client app slowed to 30-second page loads at around 1,000 users, and the causes were an endpoint returning every record, a database call per row, and no pagination. All three are in this post.

select * with no limit, and the 1,000 row cap

Generated code fetches with select("*")and no limit because that worked on the test data. In production it does two things. It downloads every column, including the long description, the JSON blob, and the base64 image someone stored in a text column. And it stops at 1,000 rows without telling anyone: Supabase's docs say projects return a maximum of 1,000 rows by default, a limit you can change in the project's API settings, and they recommend keeping it low and paginating with range().

So an app with 1,400 customers shows 1,000 and the founder thinks 400 are lost. Raising the cap makes the page slower, not the data safer. Paginate instead. Supabase's docs say range() is 0-based and inclusive, so range(0, 19) is the first 20 rows, and the count option returns the total for the page control:

Pagination with range() and an exact count
const pageSize = 20;
const from = page * pageSize;

const { data, count } = await supabase
  .from("customers")
  .select("id, name, email, created_at", { count: "exact" })
  .order("created_at", { ascending: false })
  .range(from, from + pageSize - 1);

Name the columns the screen shows. A list needs five fields, not forty, and the response often drops from megabytes to kilobytes with no other change. Keep the order clause: without it, Postgres can return pages in a different order each time and rows appear twice or go missing as the user pages.

Missing indexes on foreign keys and filtered columns

An index is a lookup structure that lets Postgres find rows without reading the whole table. Postgres indexes primary keys on its own and nothing else. Generated schemas add user_id, organization_id, and statuscolumns and filter on them on every screen, with no index on any of them. Supabase's docs say to index the columns in where, join, and order by clauses that queries use often, and that aligning indexes with common query patterns can speed up retrieval by an order of magnitude.

Confirm it with EXPLAIN ANALYZE

Take the slow query from the report and run it in the SQL editor with explain analyze in front. The line to look for is Seq Scan on orders, which means Postgres read every row, and the Execution Timeat the bottom. Supabase's RLS performance page shows a sequential scan over a 100,000 row table taking 171 ms, with Rows Removed by Filter: 100000 as the sign that a filter ran on every row.

SQL: find the scan, add the index, check again
explain analyze
select id, total, created_at
from orders
where user_id = '0b6d1f3e-...'
order by created_at desc
limit 20;

-- Seq Scan on orders ... Execution Time: 171.033 ms

create index concurrently orders_user_id_created_at_idx
  on orders (user_id, created_at desc);

-- run the explain again:
-- Index Scan using orders_user_id_created_at_idx ... Execution Time: 0.2 ms

Supabase's docs say a plain create index locks the table against writes while it builds, and create index concurrently avoids that at the cost of taking longer, so use the second form on a live app. A composite index like the one above serves both the filter and the sort. The docs also warn against indexing everything: each index slows writes, so if the plan's cost doesn't drop, remove it.

Let Supabase find them

The Performance Advisor in the dashboard (Advisors, then Performance) runs checks that Supabase's docs list, including unindexed_foreign_keys for foreign key columns with no index and unused_index for ones no query uses. On a generated schema I expect the first list to be long. Index every foreign key the app filters or joins on, and every column a policy filters on, which is the next section.

RLS policies that call a function on every row

Row level security is the set of rules in the database that decide which rows each user can see. A policy like using (auth.uid() = user_id) is correct and slow, because Postgres may call auth.uid()once per row. Supabase's RLS docs say to write (select auth.uid()) instead, which lets Postgres run the function once per statement and cache the result. Their benchmark on a 100,000 row table shows what that is worth:

Supabase's RLS benchmark on a 100,000 row table, from its RLS performance page
Policy beforePolicy afterBeforeAfter
auth.uid() = user_id, no indexSame, with user_id indexed171 msunder 0.1 ms
auth.uid() = user_id(select auth.uid()) = user_id179 ms9 ms
is_admin() with a table join(select is_admin())11,000 ms7 ms
has_role() = role(select has_role()) = role178,000 ms12 ms
No to clause, anonymous userto authenticated170 msunder 0.1 ms

The 178 second row is the one that catches generated apps. The schemas I see from Lovable usually have a has_role() or is_admin() helper used in every policy, and each policy then runs that helper, with its own query, for every row it checks. In a Supabase GitHub discussion, a developer whose functions called auth.uid() per row saw one call go from 59 seconds to 251 ms after caching the value once, and database CPU fall from 99% to about 30%.

SQL: rewrite a slow policy
-- before
create policy "Users see own orders" on orders
  for select using (auth.uid() = user_id);

-- after
drop policy "Users see own orders" on orders;
create policy "Users see own orders" on orders
  for select to authenticated
  using ((select auth.uid()) = user_id);

create index concurrently if not exists orders_user_id_idx
  on orders (user_id);

Three changes: the function call is wrapped in select, the policy names the role so it doesn't run at all for anonymous requests, and the column it filters on has an index. Supabase's docs add that a column only counts as indexed when it is the first column of an index, so a primary key on (team_id, user_id) does not index user_id. The Performance Advisor flags unwrapped calls as auth_rls_initplan, "Slow security policy detected". Speed and safety are separate questions, though: a fast policy can still let the wrong people in, which is what the Supabase RLS checklist covers.

Filtering in the browser and fetching on every keystroke

The other half of the problem is on the front end. Generated code often fetches every row and then filters, sorts, and searches the array in JavaScript, because that was the shortest path to a working screen. With 300 rows nobody notices. With 30,000, every keystroke in the search box runs a filter over the whole array and re-renders the whole list, and the page freezes while the user types.

  • Move the filter into the query. .eq("status", "open"), .ilike("name", "%" + term + "%"), and .order() do in the database, with an index, what the array methods did in the browser without one.
  • Debounce the search box.Wait about 300 ms after the last keystroke before sending the request, and ignore a response that arrives after a newer request was sent, or the list flickers between results for "jo" and "john".
  • Fix the effect that fetches too often. A useEffect whose dependency array contains an object or array created during render runs after every render, which means a fetch after every render. The network tab shows the same request repeating with nothing changing. Depend on the ID, not the object.
  • Virtualize long lists. A table of 5,000 rows is 5,000 rows of DOM. Render only the rows on screen.

React's Profiler, in the React DevTools browser extension, shows which components render and how often. If one list item renders every time the parent does, wrap it in memo and stop passing it a new function on every render.

Large images and bundles in a Lovable app

A Lovable app is a React app that runs in the browser, so the visitor downloads the JavaScript for every screen before the first one appears. If the network tab shows requests to supabase.co that are quick and few, and the app still takes seconds to show anything on a phone, this is where to look. The two usual causes are user-uploaded images served at full size, so a grid of twenty 3 MB photos is 60 MB, and a bundle that includes a charting library, a rich text editor, and a PDF generator on every page, including the login page.

Resize images on upload or on request, set width and height so the page doesn't jump, and load images below the fold only when they scroll into view. Split the bundle by route with React.lazy, so the dashboard's charts load with the dashboard. This is the overlap with ordinary website speed work; if the slow thing is a marketing site rather than an app, the causes and fixes are in why a business website is slow.

Supabase free tier: compute limits and pausing

Two things about the free plan look like slowness and aren't a code problem. Supabase's docs list the free plan's Nano compute at shared CPU, up to 0.5 GB of memory, and 60 direct database connections, with a recommended maximum database size of 500 MB. That is enough for a small app with good queries, and not enough to hide bad ones: a sequential scan over a table that doesn't fit in memory hits the disk every time. In a GitHub discussion a Supabase collaborator wrote that a free instance performs about the same as an entry Pro instance, so upgrading without fixing the queries changes little.

The second is pausing. Supabase's docs say free projects that show low activity over a 7-day period are paused, with a warning email about a week before, and that a few user requests to the database each day are usually enough to avoid it. A paused project doesn't respond at all until someone restores it from the dashboard, so the symptom is an app that "takes forever" on the first visit after a quiet stretch, then works. Paid projects can't be paused. On Lovable Cloud the equivalent is the instance size: Lovable's docs say the free plan gets the Tiny size only, and that a database can also be paused when credits run out.

Order of operations: fix the queries and policies first, because they cost nothing and the gains are a hundredfold. Then, if the CPU and memory cards in Supabase or Lovable still show pressure at busy times, or you see "remaining connection slots are reserved" errors, buy compute.

What fast enough looks like for a Lovable app

Founders ask me for a number. For a Lovable app on Supabase, these are the targets I work to, and they come from the numbers above rather than from a benchmark of my own:

For comparison, two vibe-coded apps I took over. The Chat Companycame to me as a buggy AI-generated demo; I kept about 20% of the code, rewrote the rest, and it now handles 100,000+ messages a day for 30+ businesses, with replies about three times faster than typical chatbot platforms. A personalization engine for a marketing technology company arrived slow and unable to handle large contact lists; after an eight-week takeover it processes around 100,000 contacts in roughly an hour, because the heavy work runs in background queues instead of inside a web request, and serves each generated page in about 60 ms. Both started from the kind of code this post describes. The difference after the fix is not a faster framework, it's fewer, better queries and heavy work moved off the request.

When to get help with a slow Lovable app

Fix it yourself if the network tab points to one of the clear cases: a loop with a query in it, a select("*") with no range, or one slow query that explain analyzeshows as a sequential scan. Each is an afternoon with the fixes above, or a precise prompt to Lovable. Get help when every table is slow and you can't tell which policy is doing it, when fixing one screen slows another, when the schema has the same data in three tables, or when paying users are waiting while you read this.

A Launch-Ready Review reads the queries, policies, and indexes alongside the logins and payments and tells you what is slow, why, and what it costs to fix. The fixes themselves, with the index changes run safely on a live database, are part of a Production Sprint. What each costs is in the cost to fix a vibe-coded app. Slowness alone is almost never a reason to start over; if someone has told you it is, read whether to fix or rebuild a Lovable app first. Otherwise, send me the link and the slowest screen, and I'll tell you which row of the table it is.

Sources

  1. Query optimization with Postgres indexes, Supabase Docs
  2. Row Level Security: add indexes, call functions with select, Supabase Docs
  3. RLS performance and best practices: benchmark on a 100K row table, Supabase Docs
  4. Slow performance with auth.uid() and RPCs: 59 seconds to 251 ms, Supabase on GitHub
  5. Is the free tier supposed to be very slow? 100–200 ms REST latency, Supabase on GitHub
  6. Database advisors: unindexed foreign keys, slow security policy, Supabase Docs
  7. index_advisor: suggested indexes in the Query Performance report, Supabase Docs
  8. Managing indexes: create index concurrently, Supabase Docs
  9. pg_stat_statements: planning and execution statistics, Supabase Docs
  10. Debugging performance: explain() on a query, Supabase Docs
  11. range() modifier: 0-based and inclusive, Supabase Docs
  12. Fetch data: projects return a maximum of 1,000 rows by default, Supabase Docs
  13. Compute and disk: Nano tier memory and connection limits, Supabase Docs
  14. Free project pausing after 7 days of low activity, Supabase Docs
  15. Database capacity: slow queries and instance sizes in Lovable Cloud, Lovable Docs
  16. How to fix a slow vibe-coded app: the r/lovable 30-second thread, DEV Community
Milan Parmar

The developer

Freelance full-stack developer in Bengaluru with 5+ years of experience. I redesign websites, take over broken apps, and build SaaS products, with 18 case studies to show for it.

FAQs

Can I just ask Lovable to make my app faster?

For one cause at a time, yes. Lovable's docs say it can rank your queries by total execution time and add an index, and a precise prompt such as 'load the orders and their customers in one query with a limit of 20' usually works. A vague 'make it faster' tends to add caching on top of the real problem. Measure first, then ask for the specific fix.

Will upgrading my Supabase plan make my Lovable app faster?

Only if the database is actually out of memory or connections. A Supabase collaborator wrote in a GitHub discussion that a free instance performs about the same as an entry Pro instance. A query that scans a whole table because an index is missing stays slow on bigger hardware. Check the Query Performance report first; upgrade when CPU or memory pressure shows after the queries are fixed.

Does the Supabase region matter for a slow Lovable app?

Yes, for every request. Each call from the browser to Supabase crosses the distance between your user and the database, and a Supabase collaborator put normal REST latency from the US at 100 to 200 ms, almost all of it network time. A project in Frankfurt serving users in Sydney adds that on every call. You can't move a project's region in place, so choose it before launch.

Can Supabase Realtime or polling make my app slow?

It can. Generated code sometimes subscribes to a whole table or polls every few seconds and never unsubscribes when the screen closes, so each open tab keeps the database busy. In the network tab, look for a request that repeats on a timer while nothing changes. Subscribe to the rows the screen shows, filter the channel, and clean up in the effect's return function.