Skip to content

Why my Base44 site kept going blank, and the 4 fixes that stopped it

My Base44 portfolio kept going blank until a reload. Nothing had crashed: the pages were stuck behind framer-motion animations that never finished. How to tell which blank you've got, the two bugs behind mine, and the four fixes that took permanent blanks to zero in about 1,000 Chromium and 71 WebKit checks.

Aatman (AJ) Jain9 min readUpdated 28 Sept 2026
A blank, undeveloped polaroid taped to yellow paper, with shake lines, a faint ghost of a house inside, an orange reload arrow and an hourglass sticker.
A blank, undeveloped polaroid taped to yellow paper, with shake lines, a faint ghost of a house inside, an orange reload arrow and an hourglass sticker.

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.

Me, after two rounds of this

(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 seeLikely causeFix
You click a link, the old page fades out and nothing replaces itThe exit animation never finished, so the new page never mounted1
Header shows, middle empty but scrollable, and every page after it blank tooAn animation error stopped framer-motion's frame loop, so every entrance stays at opacity 02 and 4
One section missing, the rest of the page fineAn in-view entrance that never fired2
Blank right after you publish, only for people who already had the tab openThe old code files are gone, so a lazy-loaded page can't load3
A totally empty screen, not even a headerThe main code file failed to load, or a crash with nothing to catch it3, plus an error boundary per route

The fastest test takes ten seconds:

  1. Inspect the blank

    Right-click the empty area and choose Inspect.

  2. Walk up the tree

    Click up through the parent elements and read their inline styles.

  3. Look for opacity 0

    An inline opacity: 0 means 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:

js
[...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.

The exit takes 160 ms. Without a watchdog, a stuck one lasts forever.

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.

Bug 2, and what fix 4 does about it

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.

jsx
// 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:

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.

What a visitor gets when a file is still missing after the one reload. Real site, with the file blocked in Playwright.

The error wording differs by browser, so the check matches each one:

js
// 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 message

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

Terminal
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-8

On 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.

js
// 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

  1. Reproduce it

    Throttle CPU and network in DevTools, then click fast and hammer back and forward.

  2. Inspect the blank

    Inline opacity: 0 means an animation problem, not a data problem.

  3. Time out the transition

    Give any AnimatePresence in mode="wait" a timeout, and only let the page wrapper's exit hold it up.

  4. Box in the crashes

    Put an error boundary around each route, so one crash can't take out the header and footer too.

  5. Add a failsafe

    Show anything still hidden after its entrance should have finished.

  6. Reload once

    Catch chunk-load errors, reload once, and remember that you did.

  7. 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.

Keep reading

More posts on the same things.

Newsletter

Liked this one?

Get the next post in your inbox. Only when there is something worth sending.

No spam. Unsubscribe whenever.

Got an app in mind?

I build and fix Base44 apps, and I'm looking for a remote internship. Tell me what you're working on.