Built for Shopify 4.1.1 Embedded app "flashes a different page before the actual page" on the reviewer's end, but I can't reproduce it on mine

TL;DR: My embedded Remix app was flagged under BFS 4.1.1 two times for “the page flashes a different page before displaying the actual page” on reload. The Shopify reviewer captured it on a screencast, but I cannot reproduce it on any of my own test stores or browsers. Looking for help identifying what causes this on a cold/first session and how to eliminate it.

Screencast from the reviewer: https://screenshot.click/28-55-40371-29635.webm

What the screencast actually shows

Looking closely, it’s not a different route/page of my app — it’s a brief flash of a bare, unstyled HTML page (no Polaris styling, essentially blank/raw) that appears for a fraction of a second, then resolves to the correct, fully-styled page. So the symptom is “flash of unstyled/blank HTML → styled app,” not “Page A → Page B.” It only happens on a cold session (fresh install / first load); on my own installs it never appears, presumably because my session and the App Bridge script are already warm/cached.

My understanding of the cause

I traced it to the App Bridge bootstrap in @shopify/shopify-app-remix. On a cold document load, authenticate.admin() renders a minimal bootstrap document — essentially:

<script data-api-key="..." src="https://cdn.shopify.com/shopifycloud/app-bridge.js"></script>
<script>window.open("<destination>", "_top")</script>

That bare page loads App Bridge, then redirects to the real styled route. Warm session = instant/invisible; cold session = the bare page lingers long enough to read as a flash. I believe this is the “purely HTML” flash.

Stack

  • @shopify/shopify-app-remix ^3.7.0, @shopify/app-bridge-react ^4.1.6, @shopify/polaris ^12.27.0, Remix ^2.16.1, React ^18.2.0
  • Embedded, token exchange (unstable_newEmbeddedAuthStrategy: true), API 2025-01, Fly.io
  • application_url = domain root; / redirects to /app preserving all query params; App Bridge provided via AppProvider in the body (not <head>).

What I’ve tried

  • Removed all defer()/Suspense/Await streaming — every loader awaits and returns json().
  • No leftover boilerplate routes.
  • Can’t reproduce on any browser/store, even throttled.

Questions

  1. Is this cold-session App Bridge bootstrap flash something apps must eliminate for BFS 4.1.1, and how?

  2. Does the extra //app redirect hop worsen it? Should application_url point directly at /app?

  3. Is moving app-bridge.js into the <head> the correct fix, or does the bootstrap bounce from authenticate.admin happen before the head script can help?

  4. Has anyone passed this exact 4.1.1 flag on embedded Remix + App Bridge v4, what fixed it?

    Kind regards,
    Tri

Looking at the video, the unstyled screen appears to be the homepage without CSS, rather than the App Bridge bootstrap page. On refresh, could you check:

  1. Does the document response contain the Polaris stylesheet link?
  2. Does the actual CSS request return 200 with a CSS content type?
  3. Does the stylesheet link appear in <head> and then disappear?

I’m leaning toward this being a CSS-loading issue. The stylesheet may be missing from the initial response, requested again, or removed during hydration.

Thanks for the reply! You’re right that it’s the homepage rendering without CSS, not the App Bridge bootstrap page.

After some digging, I believe cause was that the Polaris stylesheet was loaded via the route module’s links() export (app/routes/app.jsx) instead of the root route. Because it was scoped to that route’s lifecycle, there was a window during hydration/navigation where the stylesheet wasn’t applied, giving the styled → unstyled → styled flash like the video. It only showed on cold sessions since a warm cache applied the CSS instantly.

I moved the Polaris links() export to app/root.jsx so it’s document-level and always present:

// app/root.jsx
import polarisStyles from "@shopify/polaris/build/esm/styles.css?url";
export const links = () => [{ rel: "stylesheet", href: polarisStyles }];

Since root never unmounts, the link stays in <head> and can’t be removed during hydration. Does that line up with what you’d expect as the correct fix?

Hopefully 3rd time’s the charm and my app gets approved for BFS :sweat_smile:

Yes, that lines up with what I’d expect.

If you run into further technical issues, I’d recommend migrating to the React Router template, which is configured around the current embedded app requirements by default.

Fingers crossed for the next review!