Skip to main content
RapidDev - Software Development Agency
lovable-issues

Optimizing Large Asset Loading in Lovable Projects

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.

Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Advanced8 min read~15 minAll Lovable projects (Vite + React)March 2026RapidDev Engineering Team
TL;DR

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 slow

The 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 threshold

JavaScript 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 MB

A 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

1

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.

Before
typescript
// Large uncompressed images:
// public/images/hero.png — 4.2MB
// public/images/team-photo.jpg — 2.1MB
// public/images/product.png — 1.8MB
After
typescript
// 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.

2

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.

Before
typescript
// 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" />
After
typescript
// 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.

3

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.

Before
typescript
// All pages imported statically — one big bundle
import 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>
After
typescript
import { lazy, Suspense } from "react";
// Dynamic imports — each creates a separate bundle
const 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.

4

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.

Before
typescript
// User uploads stored in /public — bloats repository
// No CDN, no caching, large Git repo
After
typescript
// 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

src/App.tsx
1import { lazy, Suspense } from "react";
2import { BrowserRouter, Routes, Route } from "react-router-dom";
3import { Loader2 } from "lucide-react";
4
5// Route-level code splitting — each page is a separate bundle
6const Home = lazy(() => import("@/pages/Home"));
7const Dashboard = lazy(() => import("@/pages/Dashboard"));
8const Settings = lazy(() => import("@/pages/Settings"));
9const NotFound = lazy(() => import("@/pages/NotFound"));
10
11// Loading fallback shown while page bundle downloads
12const 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);
17
18const 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};
32
33export 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.

ChatGPT Prompt

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

Lovable Prompt

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.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your issue.

Book a free consultation

Need help with your Lovable project?

Our experts have built 600+ apps and can solve your issue fast. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.