You open Giselle Playground, pick an app, type a request, and Run.
The best version of this experience is immediate:
- you get an instant “we’re moving” acknowledgment
- you see destination-shaped UI (task context + status)
- you never wonder if your click registered
This instant feeling feels natural to users. However, in Next.js, it's surprisingly hard to achieve using only the framework's standard features.
In Next.js, when you implement a flow like an input screen → a results screen, it’s common to split it into separate pages. And it’s also common to use loading.tsx (or loading.js) to improve the experience until the next page renders. However, loading.tsx can only show after the next page’s response has started coming back—meaning while your proxy (formerly middleware) is still running, loading.tsx won’t show at all. That moment of dead air is what I’ll call the No UI gap.
In this post, I’ll use a sample application to show two things:
- demonstrate how the No UI gap appears while
proxy.tsis running - the UX pattern we use in Giselle to keep the experience snappy anyway: an Optimistic Transition (a destination-shaped overlay)
1) Why “just add a spinner” isn’t good enough
A spinner is fine for “this button is busy.”
But Playground is a destination-shaped flow:
- after “Run”, users expect to land on a task surface
- they want to see what’s happening in context (agent, input, status)
- they want reassurance that the app has transitioned state
A spinner on the source screen says: “wait here.” A destination-like shell says: “you’re already on your way there.”
That difference matters most when navigation can’t even start rendering.
2) Next.js is great at smooth transitions—until it isn’t
In the App Router, two big tools improve perceived performance after the destination starts rendering:
loading.tsx: route-level render-time loading UI
A loading.tsx file can show while a segment is being prepared.
But the critical condition is timing:
loading.tsxcan only render once Next.js has actually entered the destination segment and started rendering it.
Streaming (Suspense): progressive rendering once the response starts
Streaming helps once the server starts producing bytes for the destination.
Again, same boundary:
- streaming only begins after rendering begins.
One boundary to remember
Both tools help after render starts.
If the delay happens before Next.js can start rendering the destination, these tools can’t show yet.
That’s exactly what middleware / proxy delay can create.
3) The surprise: loading.tsx and streaming don’t start under middleware / proxy.ts delay
To make this concrete, we’ll use a small sample app you can try in the browser: Next.js Loading Lab.
If you add a deterministic delay in request-time code (middleware/proxy), the timeline looks like this:
- user triggers navigation (
Link,router.push, action → redirect, etc.) - the request is held upstream by middleware / proxy delay
- only when that finishes can Next.js route + render the destination
- only then can
loading.tsx/ streaming appear
So when you feel the No UI gap, it’s not that Next “forgot” your loading.tsx.
It’s that render-time UI can’t start yet because the request hasn’t reached rendering.
This is an ordering problem (request-time vs render-time), not a bug.
4) Try it yourself: feel the No UI gap with proxy delay
If this still feels abstract, let’s try it. The goal is to make the boundary undeniable by introducing a delay you can feel consistently.
Steps
- Open Next.js Loading Lab.
- In the left menu, select
loading.tsx. - On the right, enter a keyword and click Open Summary.
- You should see the skeleton UI immediately, then the result page renders and fetches the Wikipedia summary.
Here is the baseline behavior without proxy delay: the loading.tsx skeleton shows immediately after click.
- Now go to Proxy (middleware) in the left menu and check Enable proxy.
- Set Proxy delay to something noticeable (e.g. 500–1500ms) and click Save.
- Repeat steps 2–3.
- This time, you’ll notice a “dead air” period: the skeleton won’t appear until after the proxy delay has elapsed.
With proxy delay enabled, notice the No UI gap: nothing can render (including loading.tsx) until the proxy finishes.
What to observe
Under proxy delay, look for:
- Before start: dead air (no route loading UI yet)
- After start: the usual App Router loading/streaming behaviors finally become available
That split is the entire point of this post.
5) The fix: “Optimistic Transition” (destination-shaped overlay that bridges the No UI gap)
You can see this solution working in the same sample app. In Next.js Loading Lab, enable Proxy (middleware) with a noticeable delay, then open Optimistic Transition and click Open Summary. Unlike loading.tsx, the overlay shows immediately on click—bridging the dead air while the proxy is still running.
With proxy delay enabled, this is what it looks like: the overlay appears immediately on click, even while the proxy is still running.
When the No UI gap happens, the problem isn’t “we need a loader.”
The real problem is:
the destination hasn’t started yet—so the user can’t see evidence of progress or state transition.
The solution is to render immediate, destination-shaped UI on the client before the destination can render.
Definition
An Optimistic Transition is UI that appears immediately and acts as a proxy of the final destination experience—so the user feels the transition has started even before the destination can render.
- not a generic spinner
- not “please wait”
- a faithful stand-in for the destination’s structure, showing what’s already known and honestly placeholdering what’s unknown
Why it works (relative to the boundary)
loading.tsx/ streaming improve what happens after render begins.- Optimistic Transition improves what happens before render can begin.
This is exactly the gap middleware/proxy delay exposes.
6) How Giselle applies it: layout-persistent overlay + store-driven control
To cover the No UI gap, two things must be true:
- the overlay must stay mounted during navigation
- the current screen must be able to show it instantly (synchronously with the user action)
That leads to a clean division of responsibilities:
Want the code? Start with the demo + the real product
If you’re an engineer, you probably want to verify the claim by reading actual code. Two good starting points:
- Next.js Loading Lab (repro demo): the exact lab used in this post is open-source at toyamarinyon/nextjs-loading-lab.
- Proxy delay (request-time):
proxy.ts - Proxy config cookies:
app/api/config/route.ts,lib/config.ts - Overlay pattern:
components/overlay.tsx(and the shared views undercomponents/)
- Proxy delay (request-time):
- Giselle (production OSS): Giselle itself is open-source at giselles-ai/giselle.
- Giselle is a monorepo: the Studio web app lives under
apps/studio.giselles.ai(Next.js App Router). - Playground routes/UI start here:
apps/studio.giselles.ai/app/(main)/playground - Tip: search within
apps/studio.giselles.aifor keywords likeoverlay,transition, oroptimisticto find the real implementation used in the product.
- Giselle is a monorepo: the Studio web app lives under
Placement: mount it in a persistent layout, not in page.tsx
If the overlay lives inside a page, it can unmount during navigation—exactly when you need it.
Mount it in a layout that persists across the transition (segment layout or root layout), and render it alongside {children}.
Control: trigger from the source screen (client), render from the layout
The source action (like “Run”) can:
- synchronously show overlay with known context (agent name, input parameters, etc.)
- start the async work / navigation
The overlay can then:
- show truthful status (“Starting…”, “Waiting for response…”, etc.)
- disappear when navigation completes (or when the destination tells it to hide)
Dismissal: make it explicit
Because a layout-mounted overlay persists, it won’t magically disappear. Make hiding explicit on success/failure:
- success path: hide on arrival (destination mounts)
- failure path: hide and show error + recovery affordance
7) When to use this pattern (and when not to)
Use Optimistic Transition when:
- the user action is high-intent (Run / Generate / Publish / Checkout)
- “dead air” would break trust
- you can render a truthful destination-like shell immediately
- you can define clear dismiss conditions
Avoid it when:
- navigation is low-intent / link-browsing heavy
- the action is not safely repeatable (unless you have idempotency)
- you can’t define crisp success/failure/cancel handling
Pitfalls to design for:
- Double-submit: users click again when they see nothing—disable triggers, consider idempotency keys
- Failure recovery: show retry/back/cancel clearly
- Honesty: no fake progress; show what you actually know (input, target, status)
8) Takeaway
If you remember one thing, make it this:
Next.js can only show loading.tsx / streaming after the destination has started rendering.
Middleware / proxy delay can block before that point—creating a No UI gap that render-time tools cannot cover.
Decision rule:
- if your slow step is before rendering begins → use an Optimistic Transition (overlay)
- if your slow step is after rendering begins → use
loading.tsxand streaming
In Giselle Playground, we optimize for “instant” by making the transition feel continuous—even when navigation hasn’t started rendering yet.
