If your Base44 site goes blank until you reload, check your animations before anything else. On mine, nothing had crashed: the content was there, stuck at opacity 0 behind framer-motion animations that never finished, and four fixes took permanent blank pages to zero.
Here's what it looked like. Click a link. Header's there, the middle is empty. You can scroll through nothing. Cmd+R and it's all back like nothing happened.
On a portfolio, that's the worst bug going. Someone clicks through to a case study and gets a white rectangle. Brilliant.
It took two rounds. Round one fixed the two bugs behind it. Round two added safety nets, so the next bug I haven't found yet can't do the same. That's the bit worth copying, and it comes down to one rule:
An animation should never be the thing that decides whether your content shows up.
(Google seeing an empty page is a different problem with its own fix: why Google sees an empty page on your Base44 app.)
First, work out which blank you've got
"Blank page" covers a few different bugs. Match what you see to the likely cause first. These are the ones I hit or tested for.
| What you see | Likely cause | Fix |
|---|---|---|
| You click a link, the old page fades out and nothing replaces it | The exit animation never finished, so the new page never mounted | 1 |
| Header shows, middle empty but scrollable, and every page after it blank too | An animation error stopped framer-motion's frame loop, so every entrance stays at opacity 0 | 2 and 4 |
| One section missing, the rest of the page fine | An in-view entrance that never fired | 2 |
| Blank right after you publish, only for people who already had the tab open | The old code files are gone, so a lazy-loaded page can't load | 3 |
| A totally empty screen, not even a header | The main code file failed to load, or a crash with nothing to catch it | 3, plus an error boundary per route |
The fastest test takes ten seconds:
Inspect the blank
Right-click the empty area and choose Inspect.
Walk up the tree
Click up through the parent elements and read their inline styles.
Look for opacity 0
An inline
opacity: 0means your content exists. It just never faded in. That's an animation problem, not a data problem.
Or ask the console for anything big that's fully transparent right now:
[...document.body.querySelectorAll('*')]
.filter((el) => getComputedStyle(el).opacity === '0' && el.getBoundingClientRect().height > 200)
.map((el) => `${el.tagName}.${el.className}`)Bug 1: a page transition that never finished
My pages fade out and in with framer-motion's AnimatePresence in mode="wait". That mode waits for the old page to finish leaving before it mounts the new one. Sensible, right up until the old page never finishes leaving.
AnimatePresence waits for every motion element inside the leaving page to report back. A shared-layout element that had moved (anything with a layoutId, like the marker on my glossary's A to Z rail) never did. So the old page sat there "leaving" forever, faded to nothing, and the new page never mounted.
Bug 2: one animation error froze every animation after it
Nastier. Base44's starter for my app came with framer-motion 11, which runs every animation job for a frame in one loop. If one job throws, that loop stays marked busy for good. A keyframe job that threw also stays queued and throws again every frame. Either way, no animation runs again in that tab.
My pages start at opacity 0 and fade in. So from then on, every page stays invisible. Blank but scrollable, until a reload gives you a fresh copy of framer.
The trigger was tiny. You click a link while a block is scrolling into view, and its "in view" callback lands just after the page change removed it. Framer reads the starting value for filter: 'blur(0px)', gets null, tries to mix null with a string and throws. One error. Whole site dead.
Round one's direct fix was small: pass the start value yourself, as filter: ['blur(6px)', 'blur(0px)'], so framer never reads it off an element that's already gone.
The 4 fixes
1. A watchdog on the page transition
The exit takes 160 ms. If the old page still hasn't left after 450 ms, the AnimatePresence boundary gets a new key, which remounts it and drops the old page. The new page fades in as normal.
Two helpers. PageContents makes the page wrapper's own fade the only exit the transition waits for, so a stuck layoutId element can't hold it up. MountSignal fires in the commit that mounts the new page, so the rest of the app (scroll-to-top, for one) knows the switch really happened. Every page also gets its own error boundary, so a crash shows a friendly message instead of wiping the header and footer too.
// Trimmed from my PageTransition.jsx
const WATCHDOG_MS = 450; // the exit itself takes 160ms
useLayoutEffect(() => {
if (prev.current === location.pathname) return undefined;
prev.current = location.pathname;
pending.current = true;
const t = window.setTimeout(() => {
if (!pending.current) return;
checkFramer(); // is framer's frame loop still alive? (fix 2)
setEpoch((e) => e + 1); // old page stuck: remount and drop it
}, WATCHDOG_MS);
return () => window.clearTimeout(t);
}, [location.pathname]);
return (
<AnimatePresence key={epoch} mode="wait" initial={epoch > 0}>
<motion.div key={location.pathname} /* fade in, fade out */>
<PageContents>
<MountSignal onMount={onPageMount} path={location.pathname} />
<ErrorBoundary retry>
<Suspense fallback={<RouteFallback />}>{outlet}</Suspense>
</ErrorBoundary>
</PageContents>
</motion.div>
</AnimatePresence>
);2. An entrance failsafe
Every element that starts hidden for an entrance carries a data-entrance attribute. Once its entrance should have finished (delay plus duration plus a second of grace, counted only while the tab is visible), anything still not fully visible and not animating is shown in its end state with plain CSS:
[data-entrance-shown],
html[data-motion-rescue] [data-entrance] {
opacity: 1 !important;
filter: none !important;
}Normal visits look exactly the same, because nothing happens until an entrance is already late. A block that reaches the top half of the screen without framer starting it gets started by the failsafe (the "one section missing" row).
When something was stuck, it also hands framer a job and counts five browser frames. If the job never runs, the loop is dead, so everything is shown and the next link click becomes a full page load. Cmd+R, done for the visitor.
3. One guarded reload when code files go missing
Not framer at all, this one. Each page's code lives in its own file with a hashed name that changes when the code changes. Publish a new build and anyone with the site already open is still running the old one. Their next click asks for a file that's gone.
One automatic reload fixes it by picking up the new build. The dangerous word is "one", because a file that's genuinely missing would reload forever. So the time of the last automatic reload goes in sessionStorage, and a second failure within 60 seconds shows a friendly message instead. It checks the site is reachable first too, because reloading offline swaps your page for the browser's error page.
The error wording differs by browser, so the check matches each one:
// From my chunkReload.js
const CHUNK_ERROR =
/Failed to fetch dynamically imported module|error loading dynamically imported module|Importing a module script failed|is not a valid JavaScript MIME type|Unable to preload CSS|Expected a JavaScript/i;
const RELOAD_KEY = 'aj:chunk-reload';
const RELOAD_GAP_MS = 60000; // a second failure inside this window shows the messageThe MIME type line is for Safari, which words it differently when a host answers a missing file with an HTML page. My first version missed it. A small inline guard in index.html does the same one-reload-then-message thing for files that load before the app even starts.
Check what your own host does, because it changes which errors you'll see. Here's mine:
curl -s -o /dev/null -w "%{http_code} %{content_type}\n" https://aatmanjain.com/assets/does-not-exist.js404 application/jsoncurl -s -o /dev/null -w "%{http_code} %{content_type}\n" https://aatmanjain.com/does-not-exist200 text/html; charset=utf-8On Base44, a missing file under /assets/ is a real 404. An unknown page is a 200 that returns the app shell. Your recovery code needs to know which is which.
4. Patching framer-motion at build time
Fixes 1 to 3 catch the damage. This one stops the crash. It's a small Vite plugin in my vite.config.js that edits framer-motion's own files as they're bundled. Seven edits: stop observing removed elements, let a removed element settle on its end value instead of null, let an animation that can't start jump to its end, and wrap the frame loop so one bad job gets dropped instead of killing the rest. The error still surfaces on a timer, so I still see it in the console.
// From my vite.config.js: the plugin, plus one of the seven edits
const FRAMER_PATCHES = [
{
file: /[\\/]framer-motion[\\/]dist[\\/]es[\\/]frameloop[\\/]render-step\.mjs$/,
find: 'callback(latestFrameData);',
replace: 'try { callback(latestFrameData); } catch (error) { step.cancel(callback); setTimeout(() => { throw error; }, 0); }',
},
// ...six more
];
function framerHardening() {
return {
name: 'aj:framer-hardening',
enforce: 'pre',
transform(code, id) {
const patches = FRAMER_PATCHES.filter((p) => p.file.test(id));
if (!patches.length) return null;
let out = code;
for (const p of patches) {
if (!out.includes(p.find)) {
this.warn(`framer-motion changed: a blank-page hardening edit was not applied to ${id.split('node_modules').pop()}`);
continue;
}
out = out.replace(p.find, p.replace);
}
return { code: out, map: null };
},
};
}How I checked it actually worked
I don't trust a fix I haven't tried to break. So I had Claude Code throw automated browser checks at the finished build, on a machine under heavy load the whole time (for once, helpful).
- Chromium checks at 1440 and 390 px
- 1,002
- WebKit checks
- 71
- pages left blank
- 0
- more checks after the performance work, still 0 blank
- 409
- Chromium: cold loads of 20 routes, double clicks, rapid back and forward, reloads mid-transition, and 4 to 6x CPU slowdown on fast 3G. No blank survived 15 seconds.
- WebKit managed about half a frame a second there, so the failsafe did the work. Pages still ended visible.
- Two builds with 118 of 158 JavaScript files renamed: exactly one reload onto the new build. A file missing for good gave one reload, then the message.
- Hung, failing and aborted backend requests: 42 of 42 pages still rendered content.
- All 340 animations after a link click matched the build before (352 with reduced motion). The one intended change: the first load no longer re-blurs blocks already on screen.
It isn't perfect. Once the one allowed reload is used up, a page whose file failed keeps showing the message until you reload by hand. And items added to a staggered list after it has played stay hidden (a framer limitation). I couldn't find anywhere on my site that does that. Yours might.
Steal this checklist
Reproduce it
Throttle CPU and network in DevTools, then click fast and hammer back and forward.
Inspect the blank
Inline
opacity: 0means an animation problem, not a data problem.Time out the transition
Give any
AnimatePresenceinmode="wait"a timeout, and only let the page wrapper's exit hold it up.Box in the crashes
Put an error boundary around each route, so one crash can't take out the header and footer too.
Add a failsafe
Show anything still hidden after its entrance should have finished.
Reload once
Catch chunk-load errors, reload once, and remember that you did.
Ask your host
Curl a fake
/assets/file and a fake page on your live site, so you know what it really returns.
Animations are the fun bit of building on Base44, and I'm keeping every one of mine. They're just not allowed to be load-bearing any more.
If your app keeps going blank and none of this matches, send me what you're seeing through the contact page. If I've seen it, I'll tell you. If I haven't, I want to.
FAQ
Why does my Base44 app show a blank page until I refresh?
On my site, the content was there but stuck invisible, because a page transition never finished or framer-motion's animations had stopped running. A refresh fixes it because it starts the animation library fresh. Inspect the empty area in DevTools: an inline opacity: 0 means it's an animation problem, not missing data.
Why does my Base44 site go blank right after I publish?
Your code files have hashed names that change when the code changes, so a tab opened before you published asks for files that no longer exist. Catch the chunk-load error and reload once, storing the time of that reload so a genuinely missing file can't cause a loop. If the reload has already happened, show a friendly message with a Reload button instead.
Should I just remove the animations?
You don't have to. Make sure no animation is the only thing that makes content visible: put a timeout on page transitions and add a failsafe that shows anything still hidden once its entrance should have finished. On my site that kept every animation identical and took permanent blanks to zero across about 1,000 browser checks.


