Problem
OpenUI Lang's premise is "render progressively as tokens arrive" — arrival rate is the reveal pace. That's ideal when the model streams the DSL smoothly. In practice the DSL often arrives bursty or whole:
- LLM tool-call generation emits a small structured DSL (a few blocks) in a fraction of a second.
- Response-streaming transports coalesce writes (AWS Lambda response streaming buffers small writes; CDNs re-clump), so even a token-streamed DSL lands in bursts.
When that happens, <Renderer> renders the whole thing at once → the components pop in instead of assembling. Text-streaming renderers solve the identical problem with client-side smoothing that decouples paint timing from arrival (Vercel's streamdown, assistant-ui's useSmooth — buffer the bursty stream, repaint at a steady rhythm). <Renderer> has no equivalent, so consumers must bolt pacing on from outside.
What we do today (workaround)
We char-reveal the DSL string ourselves before handing it to <Renderer>: advance a growing character count on a clock and pass response={dsl.slice(0, count)}. It works — the parser is permissive over partial input, and reconciliation by statement id keeps it stable across a growing response — but:
- It's indirect: we're throttling the serialized DSL string, an intermediate representation, from outside the parser.
- It has to be count-based / never-reset. A naive prefix reveal breaks when the DSL is non-monotonic — a ref-list-first
root = Section(title, [b0, b1, …]) line rewrites as blocks append, so a full.startsWith(shown) reveal blanks-and-rebuilds on every block. The renderer already handles this correctly internally (reconcile by statementId); a native option could reveal at the statement/element granularity and avoid the string-level fragility entirely.
Proposed API
A pacing option on <Renderer>, e.g.:
<Renderer response={dsl} isStreaming revealRate={{ targetMs: 1100 }} />
// or: reveal="smooth" | reveal={false}
Reveal newly-parsed elements/statements on a clock — drain the backlog toward a target build duration, honor prefers-reduced-motion, and show settled/hydrated content immediately (no fake assembly over old content). Because the parser already knows statement boundaries and reconciles by id, pacing at that layer is cleaner and more robust than any external string throttle.
Why it belongs in the library
Every OpenUI consumer streaming from a real LLM + transport hits bursty arrival, and each will otherwise reinvent a fragile external pacer. Text-streaming ecosystems standardized this (streamdown / useSmooth); the generative-UI renderer is the natural owner of the same capability.
Happy to contribute a PR if there's interest in the shape.
Problem
OpenUI Lang's premise is "render progressively as tokens arrive" — arrival rate is the reveal pace. That's ideal when the model streams the DSL smoothly. In practice the DSL often arrives bursty or whole:
When that happens,
<Renderer>renders the whole thing at once → the components pop in instead of assembling. Text-streaming renderers solve the identical problem with client-side smoothing that decouples paint timing from arrival (Vercel'sstreamdown, assistant-ui'suseSmooth— buffer the bursty stream, repaint at a steady rhythm).<Renderer>has no equivalent, so consumers must bolt pacing on from outside.What we do today (workaround)
We char-reveal the DSL string ourselves before handing it to
<Renderer>: advance a growing character count on a clock and passresponse={dsl.slice(0, count)}. It works — the parser is permissive over partial input, and reconciliation by statement id keeps it stable across a growingresponse— but:root = Section(title, [b0, b1, …])line rewrites as blocks append, so afull.startsWith(shown)reveal blanks-and-rebuilds on every block. The renderer already handles this correctly internally (reconcile by statementId); a native option could reveal at the statement/element granularity and avoid the string-level fragility entirely.Proposed API
A pacing option on
<Renderer>, e.g.:Reveal newly-parsed elements/statements on a clock — drain the backlog toward a target build duration, honor
prefers-reduced-motion, and show settled/hydrated content immediately (no fake assembly over old content). Because the parser already knows statement boundaries and reconciles by id, pacing at that layer is cleaner and more robust than any external string throttle.Why it belongs in the library
Every OpenUI consumer streaming from a real LLM + transport hits bursty arrival, and each will otherwise reinvent a fragile external pacer. Text-streaming ecosystems standardized this (streamdown / useSmooth); the generative-UI renderer is the natural owner of the same capability.
Happy to contribute a PR if there's interest in the shape.