Base44 apps are slow by default because of client-side rendering, monolithic JavaScript bundles, unpaginated entity queries that fetch thousands of records on mount, synchronous integration calls in render paths, and N+1 queries on entity relations. The fixes are mechanical: code-split routes via React lazy imports, paginate every entity list to 50 records with cursor navigation, cache integration calls (invokeLLM, generateImage) in entity-backed caches, batch entity reads with $in instead of N+1 loops, set explicit width/height/loading on every img tag, defer non-critical third-party scripts, move heavy data work to backend functions off the main thread, and put Cloudflare or Vercel in front of the platform domain to add HTTP caching and security headers. Measure field LCP and INP before and after the pass so each change proves its own gain.
Why base44 performance optimization matters for production apps
Performance is not vanity. INP above 200ms loses Google ranking. LCP above 2.5s leaves mobile visitors staring at an unfinished screen, and some leave. CLS above 0.1 makes mobile users tap the wrong button and abandon. An unoptimized Base44 app can miss all three thresholds on real-user data, on real mobile devices. That has direct revenue and SEO consequences.
The good news: every bottleneck is mechanical. There is no "Base44 is slow, accept it" answer. There is "the AI agent didn't paginate, didn't code-split, didn't cache, and didn't lazy-load images, and you have to fix each one." If you're not yet sure which of these is hurting you, start by diagnosing why your Base44 app feels slow, then come back here for the fixes. This guide, written by the lead engineer at Base44Devs, walks the fixes in priority order.
What good base44 performance optimization targets look like
Use Google's official Core Web Vitals thresholds, measured on the 75th percentile of real-user mobile traffic, not on your laptop in Chrome:
- LCP (Largest Contentful Paint) ≤ 2.5s — the moment the largest visible element finishes painting.
- INP (Interaction to Next Paint) ≤ 200ms — the worst input-response latency in the session.
- CLS (Cumulative Layout Shift) ≤ 0.1 — total layout movement during load.
- TTFB (Time to First Byte) ≤ 600ms — server response time.
Measure with PageSpeed Insights (real-user CrUX data), Vercel Speed Insights, or SpeedCurve. Lab data (Lighthouse local) flatters; field data tells the truth.
The 8 bottlenecks base44 performance optimization must fix, in order of impact
1. Unpaginated entity queries (highest impact)
The Base44 AI agent routinely emits code like:
// Bad: loads everything on mount.
useEffect(() => {
base44.entities.Todo.filter({ user_id: currentUser.id }).then(setTodos);
}, []);
If Todo has 2,000 rows, this fetches 2,000 records, serializes 2,000 JSON objects, and renders 2,000 list items. The fetch and the render both grow with the row count, and LCP waits on both.
Fix: paginate.
const PAGE_SIZE = 50;
useEffect(() => {
base44.entities.Todo.filter(
{ user_id: currentUser.id },
{ sort: "-created_date", limit: PAGE_SIZE },
).then((page) => setTodos(page.items));
}, []);
For pages that need more than the first page, add a load-more button or infinite scroll that passes the page's next_cursor back as cursor.
Expected improvement: faster LCP on entity-list-heavy pages, because the first fetch and render shrink to one page of records.
2. No code splitting
Base44's AI agent emits monolithic page bundles. Every route's JS is loaded on initial page load, even routes the user will never visit. Much of that initial JavaScript is unused on the route the user actually opened.
Fix: dynamic imports per route.
import { lazy, Suspense } from "react";
const SettingsPage = lazy(() => import("./pages/Settings"));
const AdminPage = lazy(() => import("./pages/Admin"));
function App() {
return (
<Suspense fallback={<Loader />}>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/settings" element={<SettingsPage />} />
<Route path="/admin" element={<AdminPage />} />
</Routes>
</Suspense>
);
}
The Base44 IDE supports React's lazy and dynamic imports. The bundler will split per import, so each lazy route comes out of the initial bundle.
Expected improvement: faster LCP and time-to-interactive on cold loads, because less JavaScript has to download and parse before first render.
3. Synchronous integration calls in render paths
The agent sometimes emits code like:
// Very bad: blocks render on a 4-second LLM call.
const summary = await base44.integrations.Core.InvokeLLM({ prompt: "..." });
return <div>{summary}</div>;
This makes the page wait for the LLM response before any content renders. INP hits 4+ seconds.
Fix: defer to a useEffect with optimistic UI.
const [summary, setSummary] = useState<string | null>(null);
useEffect(() => {
base44.integrations.Core.InvokeLLM({ prompt: "..." }).then(setSummary);
}, []);
return summary === null
? <Skeleton lines={3} />
: <div>{summary}</div>;
Better still, cache the result in an entity so subsequent loads are instant:
async function getSummary(promptHash: string) {
const cached = await base44.entities.LLMCache.filter({ hash: promptHash }, undefined, 1);
if (cached.length > 0) return cached[0].response;
const response = await base44.integrations.Core.InvokeLLM({ prompt: "..." });
await base44.entities.LLMCache.create({ hash: promptHash, response });
return response;
}
Expected improvement: the page renders without waiting on the LLM; repeat requests hit the cache instead of spending credits, so the savings track your cache hit rate.
4. N+1 queries on entity relations
Another agent-emitted pattern:
// Loads todos, then for each todo loads its project.
const todos = await base44.entities.Todo.filter({ user_id });
const todosWithProjects = await Promise.all(
todos.map(async t => ({
...t,
project: await base44.entities.Project.get(t.project_id),
}))
);
For 50 todos that's 51 sequential or near-sequential API calls.
Fix: batch with $in.
const todos = await base44.entities.Todo.filter({ user_id });
const projectIds = [...new Set(todos.map(t => t.project_id))];
const projects = await base44.entities.Project.filter({ id: { $in: projectIds } });
const byId = Object.fromEntries(projects.map(p => [p.id, p]));
const todosWithProjects = todos.map(t => ({ ...t, project: byId[t.project_id] }));
Two API calls instead of 51. Latency drops proportionally.
Expected improvement: a speedup on relation-heavy lists that grows with the number of rows.
5. Images without explicit dimensions and lazy loading
The agent rarely sets width, height, and loading on <img> tags. Result: layout shift on every image load (CLS), and every off-screen image downloads on initial load (LCP).
Fix:
<img
src="/avatar.jpg"
alt="User avatar"
width="48"
height="48"
loading="lazy"
decoding="async"
/>
For above-the-fold images, use loading="eager" and fetchpriority="high". Below-the-fold, always lazy. Always set explicit dimensions to reserve layout space.
For user-uploaded images, generate a thumbnail variant in a backend function and serve it for list views; only load the full image on detail.
Expected improvement: layout shift from images goes away, and LCP improves on image-heavy pages because off-screen images stop competing for bandwidth.
6. No HTTP caching
Base44 serves all assets with conservative cache headers, and dynamic responses with no caching at all. Every page load re-downloads assets the browser has from yesterday.
Fix: put Cloudflare in front of your custom domain, and add a Worker that:
- Sets
Cache-Control: public, max-age=31536000, immutableon hashed asset URLs (JS, CSS, fonts). - Adds
Cache-Control: public, max-age=300, stale-while-revalidate=86400to public marketing pages. - Strips cache headers entirely from authenticated routes (they should not be cached at the edge).
Cloudflare's free tier handles this. The Worker code is roughly 30 lines.
Expected improvement: repeat visits load cached assets instead of re-downloading them, cutting repeat-visit LCP and bandwidth cost.
7. Heavy work on the main thread
The AI agent emits synchronous filter/sort/aggregate code in render paths:
// Blocks main thread for hundreds of ms on large lists.
const sorted = todos
.filter(t => !t.archived)
.sort((a, b) => b.priority - a.priority)
.map(t => ({ ...t, slug: slugify(t.title) }));
For 50 todos this is fine. For 5,000, it locks the UI.
Fix: do the work server-side via a backend function, or push to a worker.
// Backend function: base44/functions/listSortedTodos/entry.ts
import { createClientFromRequest } from "npm:@base44/sdk";
Deno.serve(async (req) => {
const base44 = createClientFromRequest(req); // acts as the signed-in caller
const { user_id } = await req.json();
const todos = await base44.entities.Todo.filter({ user_id, archived: false });
todos.sort((a, b) => b.priority - a.priority);
return new Response(JSON.stringify(todos.map(t => ({ ...t, slug: slugify(t.title) }))), {
status: 200,
headers: { "content-type": "application/json" },
});
});
This keeps the main thread free for paint and input.
Expected improvement: INP drops dramatically on data-heavy pages.
8. Render-blocking third-party scripts
Stripe.js, Intercom, analytics scripts often land in <head> synchronously. Each one delays first paint.
Fix: load every third-party script with async or defer, ideally only on the page that needs it. Stripe.js, for instance, only needs to load on the checkout page.
// Load Stripe.js only when the user is about to check out.
const stripePromise = loadStripe(STRIPE_PUBLISHABLE_KEY);
function CheckoutPage() {
return (
<Elements stripe={stripePromise}>
<CheckoutForm />
</Elements>
);
}
For analytics, prefer a lightweight async script such as Plausible over Google Analytics, which is heavier and makes more round trips.
Expected improvement: earlier first paint; INP improvement on input-heavy flows.
CSR and SEO interaction
Base44's default client-side rendering is the single biggest performance problem for marketing pages, and a much smaller SEO problem. Base44 apps render client-side for visitors, but Base44 serves search engines and AI crawlers a prerendered version of each page — real headings, content, meta tags and structured data — refreshed when you publish (Base44 SEO docs, read 23 Sep 2026). Crawlers do not wait on your JavaScript; your visitors do, which is why a 4-second LCP still costs you field Core Web Vitals and bounces. When a Base44 app does not show in Google, the cause is usually configuration — the SEO toggle switched off (which disables prerendering), pages behind login or set to noindex, or a new domain Google has not crawled yet. We cover that diagnosis in why a Base44 app is not showing in Google and the Base44 SEO best practices article.
For marketing pages where first-load speed matters, the right answer is to leave Base44's default rendering and serve those routes to visitors from a static or SSR layer in front. A simple Cloudflare Worker that fetches the public-facing data and returns server-rendered HTML covers the marketing case. The logged-in app continues to run client-side as the platform expects.
How to measure base44 performance optimization results
Without measurement, every "optimization" is theater. Wire these in before you make changes:
- PageSpeed Insights for the homepage and top three landing pages. Bookmark and check weekly. Watch field metrics, not lab.
- Vercel Speed Insights or SpeedCurve for production. Real-user data segmented by route, device, country.
- Web Vitals JS in your app shell, sending to Plausible or your analytics. Catches regressions between PSI runs.
- A weekly Lighthouse run via WebPageTest with the "median of 9 runs" config, on a Moto G4 throttled to 4G.
Set a budget: any commit that regresses LCP by more than 200ms or INP by more than 50ms is a bug to fix before the next deploy.
Common base44 performance optimization mistakes to avoid
Optimizing on a fast laptop in Chrome. The benchmark is a mid-tier Android on 3G. Test there.
Trusting Lighthouse over CrUX. Lighthouse is lab; CrUX is field. They diverge significantly. Real users are what matters.
Treating one page's metrics as the whole app's. Marketing pages and authenticated app shells have different bottlenecks. Measure both separately.
Skipping the cache pass. HTTP caching is the highest-leverage change after pagination, and it is easy to skip because nothing breaks without it.
Adding tools without removing any. Each new analytics, monitoring, or chat tool adds script weight and main-thread work. Audit your script tags monthly and delete what you don't use.
Treating performance as one-time work. Every new feature can regress. Bake budgets into CI.
Base44 performance optimization checklist (10 items)
| # | Change | Effort | Expected gain |
|---|---|---|---|
| 1 | Paginate every entity list to 50 records | 30 min/page | Faster LCP on list pages |
| 2 | Code-split routes via dynamic imports | 1–2 hours | Smaller initial bundle, faster cold loads |
| 3 | Cache LLM and integration calls in entities | 2–4 hours | Render stops waiting on the call; fewer credits |
| 4 | Batch N+1 entity queries with $in | 30 min/site | One request per relation instead of one per row |
| 5 | Set width, height, loading on every img | 1–2 hours | Lower CLS |
| 6 | Add Cloudflare Workers for HTTP caching | 2–4 hours | Faster repeat visits |
| 7 | Move heavy data work to backend functions | 1–3 hours | Main thread free for input (lower INP) |
| 8 | Defer non-critical third-party scripts | 1 hour | Earlier first paint |
| 9 | Pre-render marketing routes via Worker | 4–8 hours | HTML on first load for visitors |
| 10 | Wire Web Vitals to analytics, set budget | 2 hours | Detection of regressions |
Work through the list in order and re-measure field data after each change; the target is passing all three Core Web Vitals at the 75th percentile.
Want us to optimize your Base44 app?
Our $497 audit measures all three Core Web Vitals on real-user data, identifies the top 5–10 bottlenecks, and delivers a prioritized fix plan with expected gains per change. If you want us to execute the fixes, that's a 48-hour fix sprint at $1,500. Order an audit or book a free 15-minute call.
Related reading
- Base44 Database Best Practices — schema and query patterns that compose with the performance work above.
- Base44 Deployment Checklist — per-deploy performance checks.
- Base44 Credit System Explained — caching integrations is also the biggest credit-saver.