Skip to content

Preload workflow step bundle lazily#2517

Draft
TooTallNate wants to merge 1 commit into
mainfrom
nate/lazy-step-bundle-preload
Draft

Preload workflow step bundle lazily#2517
TooTallNate wants to merge 1 commit into
mainfrom
nate/lazy-step-bundle-preload

Conversation

@TooTallNate

Copy link
Copy Markdown
Member

Summary

  • Split deferred Next workflow flow routes so step registrations live in a sibling __workflow_steps.js module instead of top-level flow route imports.
  • Add a preloadStepBundle runtime hook that starts step bundle loading after run_started or immediately for background step deliveries, then awaits it only before step execution.
  • Cover no-step replay preload failures and inline-step preload ordering in runtime tests, plus generated route shape in deferred builder tests.

Testing

  • pnpm vitest run packages/core/src/runtime.test.ts
  • pnpm vitest run packages/next/src/builder-deferred.test.ts
  • git diff --check

Notes

  • Attempted pnpm --filter @workflow/core typecheck and pnpm --filter @workflow/next build; both currently fail on existing unrelated TypeScript issues in step-executor.ts and older builder-deferred.ts call sites.

@vercel

vercel Bot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
example-nextjs-workflow-turbopack Ready Ready Preview, Comment Jun 18, 2026 7:21pm
example-nextjs-workflow-webpack Ready Ready Preview, Comment Jun 18, 2026 7:21pm
example-workflow Ready Ready Preview, Comment Jun 18, 2026 7:21pm
workbench-astro-workflow Ready Ready Preview, Comment Jun 18, 2026 7:21pm
workbench-express-workflow Ready Ready Preview, Comment Jun 18, 2026 7:21pm
workbench-fastify-workflow Ready Ready Preview, Comment Jun 18, 2026 7:21pm
workbench-hono-workflow Ready Ready Preview, Comment Jun 18, 2026 7:21pm
workbench-nitro-workflow Ready Ready Preview, Comment Jun 18, 2026 7:21pm
workbench-nuxt-workflow Ready Ready Preview, Comment Jun 18, 2026 7:21pm
workbench-sveltekit-workflow Ready Ready Preview, Comment Jun 18, 2026 7:21pm
workbench-tanstack-start-workflow Ready Ready Preview, Comment Jun 18, 2026 7:21pm
workbench-vite-workflow Ready Ready Preview, Comment Jun 18, 2026 7:21pm
workflow-docs Ready Ready Preview, Comment, Open in v0 Jun 18, 2026 7:21pm
workflow-swc-playground Ready Ready Preview, Comment Jun 18, 2026 7:21pm
workflow-tarballs Ready Ready Preview, Comment Jun 18, 2026 7:21pm
workflow-web Ready Ready Preview, Comment Jun 18, 2026 7:21pm

@changeset-bot

changeset-bot Bot commented Jun 18, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 5c9d38e

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@github-actions

github-actions Bot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

Passed Failed Skipped Total
✅ ▲ Vercel Production 1442 0 230 1672
✅ 💻 Local Development 1909 0 219 2128
✅ 📦 Local Production 1909 0 219 2128
✅ 🐘 Local Postgres 1895 0 233 2128
✅ 🪟 Windows 152 0 0 152
✅ 📋 Other 885 0 179 1064
Total 8192 0 1080 9272

Details by Category

✅ ▲ Vercel Production
App Passed Failed Skipped
✅ astro 125 0 27
✅ example 125 0 27
✅ express 125 0 27
✅ fastify 125 0 27
✅ hono 125 0 27
✅ nextjs-turbopack 149 0 3
✅ nextjs-webpack 149 0 3
✅ nitro 125 0 27
✅ nuxt 125 0 27
✅ sveltekit 144 0 8
✅ vite 125 0 27
✅ 💻 Local Development
App Passed Failed Skipped
✅ astro-stable 127 0 25
✅ express-stable 127 0 25
✅ fastify-stable 127 0 25
✅ hono-stable 127 0 25
✅ nextjs-turbopack-canary 133 0 19
✅ nextjs-turbopack-stable-lazy-discovery-disabled 152 0 0
✅ nextjs-turbopack-stable-lazy-discovery-enabled 152 0 0
✅ nextjs-webpack-canary 133 0 19
✅ nextjs-webpack-stable-lazy-discovery-disabled 152 0 0
✅ nextjs-webpack-stable-lazy-discovery-enabled 152 0 0
✅ nitro-stable 127 0 25
✅ nuxt-stable 127 0 25
✅ sveltekit-stable 146 0 6
✅ vite-stable 127 0 25
✅ 📦 Local Production
App Passed Failed Skipped
✅ astro-stable 127 0 25
✅ express-stable 127 0 25
✅ fastify-stable 127 0 25
✅ hono-stable 127 0 25
✅ nextjs-turbopack-canary 133 0 19
✅ nextjs-turbopack-stable-lazy-discovery-disabled 152 0 0
✅ nextjs-turbopack-stable-lazy-discovery-enabled 152 0 0
✅ nextjs-webpack-canary 133 0 19
✅ nextjs-webpack-stable-lazy-discovery-disabled 152 0 0
✅ nextjs-webpack-stable-lazy-discovery-enabled 152 0 0
✅ nitro-stable 127 0 25
✅ nuxt-stable 127 0 25
✅ sveltekit-stable 146 0 6
✅ vite-stable 127 0 25
✅ 🐘 Local Postgres
App Passed Failed Skipped
✅ astro-stable 126 0 26
✅ express-stable 126 0 26
✅ fastify-stable 126 0 26
✅ hono-stable 126 0 26
✅ nextjs-turbopack-canary 132 0 20
✅ nextjs-turbopack-stable-lazy-discovery-disabled 151 0 1
✅ nextjs-turbopack-stable-lazy-discovery-enabled 151 0 1
✅ nextjs-webpack-canary 132 0 20
✅ nextjs-webpack-stable-lazy-discovery-disabled 151 0 1
✅ nextjs-webpack-stable-lazy-discovery-enabled 151 0 1
✅ nitro-stable 126 0 26
✅ nuxt-stable 126 0 26
✅ sveltekit-stable 145 0 7
✅ vite-stable 126 0 26
✅ 🪟 Windows
App Passed Failed Skipped
✅ nextjs-turbopack 152 0 0
✅ 📋 Other
App Passed Failed Skipped
✅ e2e-local-dev-nest-stable 127 0 25
✅ e2e-local-dev-tanstack-start- 127 0 25
✅ e2e-local-postgres-nest-stable 126 0 26
✅ e2e-local-postgres-tanstack-start- 126 0 26
✅ e2e-local-prod-nest-stable 127 0 25
✅ e2e-local-prod-tanstack-start- 127 0 25
✅ e2e-vercel-prod-tanstack-start 125 0 27

📋 View full workflow run

@github-actions

github-actions Bot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

📈 Comparing against baseline from main branch. Green 🟢 = faster, Red 🔺 = slower.

workflow with no steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Nitro 0.039s (-3.2%) 1.006s (~) 0.967s 10 1.00x
💻 Local Express 0.043s (~) 1.006s (~) 0.963s 10 1.10x
🐘 Postgres Express 0.056s (-22.3% 🟢) 1.012s (~) 0.955s 10 1.44x
💻 Local Next.js (Turbopack) 0.061s (-8.0% 🟢) 1.006s (~) 0.946s 10 1.54x
🐘 Postgres Nitro 0.063s (+1.0%) 1.014s (~) 0.951s 10 1.61x
🐘 Postgres Next.js (Turbopack) 0.069s (-1.4%) 1.013s (~) 0.944s 10 1.76x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 0.376s (+29.0% 🔺) 2.269s (+20.0% 🔺) 1.893s 10 1.00x
▲ Vercel Express ⚠️ missing - - - -
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Next.js (Turbopack)

workflow with 1 step

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Nitro 1.088s (~) 2.006s (~) 0.918s 10 1.00x
💻 Local Express 1.094s (-0.7%) 2.006s (~) 0.912s 10 1.00x
🐘 Postgres Nitro 1.103s (-1.6%) 2.009s (~) 0.906s 10 1.01x
🐘 Postgres Express 1.103s (~) 2.010s (~) 0.907s 10 1.01x
💻 Local Next.js (Turbopack) 1.125s (-2.0%) 2.008s (~) 0.882s 10 1.03x
🐘 Postgres Next.js (Turbopack) 1.145s (~) 2.010s (~) 0.865s 10 1.05x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 1.982s (+12.3% 🔺) 4.139s (+11.5% 🔺) 2.157s 10 1.00x
▲ Vercel Express ⚠️ missing - - - -
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Next.js (Turbopack)

workflow with 10 sequential steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 10.483s (-0.6%) 11.012s (~) 0.529s 3 1.00x
💻 Local Nitro 10.491s (~) 11.021s (~) 0.530s 3 1.00x
💻 Local Express 10.514s (~) 11.021s (~) 0.507s 3 1.00x
🐘 Postgres Nitro 10.521s (~) 11.017s (~) 0.496s 3 1.00x
🐘 Postgres Next.js (Turbopack) 10.798s (-0.6%) 11.017s (~) 0.219s 3 1.03x
💻 Local Next.js (Turbopack) 10.813s (~) 11.022s (~) 0.209s 3 1.03x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 16.037s (+12.0% 🔺) 18.781s (+15.2% 🔺) 2.745s 2 1.00x
▲ Vercel Express ⚠️ missing - - - -
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Next.js (Turbopack)

workflow with 25 sequential steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 13.673s (-1.3%) 14.019s (~) 0.347s 5 1.00x
💻 Local Nitro 13.693s (~) 14.028s (~) 0.335s 5 1.00x
🐘 Postgres Nitro 13.704s (-1.0%) 14.017s (~) 0.314s 5 1.00x
💻 Local Express 13.709s (~) 14.027s (~) 0.318s 5 1.00x
💻 Local Next.js (Turbopack) 14.378s (~) 15.030s (~) 0.651s 4 1.05x
🐘 Postgres Next.js (Turbopack) 14.415s (~) 15.015s (~) 0.600s 4 1.05x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 24.453s (-31.0% 🟢) 26.539s (-27.9% 🟢) 2.085s 3 1.00x
▲ Vercel Express ⚠️ missing - - - -
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Next.js (Turbopack)

workflow with 50 sequential steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Nitro 12.261s (+1.6%) 13.025s (+1.0%) 0.764s 7 1.00x
🐘 Postgres Express 12.346s (-0.8%) 13.018s (~) 0.673s 7 1.01x
💻 Local Express 12.401s (-1.2%) 13.025s (~) 0.624s 7 1.01x
🐘 Postgres Nitro 12.460s (~) 13.019s (~) 0.559s 7 1.02x
💻 Local Next.js (Turbopack) 13.689s (~) 14.028s (~) 0.338s 7 1.12x
🐘 Postgres Next.js (Turbopack) 13.690s (-1.2%) 14.018s (-1.0%) 0.328s 7 1.12x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 32.489s (-7.7% 🟢) 34.887s (-5.5% 🟢) 2.398s 3 1.00x
▲ Vercel Express ⚠️ missing - - - -
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Next.js (Turbopack)

Promise.all with 10 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 1.181s (-2.6%) 2.007s (~) 0.826s 15 1.00x
🐘 Postgres Nitro 1.193s (-0.5%) 2.007s (~) 0.815s 15 1.01x
💻 Local Nitro 1.204s (+2.9%) 2.006s (~) 0.802s 15 1.02x
💻 Local Express 1.221s (+4.7%) 2.006s (~) 0.785s 15 1.03x
🐘 Postgres Next.js (Turbopack) 1.253s (-2.6%) 2.008s (~) 0.754s 15 1.06x
💻 Local Next.js (Turbopack) 1.432s (+10.5% 🔺) 2.008s (~) 0.576s 15 1.21x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 2.816s (+5.6% 🔺) 4.455s (+8.7% 🔺) 1.639s 7 1.00x
▲ Vercel Express ⚠️ missing - - - -
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Next.js (Turbopack)

Promise.all with 25 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 1.281s (-6.2% 🟢) 2.006s (-20.0% 🟢) 0.726s 15 1.00x
🐘 Postgres Express 1.305s (-8.4% 🟢) 2.007s (-16.2% 🟢) 0.702s 15 1.02x
🐘 Postgres Next.js (Turbopack) 1.440s (-12.1% 🟢) 2.008s (-16.1% 🟢) 0.568s 15 1.12x
💻 Local Express 1.995s (+22.7% 🔺) 2.508s (+25.0% 🔺) 0.512s 12 1.56x
💻 Local Nitro 2.038s (+31.3% 🔺) 2.392s (+19.0% 🔺) 0.354s 13 1.59x
💻 Local Next.js (Turbopack) 2.435s (+27.5% 🔺) 3.008s (+31.2% 🔺) 0.573s 10 1.90x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 2.741s (-39.0% 🟢) 4.840s (-25.3% 🟢) 2.099s 7 1.00x
▲ Vercel Express ⚠️ missing - - - -
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Next.js (Turbopack)

Promise.all with 50 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 1.427s (-10.5% 🟢) 3.886s (-3.1%) 2.459s 8 1.00x
🐘 Postgres Express 1.438s (-18.4% 🟢) 3.679s (-5.3% 🟢) 2.241s 9 1.01x
🐘 Postgres Next.js (Turbopack) 2.471s (-21.8% 🟢) 3.457s (-19.7% 🟢) 0.987s 9 1.73x
💻 Local Nitro 4.879s (+42.6% 🔺) 5.181s (+29.2% 🔺) 0.301s 6 3.42x
💻 Local Express 5.281s (+20.8% 🔺) 5.847s (+20.1% 🔺) 0.566s 6 3.70x
💻 Local Next.js (Turbopack) 7.159s (+24.0% 🔺) 7.519s (+21.0% 🔺) 0.361s 4 5.02x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 3.738s (-33.5% 🟢) 6.075s (-21.3% 🟢) 2.336s 5 1.00x
▲ Vercel Express ⚠️ missing - - - -
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Next.js (Turbopack)

Promise.race with 10 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 1.191s (-1.7%) 2.007s (~) 0.816s 15 1.00x
🐘 Postgres Express 1.193s (-2.0%) 2.007s (~) 0.814s 15 1.00x
💻 Local Nitro 1.224s (+2.0%) 2.006s (~) 0.782s 15 1.03x
💻 Local Express 1.226s (-22.2% 🟢) 2.007s (~) 0.780s 15 1.03x
🐘 Postgres Next.js (Turbopack) 1.254s (-2.1%) 2.007s (~) 0.754s 15 1.05x
💻 Local Next.js (Turbopack) 1.495s (+6.9% 🔺) 2.007s (~) 0.513s 15 1.26x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 2.416s (-35.1% 🟢) 4.362s (-16.6% 🟢) 1.946s 7 1.00x
▲ Vercel Express ⚠️ missing - - - -
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Next.js (Turbopack)

Promise.race with 25 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 1.279s (-11.4% 🟢) 2.007s (-16.2% 🟢) 0.728s 15 1.00x
🐘 Postgres Express 1.282s (-6.1% 🟢) 2.074s (-10.4% 🟢) 0.792s 15 1.00x
🐘 Postgres Next.js (Turbopack) 1.433s (-7.2% 🟢) 2.007s (-9.7% 🟢) 0.575s 15 1.12x
💻 Local Express 1.977s (+4.2%) 2.592s (+20.6% 🔺) 0.615s 12 1.55x
💻 Local Nitro 2.003s (+18.3% 🔺) 2.391s (+19.1% 🔺) 0.388s 13 1.57x
💻 Local Next.js (Turbopack) 2.392s (+10.3% 🔺) 3.009s (~) 0.617s 10 1.87x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 4.560s (+2.1%) 6.285s (+2.1%) 1.725s 5 1.00x
▲ Vercel Express ⚠️ missing - - - -
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Next.js (Turbopack)

Promise.race with 50 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 1.465s (-18.6% 🟢) 3.457s (-13.8% 🟢) 1.992s 9 1.00x
🐘 Postgres Nitro 1.497s (-17.0% 🟢) 3.565s (-17.0% 🟢) 2.068s 9 1.02x
🐘 Postgres Next.js (Turbopack) 2.312s (-43.2% 🟢) 3.009s (-32.3% 🟢) 0.697s 10 1.58x
💻 Local Nitro 5.570s (+24.0% 🔺) 6.214s (+24.0% 🔺) 0.644s 5 3.80x
💻 Local Express 5.906s (+28.7% 🔺) 6.214s (+23.9% 🔺) 0.308s 5 4.03x
💻 Local Next.js (Turbopack) 7.080s (+22.0% 🔺) 7.769s (+21.1% 🔺) 0.689s 4 4.83x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 4.303s (+13.8% 🔺) 5.947s (+13.6% 🔺) 1.643s 6 1.00x
▲ Vercel Express ⚠️ missing - - - -
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Next.js (Turbopack)

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 0.517s (-9.4% 🟢) 1.006s (~) 0.489s 60 1.00x
🐘 Postgres Nitro 0.541s (-9.0% 🟢) 1.006s (-3.3%) 0.466s 60 1.05x
💻 Local Express 0.564s (-9.8% 🟢) 1.005s (-1.6%) 0.441s 60 1.09x
💻 Local Nitro 0.590s (+18.0% 🔺) 1.022s (+1.4%) 0.432s 59 1.14x
🐘 Postgres Next.js (Turbopack) 0.776s (-7.1% 🟢) 1.007s (-1.6%) 0.231s 60 1.50x
💻 Local Next.js (Turbopack) 0.840s (-5.4% 🟢) 1.005s (-3.3%) 0.165s 60 1.63x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 4.432s (-29.8% 🟢) 6.395s (-21.5% 🟢) 1.963s 10 1.00x
▲ Vercel Express ⚠️ missing - - - -
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Next.js (Turbopack)

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 1.215s (-10.4% 🟢) 2.007s (-1.1%) 0.792s 45 1.00x
🐘 Postgres Nitro 1.281s (-7.8% 🟢) 2.008s (-1.1%) 0.727s 45 1.05x
💻 Local Nitro 1.391s (+17.1% 🔺) 2.006s (~) 0.615s 45 1.14x
💻 Local Express 1.436s (-3.5%) 2.006s (~) 0.570s 45 1.18x
🐘 Postgres Next.js (Turbopack) 1.870s (-3.7%) 2.124s (+2.3%) 0.254s 43 1.54x
💻 Local Next.js (Turbopack) 2.111s (~) 3.008s (~) 0.898s 30 1.74x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 9.205s (-32.3% 🟢) 11.053s (-27.9% 🟢) 1.848s 9 1.00x
▲ Vercel Express ⚠️ missing - - - -
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Next.js (Turbopack)

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 2.476s (-8.7% 🟢) 3.058s (-1.7%) 0.582s 40 1.00x
🐘 Postgres Nitro 2.578s (-7.9% 🟢) 3.059s (-3.4%) 0.481s 40 1.04x
💻 Local Nitro 3.067s (+12.3% 🔺) 3.736s (+15.9% 🔺) 0.669s 33 1.24x
💻 Local Express 3.105s (-2.9%) 3.821s (-4.7%) 0.716s 32 1.25x
🐘 Postgres Next.js (Turbopack) 3.691s (-4.6%) 4.009s (-1.6%) 0.318s 30 1.49x
💻 Local Next.js (Turbopack) 4.335s (~) 5.011s (~) 0.675s 24 1.75x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 21.505s (-14.0% 🟢) 24.045s (-10.2% 🟢) 2.540s 6 1.00x
▲ Vercel Express ⚠️ missing - - - -
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Next.js (Turbopack)

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 0.225s (-2.6%) 1.006s (~) 0.781s 60 1.00x
🐘 Postgres Nitro 0.237s (+0.7%) 1.006s (~) 0.769s 60 1.05x
🐘 Postgres Next.js (Turbopack) 0.267s (-9.4% 🟢) 1.006s (~) 0.740s 60 1.18x
💻 Local Nitro 0.415s (+11.2% 🔺) 1.005s (~) 0.590s 60 1.84x
💻 Local Express 0.425s (-3.4%) 1.005s (~) 0.580s 60 1.89x
💻 Local Next.js (Turbopack) 0.667s (+14.7% 🔺) 1.006s (~) 0.338s 60 2.96x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 1.861s (-0.6%) 3.581s (-7.9% 🟢) 1.720s 17 1.00x
▲ Vercel Express ⚠️ missing - - - -
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Next.js (Turbopack)

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 0.316s (-11.0% 🟢) 1.006s (-3.3%) 0.690s 90 1.00x
🐘 Postgres Nitro 0.320s (-6.3% 🟢) 1.006s (-1.1%) 0.686s 90 1.01x
🐘 Postgres Next.js (Turbopack) 0.452s (-16.1% 🟢) 1.018s (-9.0% 🟢) 0.566s 89 1.43x
💻 Local Express 2.171s (+3.9%) 2.715s (+2.2%) 0.544s 34 6.87x
💻 Local Nitro 2.189s (+43.6% 🔺) 2.738s (+27.2% 🔺) 0.549s 33 6.92x
💻 Local Next.js (Turbopack) 2.910s (+24.8% 🔺) 3.691s (+20.0% 🔺) 0.781s 25 9.20x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 3.046s (~) 4.771s (~) 1.725s 20 1.00x
▲ Vercel Express ⚠️ missing - - - -
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Next.js (Turbopack)

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 0.514s (-9.5% 🟢) 1.032s (-16.2% 🟢) 0.518s 117 1.00x
🐘 Postgres Express 0.515s (-9.5% 🟢) 1.128s (-7.3% 🟢) 0.613s 107 1.00x
🐘 Postgres Next.js (Turbopack) 1.623s (-38.0% 🟢) 2.481s (-28.5% 🟢) 0.858s 49 3.16x
💻 Local Express 10.054s (+21.0% 🔺) 10.695s (+20.4% 🔺) 0.641s 12 19.57x
💻 Local Nitro 10.136s (+60.6% 🔺) 10.780s (+59.8% 🔺) 0.643s 12 19.73x
💻 Local Next.js (Turbopack) 11.102s (+3.2%) 12.233s (+5.7% 🔺) 1.131s 10 21.61x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 5.403s (+11.6% 🔺) 7.668s (+17.4% 🔺) 2.265s 17 1.00x
▲ Vercel Express ⚠️ missing - - - -
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Next.js (Turbopack)

Stream Benchmarks (includes TTFB metrics)
workflow with stream

💻 Local Development

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 1.148s (-2.6%) 1.995s (~) 0.001s (~) 2.010s (~) 0.862s 10 1.00x
💻 Local Nitro 1.165s (+4.2%) 2.005s (~) 0.010s (-63.3% 🟢) 2.018s (-1.0%) 0.853s 10 1.01x
🐘 Postgres Nitro 1.165s (~) 1.998s (~) 0.001s (+30.0% 🔺) 2.010s (~) 0.845s 10 1.02x
💻 Local Express 1.166s (+1.4%) 2.005s (~) 0.013s (+24.8% 🔺) 2.020s (~) 0.854s 10 1.02x
💻 Local Next.js (Turbopack) 1.219s (+0.7%) 2.004s (~) 0.013s (+5.6% 🔺) 2.021s (~) 0.802s 10 1.06x
🐘 Postgres Next.js (Turbopack) 1.223s (-0.6%) 2.001s (~) 0.001s (~) 2.010s (~) 0.787s 10 1.07x

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 2.467s (-2.2%) 3.790s (-0.8%) 4.258s (+427.4% 🔺) 8.572s (+68.6% 🔺) 6.105s 10 1.00x
▲ Vercel Express ⚠️ missing - - - - -
▲ Vercel Nitro ⚠️ missing - - - - -

🔍 Observability: Next.js (Turbopack)

stream pipeline with 5 transform steps (1MB)

💻 Local Development

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 1.545s (-2.5%) 2.002s (~) 0.005s (+10.7% 🔺) 2.025s (~) 0.480s 30 1.00x
💻 Local Nitro 1.559s (+8.4% 🔺) 2.010s (~) 0.012s (-30.0% 🟢) 2.024s (~) 0.466s 30 1.01x
🐘 Postgres Nitro 1.566s (-0.8%) 2.008s (~) 0.005s (-8.0% 🟢) 2.026s (~) 0.461s 30 1.01x
💻 Local Express 1.578s (+0.7%) 2.011s (~) 0.012s (-7.2% 🟢) 2.025s (~) 0.447s 30 1.02x
🐘 Postgres Next.js (Turbopack) 1.753s (-1.8%) 2.012s (~) 0.005s (+1.3%) 2.028s (~) 0.274s 30 1.13x
💻 Local Next.js (Turbopack) 1.756s (~) 2.009s (~) 0.013s (+5.9% 🔺) 2.025s (~) 0.269s 30 1.14x

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 6.759s (-20.4% 🟢) 7.724s (-21.3% 🟢) 0.384s (+45.9% 🔺) 8.935s (-15.5% 🟢) 2.176s 7 1.00x
▲ Vercel Express ⚠️ missing - - - - -
▲ Vercel Nitro ⚠️ missing - - - - -

🔍 Observability: Next.js (Turbopack)

10 parallel streams (1MB each)

💻 Local Development

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 0.772s (+1.1%) 1.068s (+4.2%) 0.000s (-48.2% 🟢) 1.106s (+4.9%) 0.334s 55 1.00x
🐘 Postgres Nitro 0.788s (+2.0%) 1.066s (+1.7%) 0.000s (~) 1.098s (~) 0.310s 55 1.02x
🐘 Postgres Next.js (Turbopack) 0.943s (-7.5% 🟢) 1.305s (-10.9% 🟢) 0.000s (-10.9% 🟢) 1.312s (-10.9% 🟢) 0.368s 46 1.22x
💻 Local Express 1.516s (+11.3% 🔺) 2.014s (~) 0.000s (+83.3% 🔺) 2.017s (~) 0.501s 30 1.96x
💻 Local Nitro 1.533s (+42.5% 🔺) 2.014s (+9.6% 🔺) 0.000s (-56.8% 🟢) 2.016s (+9.6% 🔺) 0.483s 30 1.98x
💻 Local Next.js (Turbopack) 1.951s (+28.9% 🔺) 2.237s (+11.1% 🔺) 0.000s (+23.5% 🔺) 2.241s (+11.1% 🔺) 0.290s 27 2.53x

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 3.786s (+9.8% 🔺) 5.244s (+9.9% 🔺) 0.000s (+9.1% 🔺) 5.771s (+9.7% 🔺) 1.985s 11 1.00x
▲ Vercel Express ⚠️ missing - - - - -
▲ Vercel Nitro ⚠️ missing - - - - -

🔍 Observability: Next.js (Turbopack)

fan-out fan-in 10 streams (1MB each)

💻 Local Development

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 1.662s (+7.1% 🔺) 2.218s (+4.0%) 0.000s (+222.2% 🔺) 2.235s (+4.1%) 0.573s 27 1.00x
🐘 Postgres Express 1.768s (+9.5% 🔺) 2.256s (+9.3% 🔺) 0.000s (+Infinity% 🔺) 2.270s (+8.7% 🔺) 0.502s 27 1.06x
🐘 Postgres Next.js (Turbopack) 1.953s (-7.5% 🟢) 2.502s (-3.3%) 0.000s (+100.0% 🔺) 2.509s (-3.4%) 0.556s 24 1.17x
💻 Local Nitro 4.338s (+96.6% 🔺) 4.803s (+75.1% 🔺) 0.001s (+53.1% 🔺) 4.808s (+74.6% 🔺) 0.470s 13 2.61x
💻 Local Express 4.801s (+63.8% 🔺) 5.364s (+46.1% 🔺) 0.000s (-15.0% 🟢) 5.368s (+46.1% 🔺) 0.567s 12 2.89x
💻 Local Next.js (Turbopack) 5.934s (+104.6% 🔺) 6.700s (+99.5% 🔺) 0.001s (+11.1% 🔺) 6.704s (+99.3% 🔺) 0.770s 9 3.57x

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 6.006s (-17.1% 🟢) 7.394s (-11.9% 🟢) 0.000s (-100.0% 🟢) 7.899s (-10.7% 🟢) 1.893s 8 1.00x
▲ Vercel Express ⚠️ missing - - - - -
▲ Vercel Nitro ⚠️ missing - - - - -

🔍 Observability: Next.js (Turbopack)

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World 🥇 Fastest Framework Wins
💻 Local Nitro 15/21
🐘 Postgres Express 14/21
▲ Vercel Next.js (Turbopack) 21/21
Fastest World by Framework

Winner determined by most benchmark wins

Framework 🥇 Fastest World Wins
Express 🐘 Postgres 19/21
Next.js (Turbopack) 🐘 Postgres 16/21
Nitro 🐘 Postgres 14/21
Column Definitions
  • Workflow Time: Runtime reported by workflow (completedAt - createdAt) - primary metric
  • TTFB: Time to First Byte - time from workflow start until first stream byte received (stream benchmarks only)
  • Slurp: Time from first byte to complete stream consumption (stream benchmarks only)
  • Wall Time: Total testbench time (trigger workflow + poll for result)
  • Overhead: Testbench overhead (Wall Time - Workflow Time)
  • Samples: Number of benchmark iterations run
  • vs Fastest: How much slower compared to the fastest configuration for this benchmark

Worlds:

  • 💻 Local: In-memory filesystem world (local development)
  • 🐘 Postgres: PostgreSQL database world (local development)
  • ▲ Vercel: Vercel production/preview deployment
  • 🌐 Turso: Community world (local development)
  • 🌐 MongoDB: Community world (local development)
  • 🌐 Redis: Community world (local development)
  • 🌐 Jazz: Community world (local development)
  • 🌐 Redis: Community world (local development)
  • 🌐 Redis + BullMQ: Community world (local development)
  • 🌐 Cloudflare: Community world (local development)
  • 🌐 MySQL: Community world (local development)
  • 🌐 Azure: Community world (local development)
  • 🌐 NATS JetStream: Community world (local development)
  • 🌐 Upstash: Community world (local development)

📋 View full workflow run


Some benchmark jobs failed:

  • Local: success
  • Postgres: success
  • Vercel: failure

Check the workflow run for details.

@pranaygp pranaygp left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review — lazy step-bundle preload

Nice, well-scoped optimization. I traced the core correctness concern thoroughly and it holds up: the step/serde registrations moved into __workflow_steps.js are only ever consumed on the host step-execution path, and both executeStep call sites now await ensureStepBundleLoaded() first.

Why deferring the imports is safe (the part I most wanted to verify):

  • The only host consumers of the step registry (getStepFunction) and class registry (getSerializationClass) are reached via hydrateStepArguments/getStepRevivers/getClassRevivers — all inside executeStep, and getReadable() is itself a "use step". Nothing outside step execution touches them.
  • workflow/internal/builtins only defines "use step" functions, so it's safe to defer alongside user steps — builtin steps also dispatch through executeStep.
  • Crucially, the workflow-VM replay uses a separate, per-VM class registry (getRegistry(global) keyed on the VM global, populated by the compiled workflowVMCode), so VM replay never depended on the host route's top-level imports — before or after this PR. So replaying an event log that contains class-typed step results in a step-less invocation does not regress.

The three preload entry points (bg-step at the top, run_started, and the lazy fallback inside ensureStepBundleLoaded) all converge correctly, and ??= + the generated route's own memoization make double-loads harmless. The two new runtime tests (no-step preload rejection is swallowed; parallel preload awaited before inline step_started) are well-constructed and exercise the real microtask ordering.

Required before merge:

  • Missing changeset. This touches @workflow/core and @workflow/next; pnpm changeset status --since=main reports changeset needed. Per repo policy every PR needs one (patch for both packages here). (.changeset/lazy-next-step-imports.md is a different, already-merged PR.)

For reviewer awareness (intentional, not a bug): this is a behavior change in failure mode. Previously a broken step bundle failed the route module import → every invocation 500'd. Now a step-bundle load failure only surfaces on invocations that actually execute a step; pure-replay/suspend invocations succeed. The new test codifies this. Worth a line in the PR description.

CI: the failing Benchmark Vercel (express) / (nitro-v3) checks are bench.bench.ts timeouts against deployed apps on frameworks that use the unchanged base-builder and pass no preloadStepBundle (runtime change is a no-op for them) — unrelated/flaky. Unit Tests + nextjs-turbopack benchmark + all dev/postgres E2E pass.

Two minor inline notes below.

const workflowEntrypointOptionsCode =
createWorkflowEntrypointOptionsCode();
const workflowEntrypointOptionsObjectCode = workflowEntrypointOptionsCode
? workflowEntrypointOptionsCode.replace(/^,\s*/, '')

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor (maintainability): this couples the generated route to the exact , { ... } string shape returned by createWorkflowEntrypointOptionsCode() — stripping the leading comma so it can be spread into the new options object. It works today (the helper returns either '' or , { namespace: ... }), but if that helper's format ever changes (e.g. different spacing, or a non-leading-comma form) this silently emits broken route code with no type/test guard.

Consider either a short comment documenting the ^, dependency here, or having createWorkflowEntrypointOptionsCode() expose an object-literal variant (e.g. createWorkflowEntrypointOptionsObjectCode()) so both call sites share one source of truth. The : '{}' fallback + ...{} spread is harmless as-is.

.then(() => undefined);
// The preload is speculative. Surface failures only if this request
// actually reaches a step execution path and awaits the promise.
stepBundlePreload.catch(() => {});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirming intent (looks correct): ensureStepBundleLoaded() returns stepBundlePreload itself, so a genuine load failure still rejects and surfaces at the await before executeStep. This .catch(() => {}) is a separate subscription that only suppresses the unhandled-rejection warning for the speculative case where no step runs — good.

Nit: it re-attaches a fresh no-op handler on every startStepBundlePreload() call (each call after the first allocates another promise via .catch). Since the goal is one-time suppression, moving it inside the ??= initializer attaches it exactly once and reads more clearly:

stepBundlePreload ??= Promise.resolve()
  .then(() => options.preloadStepBundle?.())
  .then(() => undefined);
stepBundlePreload.catch(() => {}); // <- only when first created

Purely cosmetic; no behavioral change.

@pranaygp

Copy link
Copy Markdown
Contributor

Follow-up: correctness deep-dive + isolated cold-start numbers

Did a deeper correctness pass on the lazy-loading and measured the cold-start delta locally, since the CI benchmark numbers are end-to-end (noisy for cold start).

Correctness — loads steps exactly when needed ✅

The safety of the change rests on one invariant, which holds on every path I traced:

The host-side step/serde registry (everything __workflow_steps.js populates) is only ever consulted inside executeStep, and both executeStep call sites now await ensureStepBundleLoaded() first.

Every registry consumer is gated:

Consumer Reached via Gated?
getStepFunction(stepId) getStepRevivershydrateStepArguments inside executeStep
getSerializationClass(classId) (host) getClassRevivers via getStepRevivers / getExternalRevivers executeStep; getReadable() is itself "use step"
builtin steps (__builtin_response_*, __builtin_set_attributes) useStepexecuteStep

Workflow-VM replay is safe (the subtle part): the class registry is per-global (getRegistry(global) keyed on the VM global). The VM gets its class/step registrations from the compiled workflowVMCode, not from the host route's imports — so replaying an event log with a class-typed step result in a no-step invocation does not hit an unregistered reviver. Host imports were always only for the host step-execution path, before and after this PR. dehydrateRunError in the handler is likewise safe — dehydration reads classId off the live instance and never does a registry lookup (only revival does).

Edge cases verified: retry/throttle re-deliveries (carry incomingStepId → early preload), multiple sequential inline steps (??= → loaded once), the webhook deferred route (only calls resumeWebhook, never imports steps — correctly left untouched), and non-Next/eager-builder frameworks (pass no preloadStepBundle → runtime no-ops).

One intentional behavior change to call out: a broken step bundle previously failed the route module import (every invocation 500'd); now it only surfaces on step-executing invocations. More resilient, but worth a line in the PR description.

Cold start — improves, with nuance

Invocation type Effect
Wake-only (hook/wait/timer resume, no inline step) Saves the entire deferred graph load — biggest win
Background step delivery (Promise.all/parallel) Preload overlaps the world.runs.get round-trip → bundle load ~free
run_started → inline step (cold) Preload overlaps only the short initial replay; most cost still awaited before the step
Warm container reuse Module cached after first load → instant thereafter

Isolated measurement (nextjs-turbopack workbench, fresh Node process, n=6)

Route base (workflow/runtime, always loaded):     ~118 ms
  + @workflow/ai/agent (durable-agent + AI SDK):    +22 ms   ← deferred
  + React (8_react_render.tsx pulls it):            +9 ms    ← deferred
                                            deferred total ≈ 30 ms

~30 ms of cold module-init is moved off the no-step critical path (and overlapped on the step path) for this AI-heavy app. Caveats: conservative lower bound (compiled user-workflow modules add more), measured on a fast local CPU (cold serverless is typically 2–5× slower), and the saving scales with the app's step-dependency weight — heavier step deps → larger win; a trivial app with cheap steps → negligible.

Confirmed via the build output: the generated route no longer statically imports steps; the built route.js is 680 B and the 29 KB step chunk is not in its eager R.c(...) chunk list (loaded lazily on demand, as intended).

Local verification

  • @workflow/core typecheck passes (exit 0); pnpm build all 27 tasks pass — the noted local typecheck/build failures don't reproduce here.
  • runtime.test.ts preload tests ✅ · builder-deferred.test.ts ✅.

Outstanding

  • Missing changeset (changeset-bot flags it too) — needs patch for @workflow/core + @workflow/next.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants