Problem
Currently, await Speech() and other async element resolution happens eagerly during validation, not as part of the render computation graph. This causes several issues:
In the render service (render/)
When user code is submitted to /api/render, the validation phase (route.ts:64) calls tsxToElement(code) which fully executes the user's code including all await expressions:
// Validation phase (route.ts:64)
element = await tsxToElement(code, { gateway: varg, providerKeys });
If user code contains:
const { audio, segments } = await Speech({
children: ["s1", "s2", "s3"]
});
The Speech() API call happens during validation, before the job is queued. This means:
- Costs money for validation — ElevenLabs TTS costs $0.20-0.50 per call. Failed validation = wasted API calls.
- Validation takes 5-30 seconds instead of milliseconds — blocks the HTTP request thread
- No backend context in validation —
ResolveContext is only provided in the worker. Before alpha76, this caused sliceAudio to fail (no ffmpeg in Docker). After alpha76, segments work but the pattern is wrong.
- Duplicate execution risk — if validation runs the full pipeline AND the worker runs it again, assets get generated twice (though cache should prevent this if working correctly)
The deeper architectural issue
await in user code should be lazy — a declarative description of "this element depends on async work" that gets executed as part of the render DAG, not eagerly on first encounter.
Current behavior (eager)
const img = await Image({ prompt: "..." }); // ← API call happens HERE
const vid = Video({ prompt: { images: [img] } });
export default <Render><Clip>{vid}</Clip></Render>;
When the line await Image(...) is encountered, it immediately:
- Calls the AI SDK's
experimental_generateImage()
- Waits for the fal.ai API response
- Returns a
ResolvedElement with the image bytes/URL
This is imperative, not declarative.
Desired behavior (lazy)
const img = Image({ prompt: "..." }); // ← returns a VargElement (lazy promise)
const vid = Video({ prompt: { images: [img] } });
export default <Render><Clip>{vid}</Clip></Render>;
The Image() call returns a lazy thenable immediately. The actual API call happens when:
- The render pipeline processes the element tree
- All dependencies are topologically sorted
- Each element is resolved in dependency order
Benefits of lazy evaluation
- Fast validation — parsing + static analysis only, no API calls
- Parallelization — independent elements can generate concurrently
- Caching at the graph level — cache keys computed before execution
- Conditional execution — elements in unused branches never execute
- Retry/resume — failed nodes can retry without re-executing the whole tree
- Preview mode — show placeholders without running generators
Prior art
- React Server Components — async components resolve during render, not at definition
- Temporal workflows — async activities are declarative, executed by the orchestrator
- Airflow/Dagster — task DAGs execute lazily based on dependencies
- Remotion — frame rendering is lazy, computed per-frame on demand
Proposed solution
Phase 1: Separate validation from execution (near-term fix)
Render service changes:
-
route.ts validation phase should NOT call await tsxToElement(...). Instead:
- Parse the code with Sucrase (syntax check)
- Extract the
export default without executing it
- Validate it's a valid VargElement structure (static analysis)
- Return 400 if invalid, otherwise queue immediately
-
All actual execution (including await Speech()) happens in the worker with full ResolveContext
This is a minimal fix that solves the cloud render blocker without architectural changes.
Phase 2: Make await optional (medium-term)
Elements should work without await in user code:
// Both should work identically
const img1 = await Image({ prompt: "..." });
const img2 = Image({ prompt: "..." });
export default <Render><Clip>{img1}</Clip></Render>;
Implementation:
resolveLazy() already handles async components (functions returning Promises)
- Extend it to detect
VargElement & PromiseLike<ResolvedElement> and resolve them lazily
- User code can omit
await — the render pipeline awaits for them
Phase 3: Full computation graph (long-term)
Build a proper DAG:
- Parse phase — extract all element definitions, detect dependencies (e.g.,
images: [img] creates an edge Image → Video)
- Plan phase — topological sort, detect cycles, compute cache keys
- Execute phase — resolve elements in dependency order, maximizing parallelism
- Compose phase — editly stitching, same as today
Libraries to explore:
task-graph-runner — simple DAG executor
bull-board — already used for job queue, could extend for task DAGs
effect — already used in gateway, has built-in dependency tracking
Related issues
Files to modify
Near-term (Phase 1):
render/src/api/render/route.ts — make validation static
render/src/api/render/tsx-eval.ts — add a parse-only mode
Medium-term (Phase 2):
sdk/src/react/renderers/resolve-lazy.ts — handle thenable VargElements
sdk/src/react/elements.ts — document that await is optional
Long-term (Phase 3):
- New
sdk/src/react/graph/ module — DAG builder, executor
sdk/src/react/render.ts — use graph executor instead of direct resolution
Problem
Currently,
await Speech()and other async element resolution happens eagerly during validation, not as part of the render computation graph. This causes several issues:In the render service (
render/)When user code is submitted to
/api/render, the validation phase (route.ts:64) callstsxToElement(code)which fully executes the user's code including allawaitexpressions:If user code contains:
The
Speech()API call happens during validation, before the job is queued. This means:ResolveContextis only provided in the worker. Before alpha76, this causedsliceAudioto fail (no ffmpeg in Docker). After alpha76, segments work but the pattern is wrong.The deeper architectural issue
awaitin user code should be lazy — a declarative description of "this element depends on async work" that gets executed as part of the render DAG, not eagerly on first encounter.Current behavior (eager)
When the line
await Image(...)is encountered, it immediately:experimental_generateImage()ResolvedElementwith the image bytes/URLThis is imperative, not declarative.
Desired behavior (lazy)
The
Image()call returns a lazy thenable immediately. The actual API call happens when:Benefits of lazy evaluation
Prior art
Proposed solution
Phase 1: Separate validation from execution (near-term fix)
Render service changes:
route.tsvalidation phase should NOT callawait tsxToElement(...). Instead:export defaultwithout executing itAll actual execution (including
await Speech()) happens in the worker with fullResolveContextThis is a minimal fix that solves the cloud render blocker without architectural changes.
Phase 2: Make
awaitoptional (medium-term)Elements should work without
awaitin user code:Implementation:
resolveLazy()already handles async components (functions returning Promises)VargElement & PromiseLike<ResolvedElement>and resolve them lazilyawait— the render pipeline awaits for themPhase 3: Full computation graph (long-term)
Build a proper DAG:
images: [img]creates an edgeImage → Video)Libraries to explore:
task-graph-runner— simple DAG executorbull-board— already used for job queue, could extend for task DAGseffect— already used in gateway, has built-in dependency trackingRelated issues
Files to modify
Near-term (Phase 1):
render/src/api/render/route.ts— make validation staticrender/src/api/render/tsx-eval.ts— add aparse-onlymodeMedium-term (Phase 2):
sdk/src/react/renderers/resolve-lazy.ts— handle thenable VargElementssdk/src/react/elements.ts— document thatawaitis optionalLong-term (Phase 3):
sdk/src/react/graph/module — DAG builder, executorsdk/src/react/render.ts— use graph executor instead of direct resolution