如何用 7 個步驟改善 AI 工具生成應用程式的載入速度與查詢效能
How to Fix a Slow Vibe-Coded App: A 7-Step Beginner Guide to Faster Pages and Queries
Vishal Chauhan 提供 7 步指南,説明如何改善由 Lovable、Bolt、Replit、v0、Cursor 或 Claude Code 建立的應用程式效能。
Your app worked fine when it was just you clicking around. Then real people showed up, and now pages hang on a spinner, images pop in one by one, and someone tells you "it feels slow on my phone."
This is one of the most common problems with apps built in Lovable, Bolt, Replit, v0, Cursor or Claude Code. AI builders are great at "make it work," but they don't think about how much data, how many images and how much code your app sends to a stranger on a phone. In a January 2026 r/lovable thread, a developer described a client app that slowed to 30+ second page loads at around 1,000 users. The causes were boring: one endpoint returning every record, a database call per row, and no pagination.
The good news: boring problems have boring fixes. This guide walks you through 7 steps, in the order I'd do them, with copy-paste prompts and short code. You don't need to be a performance expert.
Quick glossary
- LCP (Largest Contentful Paint): how long until the biggest thing on screen (usually a hero image or headline) shows up. Good is 2.5 seconds or less.
- INP (Interaction to Next Paint): how fast the page reacts when someone taps or clicks. Good is 200 milliseconds or less.
- CLS (Cumulative Layout Shift): how much stuff jumps around while loading. Good is 0.1 or less.
- Index: a lookup table for your database, like the index at the back of a book. Without one, the database reads every row to find what you asked for.
- Bundle: the JavaScript file(s) your app ships to the browser. Bigger bundle, slower start.
These three LCP/INP/CLS numbers are Google's Core Web Vitals. Google says a page passes when 75% of visits hit "good" on all three, measured separately on mobile and desktop.
Step 1: Measure before you change anything
If you skip this, you'll ask your AI to "make it faster," it will change 40 files, and you won't know if anything improved.
Do this once and write the numbers down:
- Open PageSpeed Insights, paste your live URL (not the editor preview), and check the Mobile tab. Note LCP, INP (if shown), CLS and the overall score.
- Run it on your 2-3 most important pages too: the landing page, the main logged-in screen, and any page with a list or feed.
- Open Chrome DevTools (right-click, Inspect), go to the Network tab, tick Disable cache, set throttling to a slower preset like Fast 4G, and reload. Note how long until the page looks usable.
One thing that confuses beginners: PageSpeed Insights shows two kinds of numbers. "Discover what your real users are experiencing" is field data from real Chrome users, and a new or low-traffic app usually won't have it yet. The lab numbers below it come from a single simulated test. Use the lab numbers as your before/after scoreboard.
Save this as a note in your project:
Speed baseline (date: ____ )
Page: / Mobile score: __ LCP: __s CLS: __
Page: /dashboard Mobile score: __ LCP: __s CLS: __
Page: /feed Mobile score: __ LCP: __s CLS: __
Slowest thing I noticed by eye: ____________________
Step 2: Find out what's actually heavy
Stay in the Network tab and look for the biggest offenders. Click the Size column to sort largest first, then use the filter buttons:
- Img: are any images over a few hundred KB? Are 1-2 MB photos being squeezed into small cards?
- JS: is one JavaScript file huge?
- Fetch/XHR: these are your database and API calls. Are there dozens of them on one page? Does one take seconds, or return a giant response?
In an April 2026 r/lovable thread about a card feed that got slower as you scrolled, a commenter suggested exactly this check: filter by Img and see if full-size photos are loading into small slots. That's usually the answer for image-heavy pages.
Write down the top 3 things you see. They tell you which steps below matter most.
Step 3: Fix your images (the fastest win)
Images are usually the biggest bytes on a page. Four rules cover most of it:
- Resize and compress before upload. A card that displays at 400px wide doesn't need a 4000px photo. Export at roughly 2x the display size and use WebP. Free tools like Squoosh work fine.
- Always set width and height. This reserves space so the layout doesn't jump (that's your CLS score).
-
Lazy-load images below the fold. Add
loading="lazy"so the browser only fetches them when the user scrolls near them. -
Never lazy-load the main image at the top. Google's web.dev guidance is blunt: never lazy-load your LCP image. Give it
fetchpriority="high"instead, and only on 1-2 images.
Here's what that looks like:
<!-- Hero image at the top: load it first -->
<img src="/hero.webp" alt="App dashboard preview" width="1200" height="630" fetchpriority="high">
<!-- Cards further down: load them when needed -->
<img src="/cards/item-42.webp" alt="Item 42" width="400" height="300" loading="lazy" decoding="async">
If your users upload photos to Supabase Storage, you can resize them on the fly with Supabase's image transformations. As of October 2026 that feature is on the Pro plan and above, with 100 origin images included per billing cycle and $5 per extra 1,000. On the free plan, resize images when they're uploaded instead.
Copy-paste prompt for your AI builder:
Audit every <img> and image component in this app.
1. The main image at the top of each page: add fetchpriority="high" and make sure it is NOT lazy-loaded.
2. Every other image: add loading="lazy" and decoding="async".
3. Every image must have explicit width and height (or a fixed aspect-ratio box) so the layout doesn't shift.
4. List any image files in the repo over 300 KB with their display size, but do not delete or replace them. I will compress them myself.
Show me the list of files you changed. Do not change any other behavior.
Step 4: Stop loading data you don't need
This is the one that bites at a few hundred users. AI-generated code often does three expensive things:
-
Fetches every column with
select('*'), including big text or JSON you never show. - Fetches every row with no limit, so the response grows with your user base.
- Makes one call per row (the "N+1 problem"): get 100 orders, then 100 more calls to get each customer name.
The fixes with the Supabase JavaScript client:
// Before: everything, forever
const { data } = await supabase.from('orders').select('*')
// After: only the columns you show, only this user's rows,
// the customer name in the SAME request, 20 at a time
const { data } = await supabase
.from('orders')
.select('id, total, created_at, customers(name)')
.eq('user_id', userId)
.order('created_at', { ascending: false })
.range(0, 19)
A few notes:
-
customers(name)pulls the related customer in the same request. It works when there's a foreign key fromorderstocustomers. (Table names here are just examples.) -
.range(0, 19)returns rows 1-20. Load the next 20 when the user scrolls or taps "Load more." - The
.eq('user_id', userId)filter looks redundant if your Row Level Security policy already limits rows to the user. Supabase's own performance docs recommend adding it anyway, because it helps the database plan a faster query.
Copy-paste prompt:
Find every database query in this app (Supabase client calls, fetch calls to my API, server routes).
For each one, tell me: which page uses it, whether it selects all columns, whether it has a limit, and whether it runs inside a loop or once per list item.
Then fix only these issues: select only the columns the UI uses, add pagination of 20 items to any list that can grow, and replace per-item queries with a single query that fetches related data together.
Keep the same UI and behavior. Show me a before/after table of the queries you changed.
Step 5: Add the database indexes your app is missing
If a specific screen is slow even after Step 4, the database may be reading every row to answer you. Supabase has a free checker for this.
- In your Supabase dashboard, open Advisors and look at the Performance Advisor.
- Look for "Unindexed foreign keys." It means a column like
orders.customer_idlinks to another table but has no index, so joins and filters on it are slower. - Also index any column your RLS policies filter on, usually
user_id.
The fix is one line of SQL per column, run in the SQL Editor:
create index if not exists orders_user_id_idx on orders (user_id);
create index if not exists orders_customer_id_idx on orders (customer_id);
One more RLS tip straight from Supabase's docs: wrap auth.uid() in a select inside your policies, so Postgres can run it once per query instead of once per row:
-- inside a policy
using ( user_id = (select auth.uid()) )
Want proof it worked? Run explain analyze on a slow query in the SQL Editor before and after. If you see "Seq Scan" on a big table and a large "Rows Removed by Filter," an index is likely the fix. Supabase's query optimization guide also says: if an index doesn't lower the cost, remove it. Indexes aren't free, so add them where the Advisor or a slow screen points, not everywhere.
If you followed my earlier guide to backups and safe database changes, add these as a migration file and test on your dev database first.
Step 6: Ship less JavaScript
Most AI-built apps are single-page React apps, and by default every page's code is packed into one big bundle. A visitor on your landing page downloads your entire dashboard, settings page and admin panel before seeing anything.
See what's in your bundle. For Vite apps (most Lovable and Bolt projects), install a visualizer as a dev dependency:
npm install -D rollup-plugin-visualizer
// vite.config.js (or .ts)
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { visualizer } from 'rollup-plugin-visualizer'
export default defineConfig({
plugins: [react(), visualizer({ open: true })],
})
Run npm run build and a map of your bundle opens. Big blocks are big libraries. Common surprises: a full icon pack, a charting library only used on one page, or two date libraries doing the same job.
Load pages only when they're visited. React's built-in lazy splits each page into its own file:
import { lazy, Suspense } from 'react'
const Dashboard = lazy(() => import('./pages/Dashboard'))
const Settings = lazy(() => import('./pages/Settings'))
// wrap your routes once:
<Suspense fallback={<div>Loading...</div>}>
{/* your <Routes> here */}
</Suspense>
Next.js apps (common from v0 and Cursor) already split code per route, so there your focus is mostly images, data and heavy libraries.
Copy-paste prompt:
I want a smaller JavaScript bundle without changing how the app looks or works.
1. Convert each top-level page/route to load with React.lazy and a single Suspense fallback.
2. List the 5 largest dependencies in the build and where each one is used.
3. For any library used on only one page, make sure it is only imported by that page.
Do not remove any library or feature without asking me first.
Step 7: Re-measure, then stop it from coming back
Run PageSpeed Insights and your Network-tab check again on the same pages, same settings, and fill in your baseline note with the new numbers. Change one area at a time (images, then queries, then bundle) so you know what helped. If something got worse, undo that change with Git.
Then protect your gains. AI tools will happily re-add a 3 MB image or a select('*') next week. Add rules to the file your tool reads every time (AGENTS.md, CLAUDE.md, Cursor rules, or Lovable's project knowledge):
Performance rules for this project:
- Never lazy-load the main image at the top of a page; give it fetchpriority="high".
- All other images use loading="lazy" and have explicit width and height.
- Never use select('*'). Select only the columns the UI shows.
- Every list that can grow is paginated (20 items) and never fetched in a loop.
- New foreign key or policy-filtered columns get an index in the same migration.
- Each page/route is lazy-loaded. Ask before adding any new dependency.
Re-check once a month and after any big feature.
Common mistakes
- Testing only on your laptop on Wi-Fi. Your users are on phones on mobile data. Always check the Mobile tab and throttle the Network tab.
- Lazy-loading everything. It feels like a speed fix, but lazy-loading the hero image makes your main number (LCP) worse.
- Asking the AI to "optimize performance" in one prompt. You get a giant diff and no idea what helped. Give it one job at a time.
- Chasing a 100 score. A landing page that goes from 45 to 85 on mobile is a real win. The last few points rarely matter to users.
- Adding indexes everywhere. Each index slows down writes a little. Add them where the Advisor or a slow screen tells you to.
Your action summary
- Record a baseline on your top 3 pages (PageSpeed Insights Mobile plus the Network tab on Fast 4G).
- Sort the Network tab by size and write down your top 3 heavy things.
- Fix images: resize to WebP, set width and height, lazy-load below the fold,
fetchpriority="high"on the hero. - Fix queries: only needed columns, paginate lists, no per-row calls, filter by user.
- Check Supabase's Performance Advisor and index unindexed foreign keys and RLS columns.
- Visualize your bundle and lazy-load each page.
- Re-measure, keep what helped, and add the performance rules to your AI rules file.
A fast app isn't about clever tricks. It's about not sending people things they didn't ask for: giant images, every row in your database, and code for pages they'll never open. Do these seven steps once, write the rules down, and your next feature starts fast instead of slowing everything else down.
來源:DEV Community · dev.to