Large assets slow down Lovable projects because every image and script loads before the page becomes interactive. Optimize by lazy loading images with the loading='lazy' attribute, code splitting routes with React.lazy(), compressing images before uploading, and using Supabase Storage CDN for user-uploaded files. Vite automatically handles code splitting for dynamic imports, so splitting route-level components is the highest-impact optimization.
Why large assets make Lovable apps load slowly
Lovable projects are single-page applications that load all JavaScript, CSS, and above-the-fold images before the page becomes interactive. If your app has large unoptimized images, uncompressed assets, or a single JavaScript bundle containing all page code, users wait longer than necessary for the initial load. Images are the biggest offender. A single unoptimized hero image can be 5-10MB, taking seconds to load on slower connections. Compressing images to WebP format and adding lazy loading for below-the-fold images dramatically improves perceived performance. Code splitting is the second biggest optimization. By default, Vite bundles all your component code into one JavaScript file. If your app has 20 pages, a user visiting the home page downloads the code for all 20 pages. Route-level code splitting with React.lazy() tells Vite to create separate bundles for each page, loading only the code needed for the current route.
- Uncompressed images (PNG, BMP) uploaded directly without optimization
- All page code bundled in one JavaScript file instead of split by route
- Large third-party libraries imported at the top level instead of dynamically
- No lazy loading on images below the fold — all images load on initial page render
- Static assets served without proper caching headers
Error messages you might see
Largest Contentful Paint (LCP) is too slowThe main visual content takes too long to appear. This is usually caused by a large hero image or unoptimized CSS loading. Compress images and consider using Vite's image optimization.
Total Blocking Time (TBT) exceeds thresholdJavaScript execution blocks the main thread for too long. This happens when a large JS bundle parses and executes. Code splitting reduces the initial bundle size.
Error: this exceeds GitHub's file size limit of 100.00 MBA file in your project is too large for GitHub. Compress the asset, move it to Supabase Storage, or use Git LFS for large files.
Before you start
- A Lovable project that loads slowly or has large asset files
- Access to an image compression tool (squoosh.app, tinypng.com)
- A general idea of which pages or assets are the heaviest
How to fix it
Compress images and convert to modern formats
WebP images are 25-35% smaller than PNG/JPEG with the same visual quality
Compress images and convert to modern formats
WebP images are 25-35% smaller than PNG/JPEG with the same visual quality
Before uploading images to your Lovable project, compress them using a tool like squoosh.app or tinypng.com. Convert images to WebP format for the best size-to-quality ratio. Target file sizes: hero images under 200KB, thumbnails under 50KB, icons under 10KB. Upload the compressed images to your /public folder, replacing the originals.
// Large uncompressed images:// public/images/hero.png — 4.2MB// public/images/team-photo.jpg — 2.1MB// public/images/product.png — 1.8MB// Compressed and converted:// public/images/hero.webp — 180KB (96% smaller)// public/images/team-photo.webp — 95KB (95% smaller)// public/images/product.webp — 72KB (96% smaller)// Update references in code:<img src="/images/hero.webp" alt="Hero" />Expected result: Page load time decreases significantly. Image quality remains visually identical at a fraction of the file size.
Add lazy loading to below-the-fold images
Lazy loading defers image downloads until they are about to scroll into view, reducing initial page load
Add lazy loading to below-the-fold images
Lazy loading defers image downloads until they are about to scroll into view, reducing initial page load
Add the loading='lazy' attribute to all images that are not visible when the page first loads (below the fold). The browser will only download these images when the user scrolls near them. Do NOT add loading='lazy' to the hero image or any image visible in the initial viewport — those should load immediately for the best perceived performance.
// All images load immediately, even those far below the fold<img src="/images/hero.webp" alt="Hero" /><img src="/images/feature-1.webp" alt="Feature 1" /><img src="/images/feature-2.webp" alt="Feature 2" /><img src="/images/testimonial.webp" alt="Testimonial" />// Hero loads immediately, others load when scrolled into view<img src="/images/hero.webp" alt="Hero" /><img src="/images/feature-1.webp" alt="Feature 1" loading="lazy" /><img src="/images/feature-2.webp" alt="Feature 2" loading="lazy" /><img src="/images/testimonial.webp" alt="Testimonial" loading="lazy" />Expected result: Only the hero image loads initially. Other images load as the user scrolls down, reducing initial page weight.
Add route-level code splitting with React.lazy
Code splitting creates separate JS bundles per page so users only download the code for the page they visit
Add route-level code splitting with React.lazy
Code splitting creates separate JS bundles per page so users only download the code for the page they visit
Replace static imports of page components with React.lazy() dynamic imports. Wrap your routes in a Suspense component with a loading fallback. Vite automatically creates separate bundles for each dynamically imported module. This means visiting the home page only downloads home page code, not the dashboard, settings, and admin code.
// All pages imported statically — one big bundleimport Home from "@/pages/Home";import Dashboard from "@/pages/Dashboard";import Settings from "@/pages/Settings";import Admin from "@/pages/Admin";<Routes> <Route path="/" element={<Home />} /> <Route path="/dashboard" element={<Dashboard />} /> <Route path="/settings" element={<Settings />} /> <Route path="/admin" element={<Admin />} /></Routes>import { lazy, Suspense } from "react";// Dynamic imports — each creates a separate bundleconst Home = lazy(() => import("@/pages/Home"));const Dashboard = lazy(() => import("@/pages/Dashboard"));const Settings = lazy(() => import("@/pages/Settings"));const Admin = lazy(() => import("@/pages/Admin"));<Suspense fallback={<div className="p-8">Loading...</div>}> <Routes> <Route path="/" element={<Home />} /> <Route path="/dashboard" element={<Dashboard />} /> <Route path="/settings" element={<Settings />} /> <Route path="/admin" element={<Admin />} /> </Routes></Suspense>Expected result: Each page loads as a separate bundle. The initial load is faster because only the current page's code is downloaded.
Use Supabase Storage for user-uploaded content
Supabase Storage serves files from a CDN with caching, and user uploads do not bloat your project's codebase
Use Supabase Storage for user-uploaded content
Supabase Storage serves files from a CDN with caching, and user uploads do not bloat your project's codebase
For user-uploaded images (profile photos, documents), store them in Supabase Storage instead of the /public folder. Supabase Storage serves files via CDN with automatic caching. Upload files through your app using the Supabase client library, and store the public URL in your database. This keeps your project repository small and leverages CDN edge caching for fast delivery. If setting up a full asset pipeline with Supabase Storage involves complex upload flows and image transformations, RapidDev's engineers have built this across 600+ Lovable projects.
// User uploads stored in /public — bloats repository// No CDN, no caching, large Git repo// Upload to Supabase Storage:const { data, error } = await supabase.storage .from('avatars') .upload(`${userId}/avatar.webp`, file);// Get the public CDN URL:const { data: urlData } = supabase.storage .from('avatars') .getPublicUrl(`${userId}/avatar.webp`);// Use in component:<img src={urlData.publicUrl} alt="Avatar" loading="lazy" />Expected result: User uploads are served from Supabase's CDN with caching. Your project repository stays small.
Complete code example
1import { lazy, Suspense } from "react";2import { BrowserRouter, Routes, Route } from "react-router-dom";3import { Loader2 } from "lucide-react";45// Route-level code splitting — each page is a separate bundle6const Home = lazy(() => import("@/pages/Home"));7const Dashboard = lazy(() => import("@/pages/Dashboard"));8const Settings = lazy(() => import("@/pages/Settings"));9const NotFound = lazy(() => import("@/pages/NotFound"));1011// Loading fallback shown while page bundle downloads12const PageLoader = () => (13 <div className="flex items-center justify-center min-h-screen">14 <Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />15 </div>16);1718const App = () => {19 return (20 <BrowserRouter>21 <Suspense fallback={<PageLoader />}>22 <Routes>23 <Route path="/" element={<Home />} />24 <Route path="/dashboard" element={<Dashboard />} />25 <Route path="/settings" element={<Settings />} />26 <Route path="*" element={<NotFound />} />27 </Routes>28 </Suspense>29 </BrowserRouter>30 );31};3233export default App;Best practices to prevent this
- Compress all images to WebP format before uploading — target under 200KB for hero images, under 50KB for thumbnails
- Add loading='lazy' to all images below the fold — never lazy load the hero image or above-the-fold content
- Use React.lazy() for route-level code splitting so each page loads its own JS bundle
- Wrap lazy-loaded routes in Suspense with a meaningful loading fallback (spinner, not blank screen)
- Store user-uploaded files in Supabase Storage instead of /public — it provides CDN delivery and caching
- Keep the /public folder small — only static assets like favicon, fonts, and essential images should live there
- Use Vite's built-in asset optimization — assets smaller than 4KB are automatically inlined as base64
- Add width and height attributes to images to prevent layout shift while they load
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
My Lovable project loads slowly. Here is my project structure: [paste file tree with asset sizes] Please: 1. Identify which assets are too large and need compression 2. Add React.lazy() code splitting to my routing setup 3. Add loading='lazy' to appropriate images 4. Show me how to upload images to Supabase Storage instead of /public 5. Suggest Vite build optimizations for smaller bundles
Optimize the loading performance of my app. Add route-level code splitting to @src/App.tsx by converting all page imports to React.lazy() with Suspense and a spinner fallback. Add loading='lazy' to all images in @src/pages/Home.tsx that are below the hero section. Do not lazy load the hero image. Keep all existing functionality — only add performance optimizations.
Frequently asked questions
How do I check what is making my Lovable app slow?
Open browser dev tools, go to the Network tab, and sort by size. The largest files are your biggest optimization targets. Also check the Lighthouse tab for a performance audit with specific recommendations.
What image format should I use in Lovable projects?
WebP for photographs and complex images — it is 25-35% smaller than JPEG/PNG with the same quality. SVG for icons and simple graphics. PNG only when you need transparency and WebP is not an option.
How does code splitting work in Lovable?
Replace static page imports with React.lazy(() => import('@/pages/Page')). Vite automatically creates separate JavaScript bundles for each lazy-loaded module. Users only download the code for the page they visit.
Should I store images in /public or Supabase Storage?
Use /public for essential static assets (logo, favicon, hero images). Use Supabase Storage for user-uploaded content (profile photos, documents). Supabase Storage provides CDN delivery and does not bloat your repository.
Does Vite optimize assets automatically?
Partially. Vite adds content hashes to filenames for cache busting, inlines small assets as base64, and tree-shakes unused code. But it does not compress images or split code automatically — you need to add lazy() for splitting and compress images before uploading.
What if I can't fix this myself?
If your project has many large assets and complex code splitting requirements, RapidDev's engineers can audit and optimize the entire app. They have optimized loading performance across 600+ Lovable projects.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your issue.
Book a free consultation