') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); perf(core): build-time V8 code cache for workflow bundles by VaguelySerious · Pull Request #2523 · vercel/workflow · GitHub
Skip to content

perf(core): build-time V8 code cache for workflow bundles - #2523

Draft
VaguelySerious wants to merge 1 commit into
mainfrom
peter/workflow-vm-compile-cache
Draft

perf(core): build-time V8 code cache for workflow bundles#2523
VaguelySerious wants to merge 1 commit into
mainfrom
peter/workflow-vm-compile-cache

Conversation

@VaguelySerious

@VaguelySeriousVaguelySerious commented Jun 18, 2026

Copy link
Copy Markdown
Member

Status: draft / not mergeable as-is. The build-time win is real (7× faster cold parse) but the delivery mechanism in this PR — embedding the V8 code cache as base64 in the generated route — bloats the route and breaks some bundlers. See Why this is parked and the Comparison with #2524 below.

Summary

On a cold serverless instance, a workflow's first replay must parse the entire workflow bundle before it can reach the first step. The in-process vm.Script cache from #2471 only helps subsequent replays in the same process — the cold first replay still pays a full parse, which grows ~linearly with bundle size (~30ms/MB) and dominates time-to-first-step for large bundles.

This PR ships a build-time V8 code cache (Script.createCachedData()) so V8 can skip parsing on that first compile:

  • Producer (@workflow/builders, @workflow/next): for bundles over a size threshold, compile the bundle at build time and emit the cache as base64 in the generated flow route, handed to workflowEntrypoint via a cachedData option.
  • Consumer (@workflow/core): getCachedWorkflowScript compiles with cachedData; V8 validates it and falls back to a full parse on any mismatch (cachedDataRejected), so a stale blob is never a correctness risk.

Measured win (fresh-process parse, no in-process cache)

bundlecold new Scriptwith cachedDatasaved
2 MB33.9 ms4.85 ms~7× / −29 ms
8 MB147 ms20.7 ms~7× / −126 ms

This is genuinely different from #2471, which evaluated createCachedData() and saw "no benefit" — but only at ≤821KB and in-process, where V8's internal compile cache already hides re-parse. At realistic sizes, fresh-process, the win is large. cachedData is also filename-independent (verified), so one build-time blob is valid for every per-workflow filename the bundle is compiled under.

Why this is parked

The delivery mechanism — embedding the cache as base64 in the route source — is not viable. The blob is roughly bundle-sized, so it ~doubles an already-large route. Measured on the 155-workflow example workbench:

generated route size
without this change10.33 MB
with this change16.77 MB (+6.4 MB / +62%)

esbuild (standalone / Build Output API) tolerates 16 MB, but in CI:

  • nuxt build → FATAL ERROR: JavaScript heap out of memory (the bundler choking on the giant string literal);
  • Turbopack build → TurbopackInternalError: failed to receive message / unexpected end of file (worker IPC crash passing the oversized route).

No size threshold fixes this: the cache scales with the bundle, so it's largest exactly where the parse win matters most and where it can least be afforded. Embedding-in-source is the wrong transport.

Comparison with #2524

#2524 (precompile workflow vm.Script at module init) targets the same cold-TTFB problem on a different axis, and ships safely:

this PR (#2523)#2524
ApproachReduce the parse cost (persisted V8 code cache)Move when the parse happens — off the replay critical path
MechanismEmbed ~bundle-sized base64; runtime compiles with itInline a small workflowFilenames array; workflowEntrypoint precompiles the Script at module-init so the first replay is a cache hit
Route size impact+6.4 MB on the example (10.3 → 16.7 MB)a few hundred bytes
Build impactBreaks nuxt (OOM) + Turbopack (IPC crash)None
Effect on cold parseReduces it ~7×Doesn't reduce it; relocates it from the first replay to module-init

They are complementary, not duplicates. #2524 removes the parse from the replay window (and pays it once, regardless of replay count); this PR would make that parse itself cheap. The right composition is: land #2524, and if cold-start boot parse still matters afterward, reduce it with a code cache — but delivered file-based (a sibling .cache read at runtime via fs, surviving each framework's function file-tracing), never embedded in source.

The reusable, harmless part of this PR is the runtime consumer (getCachedWorkflowScript / runWorkflow / workflowEntrypoint accept optional cachedData, with cachedDataRejected fallback). The producer (embed) is the part to discard or rework.

Recommendation

Prefer #2524 for the immediate cold-TTFB win. This PR should either be closed (superseded by #2524 for the "earlier" win; embed delivery unviable) or repurposed into a follow-up that delivers cachedData via a file-based transport, stacked on top of #2524 — only if measurement after #2524 shows boot-time parse is still worth attacking.

🤖 Generated with Claude Code

On a cold serverless instance the first replay parses the entire workflow
bundle before it can reach the first step. The in-process vm.Script cache
(#2471) only helps subsequent replays in the same process; the cold first
one still pays a full parse, which grows ~30ms/MB and dominates
time-to-first-step for large bundles.
Producer (builders/next): for bundles over a size threshold, compile the
bundle at build time and emit Script.createCachedData() as base64 in the
generated flow route, passed via workflowEntrypoint's cachedData option.
Skipped for small bundles (parse is ~1ms; the blob would just bloat the
artifact) and via WORKFLOW_DISABLE_BUNDLE_CODE_CACHE.
Consumer (core): getCachedWorkflowScript compiles with the cachedData so
V8 skips parsing on the first compile. The blob is filename-independent so
one build-time cache serves every per-workflow filename; V8 validates it
and falls back to a full parse on any version/source mismatch, so a stale
blob is never a correctness risk.
Measured fresh-process parse: 2MB 34->4.8ms, 8MB 147->21ms (~7x).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@VaguelySerious
VaguelySerious requested review from a team and ijjk as code ownersJune 18, 2026 21:28
@changeset-bot

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: fd621b1

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 16 packages
NameType
@workflow/corePatch
@workflow/buildersPatch
@workflow/nextPatch
@workflow/cliPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
workflowPatch
@workflow/world-testingPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch
@workflow/nuxtPatch

Not sure what this means? Click here to learn what changesets are.

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

@vercel

vercelBot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
❌ ▲ Vercel Production144022301672
✅ 💻 Local Development190902192128
✅ 📦 Local Production163001941824
❌ 🐘 Local Postgres176812071976
✅ 🪟 Windows15200152
✅ 📋 Other88501791064
Total7784310298816

❌ Failed Tests

▲ Vercel Production (2 failed)

express (1 failed):

  • AbortController abortFromStepWorkflow: step abort cancels an in-flight sibling step

nextjs-turbopack (1 failed):

  • distributedAbortController - manual abort triggers signal | wrun_01KVEB4E6ATBY283AJSF3XS7TQ | 🔍 observability
🐘 Local Postgres (1 failed)

nextjs-turbopack-canary (1 failed):

  • fibonacciWorkflow - recursive workflow composition via start() | wrun_01KVEAT62CZ1F85GE0G6NE442N

Details by Category

❌ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro125027
✅ example125027
❌ express124127
✅ fastify125027
✅ hono125027
❌ nextjs-turbopack14813
✅ nextjs-webpack14903
✅ nitro125027
✅ nuxt125027
✅ sveltekit14408
✅ vite125027
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable127025
✅ express-stable127025
✅ fastify-stable127025
✅ hono-stable127025
✅ nextjs-turbopack-canary133019
✅ nextjs-turbopack-stable-lazy-discovery-disabled15200
✅ nextjs-turbopack-stable-lazy-discovery-enabled15200
✅ nextjs-webpack-canary133019
✅ nextjs-webpack-stable-lazy-discovery-disabled15200
✅ nextjs-webpack-stable-lazy-discovery-enabled15200
✅ nitro-stable127025
✅ nuxt-stable127025
✅ sveltekit-stable14606
✅ vite-stable127025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable127025
✅ express-stable127025
✅ fastify-stable127025
✅ hono-stable127025
✅ nextjs-turbopack-canary133019
✅ nextjs-turbopack-stable-lazy-discovery-disabled15200
✅ nextjs-webpack-canary133019
✅ nextjs-webpack-stable-lazy-discovery-disabled15200
✅ nextjs-webpack-stable-lazy-discovery-enabled15200
✅ nitro-stable127025
✅ sveltekit-stable14606
✅ vite-stable127025
❌ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable126026
✅ express-stable126026
✅ fastify-stable126026
✅ hono-stable126026
❌ nextjs-turbopack-canary131120
✅ nextjs-turbopack-stable-lazy-discovery-disabled15101
✅ nextjs-turbopack-stable-lazy-discovery-enabled15101
✅ nextjs-webpack-canary132020
✅ nextjs-webpack-stable-lazy-discovery-disabled15101
✅ nextjs-webpack-stable-lazy-discovery-enabled15101
✅ nitro-stable126026
✅ sveltekit-stable14507
✅ vite-stable126026
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack15200
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable127025
✅ e2e-local-dev-tanstack-start-127025
✅ e2e-local-postgres-nest-stable126026
✅ e2e-local-postgres-tanstack-start-126026
✅ e2e-local-prod-nest-stable127025
✅ e2e-local-prod-tanstack-start-127025
✅ e2e-vercel-prod-tanstack-start125027

📋 View full workflow run


Some E2E test jobs failed:

  • Vercel Prod: failure
  • Local Dev: success
  • Local Prod: failure
  • Local Postgres: failure
  • Windows: success

Check the workflow run for details.

@github-actions

github-actionsBot 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

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro0.039s (-3.2%)1.006s (~)0.967s101.00x
💻 LocalExpress0.042s (-3.0%)1.006s (~)0.964s101.07x
🐘 PostgresNext.js (Turbopack)0.051s (-27.3% 🟢)1.010s (~)0.959s101.30x
🐘 PostgresNitro0.057s (-9.2% 🟢)1.012s (~)0.955s101.45x
💻 LocalNext.js (Turbopack)0.059s (-10.6% 🟢)1.006s (~)0.947s101.50x
🐘 PostgresExpress0.064s (-12.4% 🟢)1.013s (~)0.949s101.62x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro0.339s (-9.5% 🟢)2.445s (+8.8% 🔺)2.105s101.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.087s (~)2.007s (~)0.920s101.00x
💻 LocalExpress1.100s (~)2.006s (~)0.907s101.01x
🐘 PostgresNitro1.107s (-1.2%)2.011s (~)0.905s101.02x
🐘 PostgresExpress1.110s (~)2.010s (~)0.900s101.02x
🐘 PostgresNext.js (Turbopack)1.111s (-2.7%)2.008s (~)0.897s101.02x
💻 LocalNext.js (Turbopack)1.124s (-2.0%)2.007s (~)0.882s101.03x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro1.613s (-30.4% 🟢)3.539s (-11.3% 🟢)1.925s101.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro10.485s (-0.5%)11.023s (~)0.538s31.00x
💻 LocalExpress10.522s (~)11.022s (~)0.500s31.00x
🐘 PostgresNitro10.538s (~)11.017s (~)0.480s31.01x
🐘 PostgresExpress10.578s (~)11.019s (~)0.440s31.01x
💻 LocalNext.js (Turbopack)10.744s (-0.5%)11.023s (~)0.279s31.02x
🐘 PostgresNext.js (Turbopack)10.783s (-0.7%)11.016s (~)0.233s31.03x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro13.217s (-30.3% 🟢)15.442s (-27.5% 🟢)2.225s21.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro13.691s (~)14.028s (~)0.337s51.00x
💻 LocalExpress13.718s (~)14.028s (~)0.310s51.00x
🐘 PostgresNitro13.753s (-0.6%)14.019s (~)0.266s51.00x
🐘 PostgresExpress13.956s (+0.8%)14.019s (~)0.064s51.02x
💻 LocalNext.js (Turbopack)14.278s (-0.6%)15.031s (~)0.753s41.04x
🐘 PostgresNext.js (Turbopack)14.697s (+1.8%)15.057s (~)0.360s41.07x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro21.080s (-32.3% 🟢)23.244s (-28.6% 🟢)2.164s31.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro12.220s (+1.2%)13.025s (+1.0%)0.806s71.00x
💻 LocalExpress12.441s (-0.8%)13.025s (~)0.583s71.02x
🐘 PostgresExpress12.636s (+1.5%)13.020s (~)0.385s71.03x
🐘 PostgresNitro12.686s (+1.4%)13.163s (+1.1%)0.477s71.04x
💻 LocalNext.js (Turbopack)13.528s (-0.9%)14.027s (~)0.499s71.11x
🐘 PostgresNext.js (Turbopack)13.921s (~)14.586s (+3.0%)0.665s71.14x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro32.286s (-17.1% 🟢)34.517s (-15.7% 🟢)2.231s31.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)1.191s (-7.4% 🟢)2.007s (~)0.815s151.00x
🐘 PostgresNitro1.198s (~)2.007s (~)0.810s151.01x
🐘 PostgresExpress1.206s (-0.6%)2.009s (~)0.803s151.01x
💻 LocalNitro1.206s (+3.2%)2.006s (~)0.799s151.01x
💻 LocalExpress1.238s (+6.3% 🔺)2.006s (~)0.768s151.04x
💻 LocalNext.js (Turbopack)1.412s (+8.9% 🔺)2.007s (~)0.595s151.19x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.808s (-3.5%)4.581s (+3.9%)1.773s71.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.294s (-5.1% 🟢)2.008s (-19.9% 🟢)0.714s151.00x
🐘 PostgresExpress1.305s (-8.4% 🟢)2.008s (-16.1% 🟢)0.702s151.01x
🐘 PostgresNext.js (Turbopack)1.347s (-17.7% 🟢)2.006s (-16.2% 🟢)0.659s151.04x
💻 LocalExpress2.060s (+26.6% 🔺)2.591s (+29.2% 🔺)0.531s121.59x
💻 LocalNitro2.079s (+34.0% 🔺)2.392s (+19.0% 🔺)0.313s131.61x
💻 LocalNext.js (Turbopack)2.305s (+20.7% 🔺)3.009s (+31.2% 🔺)0.704s101.78x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro5.183s (+28.0% 🔺)7.276s (+27.6% 🔺)2.093s51.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.513s (-5.1% 🟢)3.343s (-16.6% 🟢)1.830s91.00x
🐘 PostgresExpress1.628s (-7.7% 🟢)4.012s (+3.2%)2.384s81.08x
🐘 PostgresNext.js (Turbopack)1.853s (-41.3% 🟢)3.341s (-22.4% 🟢)1.488s91.22x
💻 LocalNitro5.095s (+48.9% 🔺)6.014s (+50.0% 🔺)0.919s53.37x
💻 LocalExpress5.715s (+30.7% 🔺)6.213s (+27.7% 🔺)0.499s53.78x
💻 LocalNext.js (Turbopack)6.673s (+15.6% 🔺)7.016s (+12.9% 🔺)0.343s54.41x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro6.464s (+5.5% 🔺)8.581s (+2.9%)2.117s41.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.209s (-0.8%)2.008s (~)0.800s151.00x
🐘 PostgresNitro1.218s (+0.5%)2.008s (~)0.789s151.01x
💻 LocalExpress1.233s (-21.8% 🟢)2.007s (~)0.774s151.02x
💻 LocalNitro1.252s (+4.2%)2.006s (~)0.755s151.04x
🐘 PostgresNext.js (Turbopack)1.512s (+18.1% 🔺)2.124s (+5.8% 🔺)0.612s151.25x
💻 LocalNext.js (Turbopack)1.521s (+8.8% 🔺)2.007s (~)0.486s151.26x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro4.034s (+13.1% 🔺)6.103s (+15.4% 🔺)2.069s51.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.287s (-5.8% 🟢)2.008s (-13.3% 🟢)0.721s151.00x
🐘 PostgresNitro1.299s (-10.0% 🟢)2.007s (-16.2% 🟢)0.709s151.01x
🐘 PostgresNext.js (Turbopack)1.624s (+5.2% 🔺)2.399s (+7.9% 🔺)0.775s131.26x
💻 LocalNitro1.953s (+15.3% 🔺)2.316s (+15.3% 🔺)0.363s131.52x
💻 LocalExpress2.006s (+5.7% 🔺)2.592s (+20.6% 🔺)0.585s121.56x
💻 LocalNext.js (Turbopack)2.428s (+12.0% 🔺)3.010s (~)0.582s101.89x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro4.111s (+46.9% 🔺)6.323s (+51.2% 🔺)2.212s51.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.485s (-17.5% 🟢)3.763s (-6.2% 🟢)2.278s81.00x
🐘 PostgresNitro1.515s (-16.1% 🟢)3.678s (-14.4% 🟢)2.163s91.02x
🐘 PostgresNext.js (Turbopack)1.935s (-52.4% 🟢)2.932s (-34.0% 🟢)0.996s111.30x
💻 LocalNitro5.373s (+19.6% 🔺)6.013s (+20.0% 🔺)0.640s53.62x
💻 LocalExpress5.517s (+20.2% 🔺)6.017s (+20.0% 🔺)0.500s53.72x
💻 LocalNext.js (Turbopack)6.776s (+16.7% 🔺)7.218s (+12.5% 🔺)0.442s54.56x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro4.669s (-66.3% 🟢)6.728s (-57.0% 🟢)2.059s51.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro0.551s (+10.1% 🔺)1.005s (~)0.454s601.00x
🐘 PostgresNitro0.553s (-6.9% 🟢)1.006s (-3.3%)0.453s601.00x
🐘 PostgresExpress0.584s (+2.3%)1.024s (+1.8%)0.440s591.06x
💻 LocalExpress0.626s (~)1.022s (~)0.397s591.14x
🐘 PostgresNext.js (Turbopack)0.665s (-20.4% 🟢)1.023s (~)0.357s591.21x
💻 LocalNext.js (Turbopack)0.822s (-7.4% 🟢)1.005s (-3.3%)0.183s601.49x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro4.317s (-34.7% 🟢)6.078s (-27.1% 🟢)1.761s101.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.379s (+1.6%)2.030s (~)0.651s451.00x
💻 LocalNitro1.414s (+19.1% 🔺)2.006s (~)0.591s451.03x
🐘 PostgresNitro1.433s (+3.2%)2.053s (+1.1%)0.619s441.04x
💻 LocalExpress1.485s (~)2.006s (~)0.521s451.08x
🐘 PostgresNext.js (Turbopack)1.557s (-19.8% 🟢)2.057s (-0.9%)0.500s441.13x
💻 LocalNext.js (Turbopack)2.013s (-4.2%)2.428s (-19.3% 🟢)0.415s381.46x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro10.729s (-9.2% 🟢)13.175s (-7.0% 🟢)2.446s71.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro2.630s (-6.0% 🟢)3.033s (-4.2%)0.403s401.00x
🐘 PostgresExpress2.928s (+8.0% 🔺)3.280s (+5.4% 🔺)0.352s371.11x
💻 LocalNitro3.024s (+10.7% 🔺)3.465s (+7.5% 🔺)0.442s351.15x
💻 LocalExpress3.161s (-1.2%)3.945s (-1.6%)0.784s311.20x
🐘 PostgresNext.js (Turbopack)3.772s (-2.5%)4.308s (+5.7% 🔺)0.536s281.43x
💻 LocalNext.js (Turbopack)4.271s (-1.7%)5.010s (~)0.739s241.62x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro20.720s (-21.2% 🟢)23.022s (-18.1% 🟢)2.302s61.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)0.207s (-29.5% 🟢)1.006s (~)0.798s601.00x
🐘 PostgresNitro0.234s (-0.7%)1.006s (~)0.772s601.13x
🐘 PostgresExpress0.235s (+1.5%)1.006s (~)0.772s601.13x
💻 LocalNitro0.401s (+7.6% 🔺)1.004s (~)0.603s601.93x
💻 LocalExpress0.412s (-6.3% 🟢)1.005s (~)0.593s601.99x
💻 LocalNext.js (Turbopack)0.734s (+26.0% 🔺)1.058s (+5.3% 🔺)0.325s573.54x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.572s (+99.3% 🔺)4.697s (+63.5% 🔺)2.125s131.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.348s (-2.0%)1.018s (-2.2%)0.669s891.00x
🐘 PostgresNitro0.354s (+3.6%)1.006s (-1.1%)0.652s901.02x
🐘 PostgresNext.js (Turbopack)0.417s (-22.6% 🟢)1.098s (-1.8%)0.682s821.20x
💻 LocalNitro2.056s (+34.9% 🔺)2.637s (+22.5% 🔺)0.581s355.90x
💻 LocalExpress2.085s (~)2.581s (-2.8%)0.496s355.99x
💻 LocalNext.js (Turbopack)2.976s (+27.7% 🔺)3.733s (+21.3% 🔺)0.757s258.54x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.067s (+11.7% 🔺)5.202s (+8.8% 🔺)2.135s181.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.540s (-4.9%)1.059s (-14.0% 🟢)0.519s1141.00x
🐘 PostgresExpress0.579s (+1.8%)1.098s (-9.7% 🟢)0.519s1101.07x
🐘 PostgresNext.js (Turbopack)1.293s (-50.6% 🟢)2.333s (-32.8% 🟢)1.039s522.39x
💻 LocalNitro9.844s (+56.0% 🔺)10.531s (+56.2% 🔺)0.687s1218.23x
💻 LocalExpress10.062s (+21.1% 🔺)10.938s (+23.2% 🔺)0.877s1118.63x
💻 LocalNext.js (Turbopack)10.951s (+1.8%)12.127s (+4.8%)1.176s1020.28x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro4.035s (-23.5% 🟢)6.119s (-12.8% 🟢)2.083s201.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.161s (+3.9%)2.005s (~)0.010s (-62.6% 🟢)2.018s (-1.0%)0.857s101.00x
💻 LocalExpress1.168s (+1.6%)2.005s (~)0.012s (+21.8% 🔺)2.020s (~)0.852s101.01x
🐘 PostgresNitro1.169s (~)1.998s (~)0.001s (+40.0% 🔺)2.011s (~)0.842s101.01x
🐘 PostgresExpress1.174s (~)1.997s (~)0.001s (~)2.012s (~)0.838s101.01x
🐘 PostgresNext.js (Turbopack)1.198s (-2.6%)2.001s (~)0.001s (-46.2% 🟢)2.009s (~)0.811s101.03x
💻 LocalNext.js (Turbopack)1.209s (~)2.004s (~)0.012s (-0.8%)2.020s (~)0.812s101.04x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.698s (+2.2%)4.334s (+13.8% 🔺)3.686s (+364.7% 🔺)8.513s (+68.4% 🔺)5.814s101.00x
▲ VercelExpress⚠️missing-----
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.579s (+9.8% 🔺)2.043s (+1.6%)0.012s (-32.6% 🟢)2.057s (+1.3%)0.478s301.00x
💻 LocalExpress1.586s (+1.2%)2.011s (~)0.013s (+3.1%)2.026s (~)0.440s301.00x
🐘 PostgresNitro1.610s (+2.0%)2.007s (~)0.005s (-5.6% 🟢)2.027s (~)0.417s301.02x
🐘 PostgresExpress1.690s (+6.7% 🔺)2.003s (~)0.005s (+9.3% 🔺)2.028s (~)0.338s301.07x
🐘 PostgresNext.js (Turbopack)1.736s (-2.8%)2.036s (+1.3%)0.004s (-16.7% 🟢)2.072s (+2.2%)0.336s291.10x
💻 LocalNext.js (Turbopack)1.741s (-0.5%)2.010s (~)0.012s (~)2.025s (~)0.284s301.10x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro7.457s (+4.1%)8.913s (+2.9%)0.342s (+44.3% 🔺)9.793s (+4.0%)2.336s71.00x
▲ VercelExpress⚠️missing-----
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.769s (-0.5%)1.067s (+1.8%)0.000s (+194.6% 🔺)1.079s (-1.3%)0.310s561.00x
🐘 PostgresExpress0.815s (+6.7% 🔺)1.067s (+4.1%)0.000s (+3.6%)1.095s (+3.8%)0.280s551.06x
🐘 PostgresNext.js (Turbopack)0.892s (-12.5% 🟢)1.230s (-16.0% 🟢)0.000s (-58.2% 🟢)1.261s (-14.3% 🟢)0.369s491.16x
💻 LocalExpress1.444s (+6.0% 🔺)2.014s (~)0.000s (+100.0% 🔺)2.016s (~)0.572s301.88x
💻 LocalNitro1.488s (+38.4% 🔺)2.013s (+9.6% 🔺)0.000s (-68.6% 🟢)2.016s (+9.5% 🔺)0.528s301.93x
💻 LocalNext.js (Turbopack)1.927s (+27.4% 🔺)2.273s (+12.9% 🔺)0.000s (-87.7% 🟢)2.277s (+12.9% 🔺)0.350s272.51x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro5.465s (+45.5% 🔺)6.954s (+29.2% 🔺)0.000s (-100.0% 🟢)7.446s (+27.1% 🔺)1.981s91.00x
▲ VercelExpress⚠️missing-----
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro

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

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)1.760s (-16.6% 🟢)2.441s (-5.6% 🟢)0.000s (-100.0% 🟢)2.455s (-5.4% 🟢)0.695s251.00x
🐘 PostgresExpress1.773s (+9.8% 🔺)2.303s (+11.6% 🔺)0.000s (+Infinity% 🔺)2.331s (+11.6% 🔺)0.559s261.01x
🐘 PostgresNitro1.821s (+17.4% 🔺)2.438s (+14.3% 🔺)0.000s (+16.0% 🔺)2.449s (+14.1% 🔺)0.628s251.03x
💻 LocalNitro3.729s (+69.0% 🔺)4.380s (+59.7% 🔺)0.001s (+34.7% 🔺)4.389s (+59.4% 🔺)0.660s142.12x
💻 LocalExpress4.179s (+42.6% 🔺)4.791s (+30.5% 🔺)0.000s (-47.7% 🟢)4.803s (+30.8% 🔺)0.625s132.37x
💻 LocalNext.js (Turbopack)5.041s (+73.8% 🔺)5.573s (+66.0% 🔺)0.001s (+118.2% 🔺)5.585s (+66.1% 🔺)0.544s112.86x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro9.023s (+71.9% 🔺)10.556s (+63.5% 🔺)0.000s (+Infinity% 🔺)11.005s (+58.9% 🔺)1.982s61.00x
▲ VercelExpress⚠️missing-----
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNitro18/21
🐘 PostgresNitro11/21
▲ VercelNitro21/21
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express🐘 Postgres14/21
Next.js (Turbopack)🐘 Postgres18/21
Nitro🐘 Postgres12/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.

@VaguelySerious
VaguelySerious marked this pull request as draft June 18, 2026 21:51

@pranaygppranaygp 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.

Reviewed the producer (@workflow/builders / @workflow/next) and consumer (@workflow/core) sides in full. The design is sound: cachedData is filename-independent, V8 validates the blob and falls back to a full parse on any mismatch, so there's no correctness risk — only a missed optimization. Threading it through workflowEntrypoint → runWorkflow → getCachedWorkflowScript is clean and backward-compatible, and the tests cover the producer/consumer contract. Notes are inline; the two I'd act on before merging:

1. Build perf — the cache is generated twice per build. See the bundleFinal comment. In both the combined and Next-deferred paths, createWorkflowsBundle generates a code cache into a temp file that's immediately deleted, then the caller regenerates it. Large bundles pay createCachedData() twice.

2. The code-cache path is likely unexercised end-to-end. The 256KB threshold means the workbench apps almost certainly fall below it, so neither the E2E suites nor the green preview deployments actually emit or consume a cache. That leaves the integration unverified at runtime:

  • The astro/sveltekit route post-processing regex workflowEntrypoint\(workflowCode(?<options>[^)]*)\) now has to capture the new , { cachedData: __workflowCodeCachedData } option and rely on the separate const __workflowCodeCachedData = ... line surviving the rewrite. By my reading it does (the .replace only rewrites the POST line, leaving the decl above it), but it's verified only by reasoning, not by an exercised route.
  • Each framework bundler (Next turbopack/webpack, vite, rollup) has to re-embed the multi-MB base64 literal intact for V8 to accept it at runtime.

I'd want at least one >256KB-bundle deploy exercised per route-generation path (combined, Next-deferred, astro/sveltekit regex) before relying on this — which also yields the real Vercel cold-start number the PR description asks for.

3. Artifact-size tradeoff (already flagged in the description). Worth confirming the doubled function artifact doesn't eat the parse win on a real cold start and stays under function size limits for the largest realistic bundles. A size cap on the emitted cache (skip if base64 > N) would be a cheap safety valve.

Unrelated to this PR: the failing E2E tests (abortFromStepWorkflow, distributedAbortController, recursive fibonacciWorkflow) are known flakes that also fail on main.

const bundleFinal = async (interimBundle: string) => {
const workflowBundleCode = interimBundle;
const { cachedDataDecl, secondArg } =
createWorkflowEntrypointArgs(workflowBundleCode);

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.

createWorkflowEntrypointArgs(workflowBundleCode) runs unconditionally here — before the if (!bundleFinalOutput) check below — but every current caller of createWorkflowsBundle passes bundleFinalOutput: false (createCombinedBundle at L1353 and the Next deferred builder at builder-deferred.ts:619). In that mode this bundleFinal writes workflowFunctionCode to a .__wf_tmp.js file that the caller deletes immediately (L1370 / builder-deferred.ts:636) and then regenerates its own route + cache from the same bundle (L1379 / builder-deferred.ts:691).

So for a >256KB bundle, V8 new Script() + createCachedData() runs twice per build and one result is thrown away (~300-400ms of avoidable build cost on an 8MB bundle). Consider gating the cache generation on bundleFinalOutput — the throwaway intermediate route never needs a code cache.

script = cachedData
? new Script(code, { filename, cachedData })
: new Script(code, { filename });
if (cachedData && script.cachedDataRejected) {

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.

Observability gap: a rejected blob degrades silently, which is correct, but a silent cachedDataRejected is also a silent perf loss in prod — e.g. a Node minor bump on the runtime invalidates every build-time cache and nobody notices the cold-start regression. runWorkflow already opens a span with attributes; consider surfacing cache hit/reject as a span attribute there (or a counter) so this is detectable in production, not only via debug logs.

let cachedDataB64 = '';
if (
process.env.WORKFLOW_DISABLE_BUNDLE_CODE_CACHE !== '1' &&
workflowCode.length >= MIN_CODE_CACHE_BYTES

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: MIN_CODE_CACHE_BYTES is a byte count, but it's compared against workflowCode.length, which is UTF-16 code units, not bytes. For ASCII-ish JS bundles these are ~equal so the heuristic holds, but the name implies bytes — either rename (e.g. MIN_CODE_CACHE_CHARS) or compare Buffer.byteLength(workflowCode).

@karthikscale3

Copy link
Copy Markdown
Contributor

AI Review:

No blocking correctness findings from me. The implementation is narrow and plausibly correctness-safe: build emits cached data for large embedded routes, runtime decodes it once, and V8 rejection falls back to normal parsing. I would leave one non-blocking ask: add an integration-style generated-route test for a large bundle, not just the helper/consumer tests, so the BaseBuilder/Next/Astro/SvelteKit route glue stays covered.

Focused checks I ran:

  • constants.test.ts
  • script-cache.test.ts
  • builder-deferred.test.ts
  • git diff --check

The first core test run needed local package builds in the temp worktree; after that it passed. Node here is v25.8.1, outside the repo declared engine range, so I treated the engine warning as environmental.

Main tradeoff:
This optimizes one very specific cost: first vm.Script(workflowCode) parse in a fresh process. It does not optimize warm replays, because the existing in-process script cache already handles those.

What we gain:

  • Faster time-to-first-step for large workflow bundles on cold instances.
  • Correctness-safe fallback if cached data is corrupt, stale, or built with incompatible V8.
  • No effect for small bundles due to the 256 KB threshold.

What we pay:

  • Bigger generated route source, potentially much bigger. The route now carries both the workflow source string and a base64 V8 cache blob.
  • More cold-start module load/scan work for the generated route itself. The cache skips parsing the workflow inside vm.Script; it does not make the serverless function route module smaller.
  • More memory: workflow source string, base64 string, decoded Buffer, then cached compiled script.
  • Build-time CPU to compile the workflow bundle and create cached data.
  • If build Node/V8 and runtime Node/V8 differ, V8 rejects the blob, so you pay artifact/memory/decode costs without the parse win.
  • The opt-out is build-time only: WORKFLOW_DISABLE_BUNDLE_CODE_CACHE=1.

My take: the mechanism is sound, but default-on at 256 KB is the product decision. I would want real Vercel cold-start numbers that include function artifact load/module init, not just isolated new Script, before leaning on this by default. Without that, I would prefer either a size cap, opt-in/experimental default, or at least build output/telemetry showing source size, cache size, and rejection rate.

Sign up for freeto 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.

3 participants

@VaguelySerious@karthikscale3@pranaygp