feat(swc): dead code eliminate unreferenced private class members in workflow mode - #1671

Merged
TooTallNate merged 3 commits into
mainfrom
fix/swc-private-member-dce
Apr 9, 2026
Merged

feat(swc): dead code eliminate unreferenced private class members in workflow mode#1671
TooTallNate merged 3 commits into
mainfrom
fix/swc-private-member-dce

Conversation

@TooTallNate

@TooTallNateTooTallNate commented Apr 9, 2026

Copy link
Copy Markdown
Member

Summary

After stripping "use step" methods from a class body in workflow mode, eliminate private members that are no longer referenced by any remaining public member. This applies to both:

  • JS native private: #field, #method()
  • TypeScript private: private field, private method()

The algorithm is iterative — references are seeded from public members, then expanded through surviving private members until a fixed point, enabling cascading elimination.

Example

Input:

exportclassRun{static[WORKFLOW_SERIALIZE](instance){return{id: instance.id};}static[WORKFLOW_DESERIALIZE](data){returnnewRun(data.id);}id: string;privateencryptionKeyPromise: Promise<any>|null=null;privateasyncgetEncryptionKey(){if(!this.encryptionKeyPromise){this.encryptionKeyPromise=importKey(this.id);}returnthis.encryptionKeyPromise;}constructor(id: string){this.id=id;}getvalue(): Promise<any>{'use step';returnthis.getEncryptionKey().then(()=>getWorld().get(this.id));}asynccancel(): Promise<void>{'use step';constkey=awaitthis.getEncryptionKey();awaitgetWorld().cancel(this.id,key);}toString(): string{return`Run(${this.id})`;}}

Workflow mode output:

exportclassRun{static[WORKFLOW_SERIALIZE](instance){return{id: instance.id};}static[WORKFLOW_DESERIALIZE](data){returnnewRun(data.id);}id;// ✅ private encryptionKeyPromise — ELIMINATED (only referenced by getEncryptionKey)// ✅ private getEncryptionKey() — ELIMINATED (only referenced by stripped step methods)constructor(id){this.id=id;}toString(){return`Run(${this.id})`;}}Run.prototype["cancel"]=globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//...");var__step_Run$value=globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//...");Object.defineProperty(Run.prototype,"value",{get(){return__step_Run$value.call(this);},configurable: true,enumerable: false});

The cascading elimination is key: encryptionKeyPromise is only referenced by getEncryptionKey(), which is itself only referenced by the stripped value getter and cancel() method. Both private members are removed, allowing the downstream module-level DCE to also eliminate the importKey and getWorld imports.

Motivation

SDK classes like Run have private helper methods that reference Node.js-only imports (encryption, world access). Without this optimization, those helpers survive into the workflow bundle even though nothing calls them after "use step" bodies are stripped, keeping the Node.js imports alive and preventing tree-shaking.

Test

New fixture: private-member-dce/input.ts with expected outputs for all three modes.

…workflow mode
After stripping 'use step' methods from a class body in workflow mode,
eliminate private members (both JS native #field/#method and TypeScript
private field/private method) that are no longer referenced by any
remaining public member.
The algorithm is iterative: references are seeded from public members,
then expanded through surviving private members until a fixed point,
enabling cascading elimination (e.g. a private field only referenced by
a private method that is itself unreferenced).
CopilotAI review requested due to automatic review settings April 9, 2026 18:09
@TooTallNate
TooTallNate requested a review from a team as a code ownerApril 9, 2026 18:09
@changeset-bot

changeset-botBot commented Apr 9, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 4ea576a

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

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

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 Apr 9, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Apr 9, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production912067979
✅ 💻 Local Development88601821068
✅ 📦 Local Production88601821068
✅ 🐘 Local Postgres88601821068
✅ 🪟 Windows810889
❌ 🌍 Community Worlds1366524225
✅ 📋 Other225042267
Total4012656874764

❌ Failed Tests

🌍 Community Worlds (65 failed)

mongodb (4 failed):

  • hookWorkflow is not resumable via public webhook endpoint | wrun_01KNSR613X7J6NJ3Q7CC1GAXC8
  • webhookWorkflow | wrun_01KNSR6A2N8E4FRA7K4W0WAJ2X
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously | wrun_01KNSRE4P2HFBR1HGDZ60FSZ4A
  • resilient start: addTenWorkflow completes when run_created returns 500 | wrun_01KNSRMTFPHDMEWW1M0C99YMJB

redis (3 failed):

  • hookWorkflow is not resumable via public webhook endpoint | wrun_01KNSR613X7J6NJ3Q7CC1GAXC8
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously | wrun_01KNSRE4P2HFBR1HGDZ60FSZ4A
  • resilient start: addTenWorkflow completes when run_created returns 500 | wrun_01KNSRMTFPHDMEWW1M0C99YMJB

turso (58 failed):

  • addTenWorkflow | wrun_01KNSR4Q12NYS08Q5M8JMBJE2P
  • addTenWorkflow | wrun_01KNSR4Q12NYS08Q5M8JMBJE2P
  • wellKnownAgentWorkflow (.well-known/agent) | wrun_01KNSR6H7RBB0VN5AF50A8K3HK
  • should work with react rendering in step
  • promiseAllWorkflow | wrun_01KNSR50EQH830PRD40KS32Q9Y
  • promiseRaceWorkflow | wrun_01KNSR54Y75RYQDBMAWK470VYP
  • promiseAnyWorkflow | wrun_01KNSR58C03DCWT8KGXWBMNCP6
  • importedStepOnlyWorkflow | wrun_01KNSR6WGHHN8XTHMJ86Q1JS10
  • hookWorkflow | wrun_01KNSR5M9AWG051SF57JDHTN15
  • hookWorkflow is not resumable via public webhook endpoint | wrun_01KNSR613X7J6NJ3Q7CC1GAXC8
  • webhookWorkflow | wrun_01KNSR6A2N8E4FRA7K4W0WAJ2X
  • sleepingWorkflow | wrun_01KNSR6G7XQP06VPNVA5S5SZ7P
  • parallelSleepWorkflow | wrun_01KNSR6W68GVQAECF6EXH5YKW4
  • nullByteWorkflow | wrun_01KNSR6ZZHZA6GX9A995NWGH4W
  • workflowAndStepMetadataWorkflow | wrun_01KNSR722J4AE22DPWYBGR40AA
  • fetchWorkflow | wrun_01KNSR9QTD0RQEWYMKGRM9P6KS
  • promiseRaceStressTestWorkflow | wrun_01KNSR9V7MWTQB99MYWGDBMPV1
  • error handling error propagation workflow errors nested function calls preserve message and stack trace
  • error handling error propagation workflow errors cross-file imports preserve message and stack trace
  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior FatalError fails immediately without retries
  • error handling retry behavior RetryableError respects custom retryAfter delay
  • error handling retry behavior maxRetries=0 disables retries
  • error handling catchability FatalError can be caught and detected with FatalError.is()
  • error handling not registered WorkflowNotRegisteredError fails the run when workflow does not exist
  • error handling not registered StepNotRegisteredError fails the step but workflow can catch it
  • error handling not registered StepNotRegisteredError fails the run when not caught in workflow
  • hookCleanupTestWorkflow - hook token reuse after workflow completion | wrun_01KNSRDFYWY5RDPW5MJW4M2RKP
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously | wrun_01KNSRE4P2HFBR1HGDZ60FSZ4A
  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running | wrun_01KNSREVJW6G5G63ZYP9DTG2CT
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars) | wrun_01KNSRFFZ3HX99QC8VH9X00X19
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument | wrun_01KNSRFS1Y9106Z5PVYGW5SZ8F
  • closureVariableWorkflow - nested step functions with closure variables | wrun_01KNSRFYM8QZ6MKBWEXNSXEA1E
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step | wrun_01KNSRG1102E8CS370B44Y7JJ1
  • health check (queue-based) - workflow and step endpoints respond to health check messages
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly | wrun_01KNSRGH373GH51TWFMF3M6THG
  • Calculator.calculate - static workflow method using static step methods from another class | wrun_01KNSRGPSM4VSQ1R3Z11NG4MZ0
  • AllInOneService.processNumber - static workflow method using sibling static step methods | wrun_01KNSRGXK317X0HN019FNHP541
  • ChainableService.processWithThis - static step methods using this to reference the class | wrun_01KNSRH5K54HKGJC6TW5YDC6P4
  • thisSerializationWorkflow - step function invoked with .call() and .apply() | wrun_01KNSRHCE7T09PPS45FA7D75MK
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE | wrun_01KNSRHM1A0G2BFRKSN1893XHR
  • instanceMethodStepWorkflow - instance methods with "use step" directive | wrun_01KNSRHTZQJP99YC0M9FNWQPM5
  • crossContextSerdeWorkflow - classes defined in step code are deserializable in workflow context | wrun_01KNSRJ68YGFBGXJ7YFR7ZHMV5
  • stepFunctionAsStartArgWorkflow - step function reference passed as start() argument | wrun_01KNSRJFBR9XRH3012W64TDAJ1
  • cancelRun - cancelling a running workflow | wrun_01KNSRJP4YQBW4BDB9ZFHE3JXZ
  • cancelRun via CLI - cancelling a running workflow | wrun_01KNSRK06K5725SZWQJ2DJZZ1K
  • pages router addTenWorkflow via pages router
  • pages router promiseAllWorkflow via pages router
  • pages router sleepingWorkflow via pages router
  • hookWithSleepWorkflow - hook payloads delivered correctly with concurrent sleep | wrun_01KNSRKDA53YRF17ST7YTD02BW
  • sleepInLoopWorkflow - sleep inside loop with steps actually delays each iteration | wrun_01KNSRM3T179X2E69VG894KAX9
  • sleepWithSequentialStepsWorkflow - sequential steps work with concurrent sleep (control) | wrun_01KNSRME2NVXNSBMBR8AND2DKJ
  • importMetaUrlWorkflow - import.meta.url is available in step bundles | wrun_01KNSRMP16CE789X5BG39DBJEA
  • metadataFromHelperWorkflow - getWorkflowMetadata/getStepMetadata work from module-level helper (#1577) | wrun_01KNSRMR5GRYXW87SN5A0AM9TV
  • resilient start: addTenWorkflow completes when run_created returns 500 | wrun_01KNSRMTFPHDMEWW1M0C99YMJB
  • getterStepWorkflow - getter functions with "use step" directive | wrun_01KNSRMXQTQ6GKDFR5NN8DMJRA

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro8207
✅ example8207
✅ express8207
✅ fastify8207
✅ hono8207
✅ nextjs-turbopack8702
✅ nextjs-webpack8702
✅ nitro8207
✅ nuxt8207
✅ sveltekit8207
✅ vite8207
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable75014
✅ express-stable75014
✅ fastify-stable75014
✅ hono-stable75014
✅ nextjs-turbopack-canary62027
✅ nextjs-turbopack-stable8108
✅ nextjs-webpack-canary62027
✅ nextjs-webpack-stable8108
✅ nitro-stable75014
✅ nuxt-stable75014
✅ sveltekit-stable75014
✅ vite-stable75014
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable75014
✅ express-stable75014
✅ fastify-stable75014
✅ hono-stable75014
✅ nextjs-turbopack-canary62027
✅ nextjs-turbopack-stable8108
✅ nextjs-webpack-canary62027
✅ nextjs-webpack-stable8108
✅ nitro-stable75014
✅ nuxt-stable75014
✅ sveltekit-stable75014
✅ vite-stable75014
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable75014
✅ express-stable75014
✅ fastify-stable75014
✅ hono-stable75014
✅ nextjs-turbopack-canary62027
✅ nextjs-turbopack-stable8108
✅ nextjs-webpack-canary62027
✅ nextjs-webpack-stable8108
✅ nitro-stable75014
✅ nuxt-stable75014
✅ sveltekit-stable75014
✅ vite-stable75014
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack8108
❌ 🌍 Community Worlds
AppPassedFailedSkipped
✅ mongodb-dev500
❌ mongodb5848
✅ redis-dev500
❌ redis5938
✅ turso-dev500
❌ turso4588
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable75014
✅ e2e-local-postgres-nest-stable75014
✅ e2e-local-prod-nest-stable75014

📋 View full workflow run

@github-actions

github-actionsBot commented Apr 9, 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🥇 Express0.043s (-1.6%)1.006s (~)0.962s101.00x
💻 LocalNitro0.044s (-4.8%)1.005s (~)0.961s101.01x
🐘 PostgresExpress0.046s (-22.5% 🟢)1.011s (~)0.965s101.07x
💻 LocalNext.js (Turbopack)0.049s (+3.6%)1.005s (~)0.957s101.12x
🌐 RedisNext.js (Turbopack)0.050s (-11.3% 🟢)1.005s (~)0.955s101.16x
🐘 PostgresNext.js (Turbopack)0.058s (-3.0%)1.011s (~)0.954s101.33x
🐘 PostgresNitro0.063s (+2.3%)1.012s (~)0.949s101.46x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express0.231s (-39.8% 🟢)2.203s (-10.5% 🟢)1.972s101.00x
▲ VercelNext.js (Turbopack)0.267s (-33.8% 🟢)2.195s (-8.4% 🟢)1.928s101.16x
▲ VercelNitro0.304s (+10.2% 🔺)2.211s (+4.0%)1.907s101.32x

🔍 Observability: Express | Next.js (Turbopack) | Nitro

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.119s (-3.1%)2.010s (~)0.891s101.00x
🌐 RedisNext.js (Turbopack)1.126s (~)2.007s (~)0.881s101.01x
💻 LocalNext.js (Turbopack)1.126s (~)2.006s (~)0.880s101.01x
💻 LocalNitro1.128s (~)2.005s (~)0.877s101.01x
💻 LocalExpress1.131s (~)2.006s (~)0.875s101.01x
🐘 PostgresNext.js (Turbopack)1.147s (~)2.009s (~)0.862s101.02x
🐘 PostgresNitro1.147s (-0.6%)2.011s (~)0.864s101.02x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.911s (+1.0%)3.886s (+0.8%)1.975s101.00x
▲ VercelNitro2.003s (+6.5% 🔺)3.356s (-10.3% 🟢)1.353s101.05x
▲ VercelNext.js (Turbopack)2.103s (+4.0%)3.881s (+3.0%)1.778s101.10x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express10.694s (-2.1%)11.028s (~)0.334s31.00x
🌐 RedisNext.js (Turbopack)10.779s (+0.7%)11.023s (~)0.244s31.01x
💻 LocalNext.js (Turbopack)10.831s (~)11.025s (~)0.194s31.01x
🐘 PostgresNext.js (Turbopack)10.849s (~)11.023s (~)0.174s31.01x
💻 LocalNitro10.920s (~)11.023s (~)0.103s31.02x
🐘 PostgresNitro10.934s (~)11.023s (~)0.088s31.02x
💻 LocalExpress10.952s (~)11.024s (~)0.072s31.02x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express16.776s (~)18.801s (-1.4%)2.025s21.00x
▲ VercelNext.js (Turbopack)17.133s (-0.8%)19.157s (~)2.024s21.02x
▲ VercelNitro17.627s (-1.4%)18.953s (-5.7% 🟢)1.326s21.05x

🔍 Observability: Express | Next.js (Turbopack) | Nitro

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express14.019s (-3.8%)14.627s (-2.6%)0.608s51.00x
🌐 RedisNext.js (Turbopack)14.257s (+0.9%)15.029s (~)0.772s41.02x
🐘 PostgresNext.js (Turbopack)14.412s (-0.6%)15.023s (~)0.612s41.03x
🐘 PostgresNitro14.702s (~)15.021s (~)0.319s41.05x
💻 LocalNext.js (Turbopack)14.709s (+1.0%)15.030s (~)0.322s41.05x
💻 LocalNitro14.924s (-0.6%)15.028s (-3.2%)0.104s41.06x
💻 LocalExpress14.989s (~)15.030s (~)0.041s41.07x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express33.420s (-3.4%)35.676s (-3.4%)2.256s21.00x
▲ VercelNext.js (Turbopack)34.176s (-3.3%)36.300s (-3.0%)2.124s21.02x
▲ VercelNitro34.324s (+0.8%)35.857s (-1.2%)1.533s21.03x

🔍 Observability: Express | Next.js (Turbopack) | Nitro

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express12.957s (-8.6% 🟢)13.021s (-13.3% 🟢)0.064s71.00x
🌐 RedisNext.js (Turbopack)13.389s (+1.2%)14.025s (~)0.636s71.03x
🐘 PostgresNext.js (Turbopack)13.757s (~)14.019s (~)0.262s71.06x
🐘 PostgresNitro14.192s (-0.9%)15.022s (~)0.829s61.10x
💻 LocalNext.js (Turbopack)16.033s (+2.8%)16.531s (+3.1%)0.498s61.24x
💻 LocalExpress16.441s (-0.7%)17.032s (~)0.591s61.27x
💻 LocalNitro16.531s (~)17.029s (~)0.498s61.28x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)56.887s (-2.0%)58.539s (-2.7%)1.652s21.00x
▲ VercelExpress59.072s (+7.5% 🔺)61.603s (+7.7% 🔺)2.531s21.04x
▲ VercelNitro59.188s (+2.0%)60.723s (~)1.535s21.04x

🔍 Observability: Next.js (Turbopack) | Express | Nitro

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.211s (-8.6% 🟢)2.009s (~)0.798s151.00x
🐘 PostgresNext.js (Turbopack)1.232s (~)2.009s (~)0.777s151.02x
🐘 PostgresNitro1.260s (~)2.009s (~)0.749s151.04x
🌐 RedisNext.js (Turbopack)1.318s (+6.2% 🔺)2.006s (~)0.688s151.09x
💻 LocalExpress1.515s (+0.8%)2.006s (~)0.491s151.25x
💻 LocalNitro1.516s (+2.1%)2.005s (~)0.490s151.25x
💻 LocalNext.js (Turbopack)1.544s (+4.6%)2.006s (~)0.461s151.27x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.253s (-6.7% 🟢)4.180s (+4.1%)1.927s81.00x
▲ VercelNitro2.496s (-15.3% 🟢)3.670s (-19.6% 🟢)1.174s91.11x
▲ VercelNext.js (Turbopack)2.719s (+23.5% 🔺)4.105s (+6.9% 🔺)1.386s81.21x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.300s (-2.9%)3.009s (~)0.708s101.00x
🐘 PostgresNitro2.330s (+0.7%)3.009s (~)0.680s101.01x
🐘 PostgresNext.js (Turbopack)2.385s (-0.7%)3.009s (~)0.624s101.04x
🌐 RedisNext.js (Turbopack)2.564s (+5.3% 🔺)3.008s (~)0.444s101.11x
💻 LocalExpress2.895s (+1.4%)3.453s (+3.3%)0.558s91.26x
💻 LocalNitro2.991s (+6.5% 🔺)3.341s (+11.1% 🔺)0.351s91.30x
💻 LocalNext.js (Turbopack)3.077s (+24.5% 🔺)3.884s (+29.1% 🔺)0.808s81.34x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.670s (-10.7% 🟢)4.008s (-14.7% 🟢)1.338s81.00x
▲ VercelNext.js (Turbopack)2.780s (+3.0%)4.402s (-1.8%)1.622s71.04x
▲ VercelExpress2.879s (+4.4%)4.437s (+2.9%)1.558s71.08x

🔍 Observability: Nitro | Next.js (Turbopack) | Express

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express3.383s (-2.3%)4.011s (~)0.628s81.00x
🐘 PostgresNitro3.472s (-0.5%)4.011s (~)0.538s81.03x
🐘 PostgresNext.js (Turbopack)3.643s (-1.1%)4.009s (~)0.367s81.08x
🌐 RedisNext.js (Turbopack)4.164s (+5.3% 🔺)5.011s (+16.6% 🔺)0.847s61.23x
💻 LocalExpress7.500s (-8.6% 🟢)8.021s (-8.5% 🟢)0.521s42.22x
💻 LocalNitro8.022s (+1.6%)8.773s (+6.1% 🔺)0.752s42.37x
💻 LocalNext.js (Turbopack)8.384s (+47.2% 🔺)9.019s (+50.0% 🔺)0.635s42.48x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.054s (-1.0%)5.367s (+9.7% 🔺)2.313s61.00x
▲ VercelNitro3.295s (+8.8% 🔺)4.508s (-7.1% 🟢)1.213s71.08x
▲ VercelNext.js (Turbopack)3.678s (+1.9%)5.769s (+8.0% 🔺)2.091s61.20x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.204s (-4.3%)2.008s (~)0.804s151.00x
🐘 PostgresNext.js (Turbopack)1.251s (+2.1%)2.009s (~)0.758s151.04x
🐘 PostgresNitro1.264s (~)2.008s (~)0.744s151.05x
🌐 RedisNext.js (Turbopack)1.278s (-0.6%)2.006s (~)0.729s151.06x
💻 LocalExpress1.519s (~)2.006s (~)0.487s151.26x
💻 LocalNitro1.524s (-3.4%)2.006s (~)0.482s151.27x
💻 LocalNext.js (Turbopack)1.556s (~)2.007s (-3.2%)0.451s151.29x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.003s (-4.2%)3.908s (-1.8%)1.905s81.00x
▲ VercelNitro2.006s (-7.6% 🟢)3.636s (-6.7% 🟢)1.630s91.00x
▲ VercelNext.js (Turbopack)2.217s (+6.1% 🔺)3.745s (+5.0% 🔺)1.529s91.11x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro2.329s (~)3.010s (~)0.681s101.00x
🐘 PostgresExpress2.332s (-0.9%)3.008s (~)0.676s101.00x
🐘 PostgresNext.js (Turbopack)2.390s (~)3.011s (~)0.620s101.03x
🌐 RedisNext.js (Turbopack)2.543s (+3.6%)3.007s (~)0.464s101.09x
💻 LocalNext.js (Turbopack)2.807s (-10.8% 🟢)3.341s (-6.3% 🟢)0.533s91.21x
💻 LocalExpress2.938s (-0.9%)3.454s (-3.1%)0.516s91.26x
💻 LocalNitro2.990s (-1.3%)3.677s (-2.2%)0.687s91.28x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.320s (~)4.314s (+9.6% 🔺)1.994s81.00x
▲ VercelNitro2.485s (-3.3%)3.963s (-5.7% 🟢)1.478s81.07x
▲ VercelNext.js (Turbopack)2.779s (+9.3% 🔺)4.314s (+3.7%)1.535s71.20x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro3.453s (~)4.010s (~)0.557s81.00x
🐘 PostgresExpress3.457s (~)4.009s (~)0.553s81.00x
🐘 PostgresNext.js (Turbopack)3.646s (~)4.009s (~)0.363s81.06x
🌐 RedisNext.js (Turbopack)4.169s (+3.5%)5.012s (+9.4% 🔺)0.843s61.21x
💻 LocalExpress8.033s (-6.6% 🟢)8.774s (-2.8%)0.741s42.33x
💻 LocalNitro8.669s (+2.3%)9.026s (~)0.357s42.51x
💻 LocalNext.js (Turbopack)8.695s (+12.2% 🔺)9.271s (+12.1% 🔺)0.576s42.52x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.728s (-4.1%)4.288s (-0.9%)1.560s71.00x
▲ VercelExpress3.170s (+16.4% 🔺)4.654s (+9.8% 🔺)1.484s71.16x
▲ VercelNext.js (Turbopack)3.271s (-13.0% 🟢)5.128s (-7.0% 🟢)1.857s61.20x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.608s (-26.8% 🟢)1.006s (~)0.399s601.00x
🌐 RedisNext.js (Turbopack)0.708s (+11.4% 🔺)1.004s (~)0.296s601.16x
🐘 PostgresNext.js (Turbopack)0.819s (+6.2% 🔺)1.041s (+1.7%)0.222s581.35x
🐘 PostgresNitro0.858s (+2.2%)1.023s (+1.7%)0.165s591.41x
💻 LocalNext.js (Turbopack)0.860s (-2.7%)1.005s (-3.3%)0.145s601.41x
💻 LocalNitro0.980s (-5.4% 🟢)1.115s (-44.4% 🟢)0.135s541.61x
💻 LocalExpress1.016s (+4.1%)1.654s (+48.3% 🔺)0.638s371.67x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro8.903s (-8.6% 🟢)10.501s (-8.5% 🟢)1.599s61.00x
▲ VercelNext.js (Turbopack)9.632s (-23.0% 🟢)11.579s (-17.7% 🟢)1.948s61.08x
▲ VercelExpress10.403s (+2.7%)12.297s (+1.4%)1.893s51.17x

🔍 Observability: Nitro | Next.js (Turbopack) | Express

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.477s (-26.2% 🟢)2.007s (-16.4% 🟢)0.530s451.00x
🌐 RedisNext.js (Turbopack)1.672s (+15.4% 🔺)2.006s (~)0.333s451.13x
🐘 PostgresNitro2.078s (+3.4%)2.944s (+16.1% 🔺)0.866s311.41x
🐘 PostgresNext.js (Turbopack)2.153s (+14.8% 🔺)2.821s (+37.5% 🔺)0.668s321.46x
💻 LocalNext.js (Turbopack)2.700s (~)3.008s (~)0.308s301.83x
💻 LocalNitro3.022s (-1.1%)3.547s (-9.6% 🟢)0.525s262.05x
💻 LocalExpress3.162s (+3.6%)3.967s (+4.3%)0.805s232.14x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro28.495s (-3.9%)30.408s (-3.5%)1.913s31.00x
▲ VercelExpress28.833s (-1.1%)30.922s (-1.2%)2.089s31.01x
▲ VercelNext.js (Turbopack)29.626s (-0.8%)31.356s (-2.6%)1.730s31.04x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express3.109s (-23.3% 🟢)3.790s (-20.7% 🟢)0.681s321.00x
🌐 RedisNext.js (Turbopack)3.352s (+11.2% 🔺)4.008s (+15.7% 🔺)0.656s301.08x
🐘 PostgresNext.js (Turbopack)3.994s (+5.8% 🔺)4.331s (+8.0% 🔺)0.337s281.28x
🐘 PostgresNitro4.191s (~)4.932s (-1.6%)0.741s251.35x
💻 LocalNext.js (Turbopack)8.691s (+2.0%)9.019s (~)0.328s142.80x
💻 LocalExpress9.039s (-1.3%)9.633s (-1.6%)0.594s132.91x
💻 LocalNitro9.104s (+2.1%)9.710s (+3.3%)0.606s132.93x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express73.513s (-14.6% 🟢)75.368s (-14.2% 🟢)1.855s21.00x
▲ VercelNitro75.626s (+6.1% 🔺)77.847s (+6.5% 🔺)2.221s21.03x
▲ VercelNext.js (Turbopack)77.885s (-4.4%)79.559s (-4.2%)1.675s21.06x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.226s (-18.4% 🟢)1.007s (~)0.781s601.00x
🐘 PostgresNext.js (Turbopack)0.251s (+4.8%)1.007s (~)0.757s601.11x
🐘 PostgresNitro0.281s (-3.7%)1.007s (~)0.725s601.24x
🌐 RedisNext.js (Turbopack)0.284s (+7.7% 🔺)1.004s (~)0.720s601.26x
💻 LocalNitro0.588s (+4.9%)1.004s (~)0.417s602.60x
💻 LocalNext.js (Turbopack)0.620s (+20.2% 🔺)1.022s (+1.7%)0.402s592.74x
💻 LocalExpress0.697s (+18.7% 🔺)1.096s (+9.1% 🔺)0.398s553.08x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.544s (-22.1% 🟢)3.163s (-11.6% 🟢)1.620s201.00x
▲ VercelNext.js (Turbopack)1.688s (-5.7% 🟢)3.573s (+3.3%)1.886s181.09x
▲ VercelNitro1.727s (-8.8% 🟢)3.318s (-4.5%)1.591s191.12x

🔍 Observability: Express | Next.js (Turbopack) | Nitro

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.363s (-27.3% 🟢)1.006s (~)0.643s901.00x
🐘 PostgresNitro0.493s (-1.8%)1.007s (~)0.514s901.36x
🐘 PostgresNext.js (Turbopack)0.508s (+4.6%)1.007s (~)0.499s901.40x
🌐 RedisNext.js (Turbopack)1.144s (+2.9%)2.006s (+17.7% 🔺)0.862s453.15x
💻 LocalExpress2.492s (-1.6%)3.009s (~)0.517s306.86x
💻 LocalNitro2.652s (+14.2% 🔺)3.180s (+5.7% 🔺)0.528s297.31x
💻 LocalNext.js (Turbopack)2.676s (+7.7% 🔺)3.076s (+2.2%)0.401s307.37x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.612s (-2.1%)4.132s (-6.5% 🟢)1.519s221.00x
▲ VercelExpress2.738s (-7.0% 🟢)4.568s (-3.6%)1.830s201.05x
▲ VercelNext.js (Turbopack)3.366s (-5.4% 🟢)4.890s (-7.9% 🟢)1.524s191.29x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.583s (-26.2% 🟢)1.007s (~)0.423s1201.00x
🐘 PostgresNitro0.809s (-0.9%)1.009s (~)0.200s1191.39x
🐘 PostgresNext.js (Turbopack)0.847s (+10.4% 🔺)1.043s (+3.6%)0.196s1161.45x
🌐 RedisNext.js (Turbopack)2.677s (-3.5%)3.007s (-0.8%)0.330s404.59x
💻 LocalNitro10.890s (+4.7%)11.485s (+4.1%)0.595s1118.67x
💻 LocalExpress10.935s (~)11.395s (-1.5%)0.460s1118.75x
💻 LocalNext.js (Turbopack)11.065s (+7.4% 🔺)11.572s (+7.4% 🔺)0.507s1118.97x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro6.908s (+9.6% 🔺)8.327s (+3.7%)1.419s151.00x
▲ VercelNext.js (Turbopack)47.028s (+673.5% 🔺)48.733s (+526.0% 🔺)1.705s66.81x
▲ VercelExpress49.426s (+616.1% 🔺)51.745s (+489.1% 🔺)2.320s77.15x

🔍 Observability: Nitro | Next.js (Turbopack) | Express

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.158s (-25.4% 🟢)1.001s (+0.5%)0.001s (-26.7% 🟢)1.009s (~)0.851s101.00x
💻 LocalNext.js (Turbopack)0.170s (+0.5%)1.002s (~)0.012s (+14.7% 🔺)1.017s (~)0.847s101.07x
🐘 PostgresNext.js (Turbopack)0.192s (+0.8%)1.001s (~)0.001s (-15.4% 🟢)1.010s (~)0.818s101.21x
💻 LocalNitro0.207s (-0.7%)1.004s (~)0.012s (+11.5% 🔺)1.017s (~)0.810s101.31x
💻 LocalExpress0.219s (+6.8% 🔺)1.004s (~)0.011s (-8.5% 🟢)1.017s (~)0.798s101.38x
🐘 PostgresNitro0.219s (+2.0%)0.996s (~)0.001s (+30.0% 🔺)1.012s (~)0.792s101.39x
🌐 RedisNext.js (Turbopack)0.242s (+62.9% 🔺)1.001s (~)0.032s (+1776.5% 🔺)1.037s (+3.0%)0.796s101.53x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)1.493s (-11.9% 🟢)2.833s (-11.4% 🟢)0.461s (-51.8% 🟢)3.696s (-20.0% 🟢)2.203s101.00x
▲ VercelExpress1.565s (+3.1%)2.942s (+1.1%)0.642s (-16.1% 🟢)4.033s (-2.6%)2.468s101.05x
▲ VercelNitro1.603s (-5.5% 🟢)2.899s (-11.0% 🟢)0.562s (+7.0% 🔺)3.812s (-9.8% 🟢)2.208s101.07x

🔍 Observability: Next.js (Turbopack) | Express | Nitro

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)0.481s (+14.5% 🔺)1.002s (~)0.003s (+4.8%)1.011s (~)0.530s601.00x
🐘 PostgresExpress0.522s (-16.5% 🟢)1.006s (~)0.004s (-10.2% 🟢)1.022s (~)0.500s591.09x
🐘 PostgresNitro0.629s (-2.0%)1.005s (~)0.004s (-11.2% 🟢)1.022s (~)0.393s591.31x
🐘 PostgresNext.js (Turbopack)0.659s (+9.3% 🔺)1.026s (+1.7%)0.008s (+110.1% 🔺)1.044s (+2.2%)0.385s581.37x
💻 LocalNext.js (Turbopack)0.763s (+13.8% 🔺)1.011s (~)0.009s (-15.6% 🟢)1.116s (+8.9% 🔺)0.353s541.59x
💻 LocalNitro0.815s (-14.6% 🟢)1.012s (~)0.010s (+2.4%)1.111s (-9.5% 🟢)0.296s571.70x
💻 LocalExpress0.847s (+18.5% 🔺)1.010s (~)0.010s (+6.7% 🔺)1.115s (+8.9% 🔺)0.268s541.76x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.092s (+1.5%)5.761s (~)0.242s (-2.7%)6.436s (-0.6%)2.344s101.00x
▲ VercelNext.js (Turbopack)4.186s (-6.5% 🟢)6.079s (-0.6%)0.220s (+9.9% 🔺)6.720s (~)2.534s91.02x
▲ VercelNitro4.499s (+6.2% 🔺)5.768s (+2.8%)0.457s (+71.8% 🔺)6.725s (+5.7% 🔺)2.226s91.10x

🔍 Observability: Express | Next.js (Turbopack) | Nitro

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)0.900s (+1.8%)1.035s (+3.5%)0.000s (+106.9% 🔺)1.039s (+3.4%)0.139s581.00x
🐘 PostgresExpress0.931s (-3.9%)1.146s (-5.9% 🟢)0.000s (+172.2% 🔺)1.159s (-6.0% 🟢)0.228s541.03x
🐘 PostgresNitro0.946s (~)1.220s (+12.0% 🔺)0.000s (-17.3% 🟢)1.232s (+10.3% 🔺)0.286s491.05x
🐘 PostgresNext.js (Turbopack)0.956s (+3.1%)1.177s (+5.6% 🔺)0.000s (+Infinity% 🔺)1.185s (+4.2%)0.229s511.06x
💻 LocalNitro1.218s (+2.4%)2.021s (~)0.000s (-7.1% 🟢)2.023s (~)0.805s301.35x
💻 LocalNext.js (Turbopack)1.256s (~)2.019s (~)0.000s (-11.1% 🟢)2.022s (~)0.767s301.40x
💻 LocalExpress1.444s (+17.6% 🔺)2.022s (~)0.001s (+14.8% 🔺)2.203s (+8.8% 🔺)0.758s281.60x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.826s (~)4.141s (+4.9%)0.000s (+150.0% 🔺)4.585s (+4.0%)1.759s141.00x
▲ VercelNitro2.860s (+7.4% 🔺)4.133s (+4.8%)0.000s (-50.0% 🟢)4.541s (+3.8%)1.681s141.01x
▲ VercelNext.js (Turbopack)3.188s (-28.9% 🟢)4.619s (-22.8% 🟢)0.000s (-100.0% 🟢)5.035s (-21.8% 🟢)1.847s121.13x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

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

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)1.728s (-1.9%)2.035s (+1.7%)0.000s (-50.0% 🟢)2.039s (+1.6%)0.311s301.00x
🐘 PostgresExpress1.739s (-1.1%)2.143s (+3.8%)0.000s (-65.5% 🟢)2.152s (+3.4%)0.413s281.01x
🐘 PostgresNitro1.789s (+2.3%)2.102s (~)0.000s (+Infinity% 🔺)2.113s (-1.0%)0.324s291.04x
🐘 PostgresNext.js (Turbopack)1.958s (+6.4% 🔺)2.147s (+0.6%)0.000s (+Infinity% 🔺)2.155s (~)0.197s281.13x
💻 LocalNitro3.410s (+6.4% 🔺)4.034s (+1.7%)0.001s (+140.0% 🔺)4.037s (+1.7%)0.627s151.97x
💻 LocalNext.js (Turbopack)3.725s (+3.2%)4.233s (+1.6%)0.000s (-12.5% 🟢)4.236s (+1.6%)0.511s152.16x
💻 LocalExpress3.802s (+9.2% 🔺)4.368s (+8.3% 🔺)0.001s (+233.3% 🔺)4.372s (+8.3% 🔺)0.570s152.20x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.969s (-25.3% 🟢)5.072s (-23.7% 🟢)0.000s (-62.5% 🟢)5.489s (-22.2% 🟢)1.521s121.00x
▲ VercelNext.js (Turbopack)3.988s5.498s0.000s5.941s1.953s111.01x
▲ VercelExpress5.598s (+32.4% 🔺)7.223s (+32.7% 🔺)0.000s (-31.3% 🟢)7.670s (+30.2% 🔺)2.072s81.41x

🔍 Observability: Nitro | Next.js (Turbopack) | Express

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNext.js (Turbopack)10/21
🐘 PostgresExpress19/21
▲ VercelExpress12/21
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express🐘 Postgres17/21
Next.js (Turbopack)🌐 Redis10/21
Nitro🐘 Postgres15/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)

📋 View full workflow run

CopilotAI 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.

Pull request overview

Adds a workflow-mode optimization to the SWC plugin so that after "use step" class members are stripped, any now-unreferenced private class members are also removed, improving downstream tree-shaking (e.g., dropping Node-only imports kept alive by unused helpers).

Changes:

  • Implement iterative private-member reachability analysis and removal after step-member stripping (class decls + class exprs).
  • Add a new transform fixture validating cascading elimination of TypeScript private members in workflow mode.
  • Document the new optimization and publish a patch changeset.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 5 comments.

Show a summary per file
FileDescription
packages/swc-plugin-workflow/transform/src/lib.rsAdds private-member reference collection + DCE pass after "use step" stripping.
packages/swc-plugin-workflow/transform/tests/fixture/private-member-dce/input.tsNew fixture input covering cascading TS private elimination.
packages/swc-plugin-workflow/transform/tests/fixture/private-member-dce/output-workflow.jsExpected workflow output with TS-private members removed and imports dropped.
packages/swc-plugin-workflow/transform/tests/fixture/private-member-dce/output-step.jsExpected step-mode output (private members retained).
packages/swc-plugin-workflow/transform/tests/fixture/private-member-dce/output-client.jsExpected client-mode output (private members retained).
packages/swc-plugin-workflow/spec.mdDocuments the private-member DCE behavior and example.
.changeset/private-member-dce.mdDeclares a patch release for the SWC plugin change.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +619 to +624
// TS private or any member: `this.foo` — only track when
// the object is `this` (to avoid false positives from
// unrelated member accesses like `obj.foo`)
MemberProp::Ident(ident) => {
if matches!(&*expr.obj, Expr::This(_)) {
self.referenced.insert(ident.sym.to_string());

CopilotAIApr 9, 2026

Copy link

Choose a reason for hiding this comment

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

ClassMemberRefCollector only records MemberProp::Ident accesses when the object is this. In TypeScript, private members are also legally accessed via other instances of the same class (e.g. static compare(a: C, b: C) { return a.x - b.x }). With the current check, those references won’t be collected and the private member can be incorrectly removed, breaking runtime behavior. Consider treating any .ident access as a reference when ident matches a known TS-private member name in the class, regardless of the receiver expression (safe, though more conservative).

Suggested change
// TS private or any member: `this.foo` — only track when
// the object is `this` (to avoid false positives from
// unrelated member accesses like `obj.foo`)
MemberProp::Ident(ident) => {
ifmatches!(&*expr.obj,Expr::This(_)){
self.referenced.insert(ident.sym.to_string());
// TS private or any member access. Track `this.foo` as before,
// and also track `obj.foo` when `foo` is a known TS-private
// member of the current class so same-class accesses like
// `a.x` / `b.x` are not missed.
MemberProp::Ident(ident) => {
let name = ident.sym.to_string();
ifmatches!(&*expr.obj,Expr::This(_)) || self.private_names.contains(&name){
self.referenced.insert(name);

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Fixed. The collector now holds a ts_private_names: HashSet<String> built from the class body. MemberProp::Ident accesses are tracked when the name is in that set, regardless of the receiver expression. This catches a.x / b.x patterns in static methods.

Comment on lines +629 to +630
// Continue visiting children
expr.obj.visit_with(self);

CopilotAIApr 9, 2026

Copy link

Choose a reason for hiding this comment

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

visit_member_expr overrides default traversal but only visits expr.obj. For computed member expressions (obj[expr]), the property expression is not visited, so any this.#x / this.x references inside the computed key won’t be collected and could cause incorrectly eliminating private members. Use expr.visit_children_with(self) (or explicitly visit the computed property expression) to preserve full traversal.

Suggested change
// Continue visiting children
expr.obj.visit_with(self);
// Continue visiting children, including computed property expressions.
expr.visit_children_with(self);

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Fixed. Changed to expr.visit_children_with(self) which visits both the object and property expressions, including computed keys.

Comment on lines +614 to +625
match &expr.prop {
// Native JS private: `this.#foo`
MemberProp::PrivateName(name) => {
self.referenced.insert(name.name.to_string());
}
// TS private or any member: `this.foo` — only track when
// the object is `this` (to avoid false positives from
// unrelated member accesses like `obj.foo`)
MemberProp::Ident(ident) => {
if matches!(&*expr.obj, Expr::This(_)) {
self.referenced.insert(ident.sym.to_string());
}

CopilotAIApr 9, 2026

Copy link

Choose a reason for hiding this comment

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

Native private #foo and public/TS member foo are both stored as the same string ("foo"). If a class contains both foo and #foo, a this.foo access will incorrectly mark #foo as referenced, preventing intended elimination. Consider encoding JS-private names distinctly (e.g. prefix with # or store an enum) so #foo and foo don’t collide.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Fixed. JS native private names are now stored with a # prefix (e.g. "#foo") while TS private names remain unprefixed ("foo"). private_member_name() returns the prefixed form for PrivateMethod/PrivateProp, and visit_member_expr inserts format!("#{}", name) for MemberProp::PrivateName.

Comment on lines +1 to +25
import { WORKFLOW_SERIALIZE, WORKFLOW_DESERIALIZE } from '@workflow/serde';
import { getWorld } from './world.js';
import { importKey } from './encryption.js';

export class Run {
static [WORKFLOW_SERIALIZE](instance: Run) {
return { id: instance.id };
}

static [WORKFLOW_DESERIALIZE](data: { id: string }) {
return new Run(data.id);
}

id: string;

// TS private field — only referenced by stripped methods
private encryptionKeyPromise: Promise<any> | null = null;

// TS private method — only called by stripped getters/methods
private async getEncryptionKey(): Promise<any> {
if (!this.encryptionKeyPromise) {
this.encryptionKeyPromise = importKey(this.id);
}
return this.encryptionKeyPromise;
}

CopilotAIApr 9, 2026

Copy link

Choose a reason for hiding this comment

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

The new DCE logic claims to support native JS private members (#field/#method()), but the fixture only covers TypeScript private. Adding a fixture that uses #private syntax (including a cascading case) would help prevent regressions in the JS-private path.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Added in the second commit: private-member-dce-native/input.js tests JS native #field and #method() DCE including cascading elimination (#encryptionKeyPromise eliminated because #getEncryptionKey is eliminated) and survival of referenced members (#label kept because toString() references it).

Comment on lines +8504 to +8510
// Dead-code-eliminate unreferenced private members
// (same logic as visit_mut_class_decl above)
let referenced =
ClassMemberRefCollector::collect_from_class_body(&class_expr.class.body);
class_expr.class.body.retain(|member| match member {
ClassMember::PrivateMethod(m) => referenced.contains(&m.key.name.to_string()),
ClassMember::PrivateProp(p) => referenced.contains(&p.key.name.to_string()),

CopilotAIApr 9, 2026

Copy link

Choose a reason for hiding this comment

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

The private-member retention filter is duplicated in both visit_mut_class_decl and visit_mut_class_expr. This duplication increases the risk of future divergence/bugs when the logic changes. Consider extracting the retain logic into a shared helper (e.g. fn retain_referenced_private_members(body: &mut Vec<ClassMember>)) used by both visitors.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Fixed. Extracted retain_referenced_private_members(body: &mut Vec<ClassMember>) on ClassMemberRefCollector, called from both visit_mut_class_decl and visit_mut_class_expr.

- Namespace JS native private names with # prefix to avoid collisions
with TS private members of the same name
- Track TS private member accesses on non-this receivers (e.g. a.x in
static methods) by maintaining a set of known TS-private names
- Use visit_children_with for full traversal including computed member
expressions
- Extract retain logic into shared retain_referenced_private_members()
helper used by both visit_mut_class_decl and visit_mut_class_expr

@VaguelySeriousVaguelySerious left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

AI review: no blocking issues

// accesses like `a.x` / `b.x` are not missed.
MemberProp::Ident(ident) => {
let name = ident.sym.to_string();
if matches!(&*expr.obj, Expr::This(_)) || self.ts_private_names.contains(&name) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

AI Review: Note

For MemberProp::Ident, any this.foo access records foo in referenced, not only TS-private members. Retain still keys off private_member_name, so this is mostly extra churn in the set rather than incorrect retention, but it is slightly less precise than filtering to private names only.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Correct - the collector is conservative: it records all this.foo accesses, not just those matching known private names. This is intentional for simplicity. The extra entries in the referenced set are harmless since retain only removes members that private_member_name() identifies as private. Public members are always kept regardless of what's in the set.

});
```

This optimization is critical for SDK classes like `Run` where private helper methods reference Node.js-only imports (encryption, world access, etc.) — eliminating them allows the downstream module-level DCE to also remove those imports from the workflow bundle.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

AI Review: Nit

This section explains the algorithm well. If you want to reduce future issue reports, a short explicit note on unsupported patterns (for example computed this[...] access to private fields) could help set expectations.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Good suggestion. Computed access like this[name] where name happens to be a private member name won't be tracked, which could lead to incorrect elimination. In practice this pattern is very rare for private members (especially in SDK code like Run), but worth noting if we want to document limitations.

@TooTallNate
TooTallNate enabled auto-merge (squash) April 9, 2026 19:22
@TooTallNate
TooTallNate merged commit 66585fd into mainApr 9, 2026
102 of 103 checks passed
@TooTallNate
TooTallNate deleted the fix/swc-private-member-dce branch April 9, 2026 19:27
@ghostghost mentioned this pull request Apr 9, 2026
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

@TooTallNate@VaguelySerious
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

feat(swc): dead code eliminate unreferenced private class members in workflow mode - #1671

Merged
TooTallNate merged 3 commits into
mainfrom
fix/swc-private-member-dce
Apr 9, 2026
Merged

feat(swc): dead code eliminate unreferenced private class members in workflow mode#1671
TooTallNate merged 3 commits into
mainfrom
fix/swc-private-member-dce

Conversation

@TooTallNate

@TooTallNateTooTallNate commented Apr 9, 2026

Copy link
Copy Markdown
Member

Summary

After stripping "use step" methods from a class body in workflow mode, eliminate private members that are no longer referenced by any remaining public member. This applies to both:

  • JS native private: #field, #method()
  • TypeScript private: private field, private method()

The algorithm is iterative — references are seeded from public members, then expanded through surviving private members until a fixed point, enabling cascading elimination.

Example

Input:

exportclassRun{static[WORKFLOW_SERIALIZE](instance){return{id: instance.id};}static[WORKFLOW_DESERIALIZE](data){returnnewRun(data.id);}id: string;privateencryptionKeyPromise: Promise<any>|null=null;privateasyncgetEncryptionKey(){if(!this.encryptionKeyPromise){this.encryptionKeyPromise=importKey(this.id);}returnthis.encryptionKeyPromise;}constructor(id: string){this.id=id;}getvalue(): Promise<any>{'use step';returnthis.getEncryptionKey().then(()=>getWorld().get(this.id));}asynccancel(): Promise<void>{'use step';constkey=awaitthis.getEncryptionKey();awaitgetWorld().cancel(this.id,key);}toString(): string{return`Run(${this.id})`;}}

Workflow mode output:

exportclassRun{static[WORKFLOW_SERIALIZE](instance){return{id: instance.id};}static[WORKFLOW_DESERIALIZE](data){returnnewRun(data.id);}id;// ✅ private encryptionKeyPromise — ELIMINATED (only referenced by getEncryptionKey)// ✅ private getEncryptionKey() — ELIMINATED (only referenced by stripped step methods)constructor(id){this.id=id;}toString(){return`Run(${this.id})`;}}Run.prototype["cancel"]=globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//...");var__step_Run$value=globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//...");Object.defineProperty(Run.prototype,"value",{get(){return__step_Run$value.call(this);},configurable: true,enumerable: false});

The cascading elimination is key: encryptionKeyPromise is only referenced by getEncryptionKey(), which is itself only referenced by the stripped value getter and cancel() method. Both private members are removed, allowing the downstream module-level DCE to also eliminate the importKey and getWorld imports.

Motivation

SDK classes like Run have private helper methods that reference Node.js-only imports (encryption, world access). Without this optimization, those helpers survive into the workflow bundle even though nothing calls them after "use step" bodies are stripped, keeping the Node.js imports alive and preventing tree-shaking.

Test

New fixture: private-member-dce/input.ts with expected outputs for all three modes.

…workflow mode
After stripping 'use step' methods from a class body in workflow mode,
eliminate private members (both JS native #field/#method and TypeScript
private field/private method) that are no longer referenced by any
remaining public member.
The algorithm is iterative: references are seeded from public members,
then expanded through surviving private members until a fixed point,
enabling cascading elimination (e.g. a private field only referenced by
a private method that is itself unreferenced).
CopilotAI review requested due to automatic review settings April 9, 2026 18:09
@TooTallNate
TooTallNate requested a review from a team as a code ownerApril 9, 2026 18:09
@changeset-bot

changeset-botBot commented Apr 9, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 4ea576a

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

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

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 Apr 9, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Apr 9, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production912067979
✅ 💻 Local Development88601821068
✅ 📦 Local Production88601821068
✅ 🐘 Local Postgres88601821068
✅ 🪟 Windows810889
❌ 🌍 Community Worlds1366524225
✅ 📋 Other225042267
Total4012656874764

❌ Failed Tests

🌍 Community Worlds (65 failed)

mongodb (4 failed):

  • hookWorkflow is not resumable via public webhook endpoint | wrun_01KNSR613X7J6NJ3Q7CC1GAXC8
  • webhookWorkflow | wrun_01KNSR6A2N8E4FRA7K4W0WAJ2X
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously | wrun_01KNSRE4P2HFBR1HGDZ60FSZ4A
  • resilient start: addTenWorkflow completes when run_created returns 500 | wrun_01KNSRMTFPHDMEWW1M0C99YMJB

redis (3 failed):

  • hookWorkflow is not resumable via public webhook endpoint | wrun_01KNSR613X7J6NJ3Q7CC1GAXC8
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously | wrun_01KNSRE4P2HFBR1HGDZ60FSZ4A
  • resilient start: addTenWorkflow completes when run_created returns 500 | wrun_01KNSRMTFPHDMEWW1M0C99YMJB

turso (58 failed):

  • addTenWorkflow | wrun_01KNSR4Q12NYS08Q5M8JMBJE2P
  • addTenWorkflow | wrun_01KNSR4Q12NYS08Q5M8JMBJE2P
  • wellKnownAgentWorkflow (.well-known/agent) | wrun_01KNSR6H7RBB0VN5AF50A8K3HK
  • should work with react rendering in step
  • promiseAllWorkflow | wrun_01KNSR50EQH830PRD40KS32Q9Y
  • promiseRaceWorkflow | wrun_01KNSR54Y75RYQDBMAWK470VYP
  • promiseAnyWorkflow | wrun_01KNSR58C03DCWT8KGXWBMNCP6
  • importedStepOnlyWorkflow | wrun_01KNSR6WGHHN8XTHMJ86Q1JS10
  • hookWorkflow | wrun_01KNSR5M9AWG051SF57JDHTN15
  • hookWorkflow is not resumable via public webhook endpoint | wrun_01KNSR613X7J6NJ3Q7CC1GAXC8
  • webhookWorkflow | wrun_01KNSR6A2N8E4FRA7K4W0WAJ2X
  • sleepingWorkflow | wrun_01KNSR6G7XQP06VPNVA5S5SZ7P
  • parallelSleepWorkflow | wrun_01KNSR6W68GVQAECF6EXH5YKW4
  • nullByteWorkflow | wrun_01KNSR6ZZHZA6GX9A995NWGH4W
  • workflowAndStepMetadataWorkflow | wrun_01KNSR722J4AE22DPWYBGR40AA
  • fetchWorkflow | wrun_01KNSR9QTD0RQEWYMKGRM9P6KS
  • promiseRaceStressTestWorkflow | wrun_01KNSR9V7MWTQB99MYWGDBMPV1
  • error handling error propagation workflow errors nested function calls preserve message and stack trace
  • error handling error propagation workflow errors cross-file imports preserve message and stack trace
  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior FatalError fails immediately without retries
  • error handling retry behavior RetryableError respects custom retryAfter delay
  • error handling retry behavior maxRetries=0 disables retries
  • error handling catchability FatalError can be caught and detected with FatalError.is()
  • error handling not registered WorkflowNotRegisteredError fails the run when workflow does not exist
  • error handling not registered StepNotRegisteredError fails the step but workflow can catch it
  • error handling not registered StepNotRegisteredError fails the run when not caught in workflow
  • hookCleanupTestWorkflow - hook token reuse after workflow completion | wrun_01KNSRDFYWY5RDPW5MJW4M2RKP
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously | wrun_01KNSRE4P2HFBR1HGDZ60FSZ4A
  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running | wrun_01KNSREVJW6G5G63ZYP9DTG2CT
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars) | wrun_01KNSRFFZ3HX99QC8VH9X00X19
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument | wrun_01KNSRFS1Y9106Z5PVYGW5SZ8F
  • closureVariableWorkflow - nested step functions with closure variables | wrun_01KNSRFYM8QZ6MKBWEXNSXEA1E
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step | wrun_01KNSRG1102E8CS370B44Y7JJ1
  • health check (queue-based) - workflow and step endpoints respond to health check messages
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly | wrun_01KNSRGH373GH51TWFMF3M6THG
  • Calculator.calculate - static workflow method using static step methods from another class | wrun_01KNSRGPSM4VSQ1R3Z11NG4MZ0
  • AllInOneService.processNumber - static workflow method using sibling static step methods | wrun_01KNSRGXK317X0HN019FNHP541
  • ChainableService.processWithThis - static step methods using this to reference the class | wrun_01KNSRH5K54HKGJC6TW5YDC6P4
  • thisSerializationWorkflow - step function invoked with .call() and .apply() | wrun_01KNSRHCE7T09PPS45FA7D75MK
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE | wrun_01KNSRHM1A0G2BFRKSN1893XHR
  • instanceMethodStepWorkflow - instance methods with "use step" directive | wrun_01KNSRHTZQJP99YC0M9FNWQPM5
  • crossContextSerdeWorkflow - classes defined in step code are deserializable in workflow context | wrun_01KNSRJ68YGFBGXJ7YFR7ZHMV5
  • stepFunctionAsStartArgWorkflow - step function reference passed as start() argument | wrun_01KNSRJFBR9XRH3012W64TDAJ1
  • cancelRun - cancelling a running workflow | wrun_01KNSRJP4YQBW4BDB9ZFHE3JXZ
  • cancelRun via CLI - cancelling a running workflow | wrun_01KNSRK06K5725SZWQJ2DJZZ1K
  • pages router addTenWorkflow via pages router
  • pages router promiseAllWorkflow via pages router
  • pages router sleepingWorkflow via pages router
  • hookWithSleepWorkflow - hook payloads delivered correctly with concurrent sleep | wrun_01KNSRKDA53YRF17ST7YTD02BW
  • sleepInLoopWorkflow - sleep inside loop with steps actually delays each iteration | wrun_01KNSRM3T179X2E69VG894KAX9
  • sleepWithSequentialStepsWorkflow - sequential steps work with concurrent sleep (control) | wrun_01KNSRME2NVXNSBMBR8AND2DKJ
  • importMetaUrlWorkflow - import.meta.url is available in step bundles | wrun_01KNSRMP16CE789X5BG39DBJEA
  • metadataFromHelperWorkflow - getWorkflowMetadata/getStepMetadata work from module-level helper (#1577) | wrun_01KNSRMR5GRYXW87SN5A0AM9TV
  • resilient start: addTenWorkflow completes when run_created returns 500 | wrun_01KNSRMTFPHDMEWW1M0C99YMJB
  • getterStepWorkflow - getter functions with "use step" directive | wrun_01KNSRMXQTQ6GKDFR5NN8DMJRA

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro8207
✅ example8207
✅ express8207
✅ fastify8207
✅ hono8207
✅ nextjs-turbopack8702
✅ nextjs-webpack8702
✅ nitro8207
✅ nuxt8207
✅ sveltekit8207
✅ vite8207
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable75014
✅ express-stable75014
✅ fastify-stable75014
✅ hono-stable75014
✅ nextjs-turbopack-canary62027
✅ nextjs-turbopack-stable8108
✅ nextjs-webpack-canary62027
✅ nextjs-webpack-stable8108
✅ nitro-stable75014
✅ nuxt-stable75014
✅ sveltekit-stable75014
✅ vite-stable75014
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable75014
✅ express-stable75014
✅ fastify-stable75014
✅ hono-stable75014
✅ nextjs-turbopack-canary62027
✅ nextjs-turbopack-stable8108
✅ nextjs-webpack-canary62027
✅ nextjs-webpack-stable8108
✅ nitro-stable75014
✅ nuxt-stable75014
✅ sveltekit-stable75014
✅ vite-stable75014
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable75014
✅ express-stable75014
✅ fastify-stable75014
✅ hono-stable75014
✅ nextjs-turbopack-canary62027
✅ nextjs-turbopack-stable8108
✅ nextjs-webpack-canary62027
✅ nextjs-webpack-stable8108
✅ nitro-stable75014
✅ nuxt-stable75014
✅ sveltekit-stable75014
✅ vite-stable75014
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack8108
❌ 🌍 Community Worlds
AppPassedFailedSkipped
✅ mongodb-dev500
❌ mongodb5848
✅ redis-dev500
❌ redis5938
✅ turso-dev500
❌ turso4588
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable75014
✅ e2e-local-postgres-nest-stable75014
✅ e2e-local-prod-nest-stable75014

📋 View full workflow run

@github-actions

github-actionsBot commented Apr 9, 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🥇 Express0.043s (-1.6%)1.006s (~)0.962s101.00x
💻 LocalNitro0.044s (-4.8%)1.005s (~)0.961s101.01x
🐘 PostgresExpress0.046s (-22.5% 🟢)1.011s (~)0.965s101.07x
💻 LocalNext.js (Turbopack)0.049s (+3.6%)1.005s (~)0.957s101.12x
🌐 RedisNext.js (Turbopack)0.050s (-11.3% 🟢)1.005s (~)0.955s101.16x
🐘 PostgresNext.js (Turbopack)0.058s (-3.0%)1.011s (~)0.954s101.33x
🐘 PostgresNitro0.063s (+2.3%)1.012s (~)0.949s101.46x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express0.231s (-39.8% 🟢)2.203s (-10.5% 🟢)1.972s101.00x
▲ VercelNext.js (Turbopack)0.267s (-33.8% 🟢)2.195s (-8.4% 🟢)1.928s101.16x
▲ VercelNitro0.304s (+10.2% 🔺)2.211s (+4.0%)1.907s101.32x

🔍 Observability: Express | Next.js (Turbopack) | Nitro

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.119s (-3.1%)2.010s (~)0.891s101.00x
🌐 RedisNext.js (Turbopack)1.126s (~)2.007s (~)0.881s101.01x
💻 LocalNext.js (Turbopack)1.126s (~)2.006s (~)0.880s101.01x
💻 LocalNitro1.128s (~)2.005s (~)0.877s101.01x
💻 LocalExpress1.131s (~)2.006s (~)0.875s101.01x
🐘 PostgresNext.js (Turbopack)1.147s (~)2.009s (~)0.862s101.02x
🐘 PostgresNitro1.147s (-0.6%)2.011s (~)0.864s101.02x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.911s (+1.0%)3.886s (+0.8%)1.975s101.00x
▲ VercelNitro2.003s (+6.5% 🔺)3.356s (-10.3% 🟢)1.353s101.05x
▲ VercelNext.js (Turbopack)2.103s (+4.0%)3.881s (+3.0%)1.778s101.10x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express10.694s (-2.1%)11.028s (~)0.334s31.00x
🌐 RedisNext.js (Turbopack)10.779s (+0.7%)11.023s (~)0.244s31.01x
💻 LocalNext.js (Turbopack)10.831s (~)11.025s (~)0.194s31.01x
🐘 PostgresNext.js (Turbopack)10.849s (~)11.023s (~)0.174s31.01x
💻 LocalNitro10.920s (~)11.023s (~)0.103s31.02x
🐘 PostgresNitro10.934s (~)11.023s (~)0.088s31.02x
💻 LocalExpress10.952s (~)11.024s (~)0.072s31.02x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express16.776s (~)18.801s (-1.4%)2.025s21.00x
▲ VercelNext.js (Turbopack)17.133s (-0.8%)19.157s (~)2.024s21.02x
▲ VercelNitro17.627s (-1.4%)18.953s (-5.7% 🟢)1.326s21.05x

🔍 Observability: Express | Next.js (Turbopack) | Nitro

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express14.019s (-3.8%)14.627s (-2.6%)0.608s51.00x
🌐 RedisNext.js (Turbopack)14.257s (+0.9%)15.029s (~)0.772s41.02x
🐘 PostgresNext.js (Turbopack)14.412s (-0.6%)15.023s (~)0.612s41.03x
🐘 PostgresNitro14.702s (~)15.021s (~)0.319s41.05x
💻 LocalNext.js (Turbopack)14.709s (+1.0%)15.030s (~)0.322s41.05x
💻 LocalNitro14.924s (-0.6%)15.028s (-3.2%)0.104s41.06x
💻 LocalExpress14.989s (~)15.030s (~)0.041s41.07x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express33.420s (-3.4%)35.676s (-3.4%)2.256s21.00x
▲ VercelNext.js (Turbopack)34.176s (-3.3%)36.300s (-3.0%)2.124s21.02x
▲ VercelNitro34.324s (+0.8%)35.857s (-1.2%)1.533s21.03x

🔍 Observability: Express | Next.js (Turbopack) | Nitro

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express12.957s (-8.6% 🟢)13.021s (-13.3% 🟢)0.064s71.00x
🌐 RedisNext.js (Turbopack)13.389s (+1.2%)14.025s (~)0.636s71.03x
🐘 PostgresNext.js (Turbopack)13.757s (~)14.019s (~)0.262s71.06x
🐘 PostgresNitro14.192s (-0.9%)15.022s (~)0.829s61.10x
💻 LocalNext.js (Turbopack)16.033s (+2.8%)16.531s (+3.1%)0.498s61.24x
💻 LocalExpress16.441s (-0.7%)17.032s (~)0.591s61.27x
💻 LocalNitro16.531s (~)17.029s (~)0.498s61.28x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)56.887s (-2.0%)58.539s (-2.7%)1.652s21.00x
▲ VercelExpress59.072s (+7.5% 🔺)61.603s (+7.7% 🔺)2.531s21.04x
▲ VercelNitro59.188s (+2.0%)60.723s (~)1.535s21.04x

🔍 Observability: Next.js (Turbopack) | Express | Nitro

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.211s (-8.6% 🟢)2.009s (~)0.798s151.00x
🐘 PostgresNext.js (Turbopack)1.232s (~)2.009s (~)0.777s151.02x
🐘 PostgresNitro1.260s (~)2.009s (~)0.749s151.04x
🌐 RedisNext.js (Turbopack)1.318s (+6.2% 🔺)2.006s (~)0.688s151.09x
💻 LocalExpress1.515s (+0.8%)2.006s (~)0.491s151.25x
💻 LocalNitro1.516s (+2.1%)2.005s (~)0.490s151.25x
💻 LocalNext.js (Turbopack)1.544s (+4.6%)2.006s (~)0.461s151.27x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.253s (-6.7% 🟢)4.180s (+4.1%)1.927s81.00x
▲ VercelNitro2.496s (-15.3% 🟢)3.670s (-19.6% 🟢)1.174s91.11x
▲ VercelNext.js (Turbopack)2.719s (+23.5% 🔺)4.105s (+6.9% 🔺)1.386s81.21x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.300s (-2.9%)3.009s (~)0.708s101.00x
🐘 PostgresNitro2.330s (+0.7%)3.009s (~)0.680s101.01x
🐘 PostgresNext.js (Turbopack)2.385s (-0.7%)3.009s (~)0.624s101.04x
🌐 RedisNext.js (Turbopack)2.564s (+5.3% 🔺)3.008s (~)0.444s101.11x
💻 LocalExpress2.895s (+1.4%)3.453s (+3.3%)0.558s91.26x
💻 LocalNitro2.991s (+6.5% 🔺)3.341s (+11.1% 🔺)0.351s91.30x
💻 LocalNext.js (Turbopack)3.077s (+24.5% 🔺)3.884s (+29.1% 🔺)0.808s81.34x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.670s (-10.7% 🟢)4.008s (-14.7% 🟢)1.338s81.00x
▲ VercelNext.js (Turbopack)2.780s (+3.0%)4.402s (-1.8%)1.622s71.04x
▲ VercelExpress2.879s (+4.4%)4.437s (+2.9%)1.558s71.08x

🔍 Observability: Nitro | Next.js (Turbopack) | Express

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express3.383s (-2.3%)4.011s (~)0.628s81.00x
🐘 PostgresNitro3.472s (-0.5%)4.011s (~)0.538s81.03x
🐘 PostgresNext.js (Turbopack)3.643s (-1.1%)4.009s (~)0.367s81.08x
🌐 RedisNext.js (Turbopack)4.164s (+5.3% 🔺)5.011s (+16.6% 🔺)0.847s61.23x
💻 LocalExpress7.500s (-8.6% 🟢)8.021s (-8.5% 🟢)0.521s42.22x
💻 LocalNitro8.022s (+1.6%)8.773s (+6.1% 🔺)0.752s42.37x
💻 LocalNext.js (Turbopack)8.384s (+47.2% 🔺)9.019s (+50.0% 🔺)0.635s42.48x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.054s (-1.0%)5.367s (+9.7% 🔺)2.313s61.00x
▲ VercelNitro3.295s (+8.8% 🔺)4.508s (-7.1% 🟢)1.213s71.08x
▲ VercelNext.js (Turbopack)3.678s (+1.9%)5.769s (+8.0% 🔺)2.091s61.20x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.204s (-4.3%)2.008s (~)0.804s151.00x
🐘 PostgresNext.js (Turbopack)1.251s (+2.1%)2.009s (~)0.758s151.04x
🐘 PostgresNitro1.264s (~)2.008s (~)0.744s151.05x
🌐 RedisNext.js (Turbopack)1.278s (-0.6%)2.006s (~)0.729s151.06x
💻 LocalExpress1.519s (~)2.006s (~)0.487s151.26x
💻 LocalNitro1.524s (-3.4%)2.006s (~)0.482s151.27x
💻 LocalNext.js (Turbopack)1.556s (~)2.007s (-3.2%)0.451s151.29x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.003s (-4.2%)3.908s (-1.8%)1.905s81.00x
▲ VercelNitro2.006s (-7.6% 🟢)3.636s (-6.7% 🟢)1.630s91.00x
▲ VercelNext.js (Turbopack)2.217s (+6.1% 🔺)3.745s (+5.0% 🔺)1.529s91.11x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro2.329s (~)3.010s (~)0.681s101.00x
🐘 PostgresExpress2.332s (-0.9%)3.008s (~)0.676s101.00x
🐘 PostgresNext.js (Turbopack)2.390s (~)3.011s (~)0.620s101.03x
🌐 RedisNext.js (Turbopack)2.543s (+3.6%)3.007s (~)0.464s101.09x
💻 LocalNext.js (Turbopack)2.807s (-10.8% 🟢)3.341s (-6.3% 🟢)0.533s91.21x
💻 LocalExpress2.938s (-0.9%)3.454s (-3.1%)0.516s91.26x
💻 LocalNitro2.990s (-1.3%)3.677s (-2.2%)0.687s91.28x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.320s (~)4.314s (+9.6% 🔺)1.994s81.00x
▲ VercelNitro2.485s (-3.3%)3.963s (-5.7% 🟢)1.478s81.07x
▲ VercelNext.js (Turbopack)2.779s (+9.3% 🔺)4.314s (+3.7%)1.535s71.20x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro3.453s (~)4.010s (~)0.557s81.00x
🐘 PostgresExpress3.457s (~)4.009s (~)0.553s81.00x
🐘 PostgresNext.js (Turbopack)3.646s (~)4.009s (~)0.363s81.06x
🌐 RedisNext.js (Turbopack)4.169s (+3.5%)5.012s (+9.4% 🔺)0.843s61.21x
💻 LocalExpress8.033s (-6.6% 🟢)8.774s (-2.8%)0.741s42.33x
💻 LocalNitro8.669s (+2.3%)9.026s (~)0.357s42.51x
💻 LocalNext.js (Turbopack)8.695s (+12.2% 🔺)9.271s (+12.1% 🔺)0.576s42.52x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.728s (-4.1%)4.288s (-0.9%)1.560s71.00x
▲ VercelExpress3.170s (+16.4% 🔺)4.654s (+9.8% 🔺)1.484s71.16x
▲ VercelNext.js (Turbopack)3.271s (-13.0% 🟢)5.128s (-7.0% 🟢)1.857s61.20x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.608s (-26.8% 🟢)1.006s (~)0.399s601.00x
🌐 RedisNext.js (Turbopack)0.708s (+11.4% 🔺)1.004s (~)0.296s601.16x
🐘 PostgresNext.js (Turbopack)0.819s (+6.2% 🔺)1.041s (+1.7%)0.222s581.35x
🐘 PostgresNitro0.858s (+2.2%)1.023s (+1.7%)0.165s591.41x
💻 LocalNext.js (Turbopack)0.860s (-2.7%)1.005s (-3.3%)0.145s601.41x
💻 LocalNitro0.980s (-5.4% 🟢)1.115s (-44.4% 🟢)0.135s541.61x
💻 LocalExpress1.016s (+4.1%)1.654s (+48.3% 🔺)0.638s371.67x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro8.903s (-8.6% 🟢)10.501s (-8.5% 🟢)1.599s61.00x
▲ VercelNext.js (Turbopack)9.632s (-23.0% 🟢)11.579s (-17.7% 🟢)1.948s61.08x
▲ VercelExpress10.403s (+2.7%)12.297s (+1.4%)1.893s51.17x

🔍 Observability: Nitro | Next.js (Turbopack) | Express

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.477s (-26.2% 🟢)2.007s (-16.4% 🟢)0.530s451.00x
🌐 RedisNext.js (Turbopack)1.672s (+15.4% 🔺)2.006s (~)0.333s451.13x
🐘 PostgresNitro2.078s (+3.4%)2.944s (+16.1% 🔺)0.866s311.41x
🐘 PostgresNext.js (Turbopack)2.153s (+14.8% 🔺)2.821s (+37.5% 🔺)0.668s321.46x
💻 LocalNext.js (Turbopack)2.700s (~)3.008s (~)0.308s301.83x
💻 LocalNitro3.022s (-1.1%)3.547s (-9.6% 🟢)0.525s262.05x
💻 LocalExpress3.162s (+3.6%)3.967s (+4.3%)0.805s232.14x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro28.495s (-3.9%)30.408s (-3.5%)1.913s31.00x
▲ VercelExpress28.833s (-1.1%)30.922s (-1.2%)2.089s31.01x
▲ VercelNext.js (Turbopack)29.626s (-0.8%)31.356s (-2.6%)1.730s31.04x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express3.109s (-23.3% 🟢)3.790s (-20.7% 🟢)0.681s321.00x
🌐 RedisNext.js (Turbopack)3.352s (+11.2% 🔺)4.008s (+15.7% 🔺)0.656s301.08x
🐘 PostgresNext.js (Turbopack)3.994s (+5.8% 🔺)4.331s (+8.0% 🔺)0.337s281.28x
🐘 PostgresNitro4.191s (~)4.932s (-1.6%)0.741s251.35x
💻 LocalNext.js (Turbopack)8.691s (+2.0%)9.019s (~)0.328s142.80x
💻 LocalExpress9.039s (-1.3%)9.633s (-1.6%)0.594s132.91x
💻 LocalNitro9.104s (+2.1%)9.710s (+3.3%)0.606s132.93x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express73.513s (-14.6% 🟢)75.368s (-14.2% 🟢)1.855s21.00x
▲ VercelNitro75.626s (+6.1% 🔺)77.847s (+6.5% 🔺)2.221s21.03x
▲ VercelNext.js (Turbopack)77.885s (-4.4%)79.559s (-4.2%)1.675s21.06x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.226s (-18.4% 🟢)1.007s (~)0.781s601.00x
🐘 PostgresNext.js (Turbopack)0.251s (+4.8%)1.007s (~)0.757s601.11x
🐘 PostgresNitro0.281s (-3.7%)1.007s (~)0.725s601.24x
🌐 RedisNext.js (Turbopack)0.284s (+7.7% 🔺)1.004s (~)0.720s601.26x
💻 LocalNitro0.588s (+4.9%)1.004s (~)0.417s602.60x
💻 LocalNext.js (Turbopack)0.620s (+20.2% 🔺)1.022s (+1.7%)0.402s592.74x
💻 LocalExpress0.697s (+18.7% 🔺)1.096s (+9.1% 🔺)0.398s553.08x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.544s (-22.1% 🟢)3.163s (-11.6% 🟢)1.620s201.00x
▲ VercelNext.js (Turbopack)1.688s (-5.7% 🟢)3.573s (+3.3%)1.886s181.09x
▲ VercelNitro1.727s (-8.8% 🟢)3.318s (-4.5%)1.591s191.12x

🔍 Observability: Express | Next.js (Turbopack) | Nitro

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.363s (-27.3% 🟢)1.006s (~)0.643s901.00x
🐘 PostgresNitro0.493s (-1.8%)1.007s (~)0.514s901.36x
🐘 PostgresNext.js (Turbopack)0.508s (+4.6%)1.007s (~)0.499s901.40x
🌐 RedisNext.js (Turbopack)1.144s (+2.9%)2.006s (+17.7% 🔺)0.862s453.15x
💻 LocalExpress2.492s (-1.6%)3.009s (~)0.517s306.86x
💻 LocalNitro2.652s (+14.2% 🔺)3.180s (+5.7% 🔺)0.528s297.31x
💻 LocalNext.js (Turbopack)2.676s (+7.7% 🔺)3.076s (+2.2%)0.401s307.37x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.612s (-2.1%)4.132s (-6.5% 🟢)1.519s221.00x
▲ VercelExpress2.738s (-7.0% 🟢)4.568s (-3.6%)1.830s201.05x
▲ VercelNext.js (Turbopack)3.366s (-5.4% 🟢)4.890s (-7.9% 🟢)1.524s191.29x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.583s (-26.2% 🟢)1.007s (~)0.423s1201.00x
🐘 PostgresNitro0.809s (-0.9%)1.009s (~)0.200s1191.39x
🐘 PostgresNext.js (Turbopack)0.847s (+10.4% 🔺)1.043s (+3.6%)0.196s1161.45x
🌐 RedisNext.js (Turbopack)2.677s (-3.5%)3.007s (-0.8%)0.330s404.59x
💻 LocalNitro10.890s (+4.7%)11.485s (+4.1%)0.595s1118.67x
💻 LocalExpress10.935s (~)11.395s (-1.5%)0.460s1118.75x
💻 LocalNext.js (Turbopack)11.065s (+7.4% 🔺)11.572s (+7.4% 🔺)0.507s1118.97x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro6.908s (+9.6% 🔺)8.327s (+3.7%)1.419s151.00x
▲ VercelNext.js (Turbopack)47.028s (+673.5% 🔺)48.733s (+526.0% 🔺)1.705s66.81x
▲ VercelExpress49.426s (+616.1% 🔺)51.745s (+489.1% 🔺)2.320s77.15x

🔍 Observability: Nitro | Next.js (Turbopack) | Express

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.158s (-25.4% 🟢)1.001s (+0.5%)0.001s (-26.7% 🟢)1.009s (~)0.851s101.00x
💻 LocalNext.js (Turbopack)0.170s (+0.5%)1.002s (~)0.012s (+14.7% 🔺)1.017s (~)0.847s101.07x
🐘 PostgresNext.js (Turbopack)0.192s (+0.8%)1.001s (~)0.001s (-15.4% 🟢)1.010s (~)0.818s101.21x
💻 LocalNitro0.207s (-0.7%)1.004s (~)0.012s (+11.5% 🔺)1.017s (~)0.810s101.31x
💻 LocalExpress0.219s (+6.8% 🔺)1.004s (~)0.011s (-8.5% 🟢)1.017s (~)0.798s101.38x
🐘 PostgresNitro0.219s (+2.0%)0.996s (~)0.001s (+30.0% 🔺)1.012s (~)0.792s101.39x
🌐 RedisNext.js (Turbopack)0.242s (+62.9% 🔺)1.001s (~)0.032s (+1776.5% 🔺)1.037s (+3.0%)0.796s101.53x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)1.493s (-11.9% 🟢)2.833s (-11.4% 🟢)0.461s (-51.8% 🟢)3.696s (-20.0% 🟢)2.203s101.00x
▲ VercelExpress1.565s (+3.1%)2.942s (+1.1%)0.642s (-16.1% 🟢)4.033s (-2.6%)2.468s101.05x
▲ VercelNitro1.603s (-5.5% 🟢)2.899s (-11.0% 🟢)0.562s (+7.0% 🔺)3.812s (-9.8% 🟢)2.208s101.07x

🔍 Observability: Next.js (Turbopack) | Express | Nitro

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)0.481s (+14.5% 🔺)1.002s (~)0.003s (+4.8%)1.011s (~)0.530s601.00x
🐘 PostgresExpress0.522s (-16.5% 🟢)1.006s (~)0.004s (-10.2% 🟢)1.022s (~)0.500s591.09x
🐘 PostgresNitro0.629s (-2.0%)1.005s (~)0.004s (-11.2% 🟢)1.022s (~)0.393s591.31x
🐘 PostgresNext.js (Turbopack)0.659s (+9.3% 🔺)1.026s (+1.7%)0.008s (+110.1% 🔺)1.044s (+2.2%)0.385s581.37x
💻 LocalNext.js (Turbopack)0.763s (+13.8% 🔺)1.011s (~)0.009s (-15.6% 🟢)1.116s (+8.9% 🔺)0.353s541.59x
💻 LocalNitro0.815s (-14.6% 🟢)1.012s (~)0.010s (+2.4%)1.111s (-9.5% 🟢)0.296s571.70x
💻 LocalExpress0.847s (+18.5% 🔺)1.010s (~)0.010s (+6.7% 🔺)1.115s (+8.9% 🔺)0.268s541.76x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.092s (+1.5%)5.761s (~)0.242s (-2.7%)6.436s (-0.6%)2.344s101.00x
▲ VercelNext.js (Turbopack)4.186s (-6.5% 🟢)6.079s (-0.6%)0.220s (+9.9% 🔺)6.720s (~)2.534s91.02x
▲ VercelNitro4.499s (+6.2% 🔺)5.768s (+2.8%)0.457s (+71.8% 🔺)6.725s (+5.7% 🔺)2.226s91.10x

🔍 Observability: Express | Next.js (Turbopack) | Nitro

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)0.900s (+1.8%)1.035s (+3.5%)0.000s (+106.9% 🔺)1.039s (+3.4%)0.139s581.00x
🐘 PostgresExpress0.931s (-3.9%)1.146s (-5.9% 🟢)0.000s (+172.2% 🔺)1.159s (-6.0% 🟢)0.228s541.03x
🐘 PostgresNitro0.946s (~)1.220s (+12.0% 🔺)0.000s (-17.3% 🟢)1.232s (+10.3% 🔺)0.286s491.05x
🐘 PostgresNext.js (Turbopack)0.956s (+3.1%)1.177s (+5.6% 🔺)0.000s (+Infinity% 🔺)1.185s (+4.2%)0.229s511.06x
💻 LocalNitro1.218s (+2.4%)2.021s (~)0.000s (-7.1% 🟢)2.023s (~)0.805s301.35x
💻 LocalNext.js (Turbopack)1.256s (~)2.019s (~)0.000s (-11.1% 🟢)2.022s (~)0.767s301.40x
💻 LocalExpress1.444s (+17.6% 🔺)2.022s (~)0.001s (+14.8% 🔺)2.203s (+8.8% 🔺)0.758s281.60x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.826s (~)4.141s (+4.9%)0.000s (+150.0% 🔺)4.585s (+4.0%)1.759s141.00x
▲ VercelNitro2.860s (+7.4% 🔺)4.133s (+4.8%)0.000s (-50.0% 🟢)4.541s (+3.8%)1.681s141.01x
▲ VercelNext.js (Turbopack)3.188s (-28.9% 🟢)4.619s (-22.8% 🟢)0.000s (-100.0% 🟢)5.035s (-21.8% 🟢)1.847s121.13x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

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

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)1.728s (-1.9%)2.035s (+1.7%)0.000s (-50.0% 🟢)2.039s (+1.6%)0.311s301.00x
🐘 PostgresExpress1.739s (-1.1%)2.143s (+3.8%)0.000s (-65.5% 🟢)2.152s (+3.4%)0.413s281.01x
🐘 PostgresNitro1.789s (+2.3%)2.102s (~)0.000s (+Infinity% 🔺)2.113s (-1.0%)0.324s291.04x
🐘 PostgresNext.js (Turbopack)1.958s (+6.4% 🔺)2.147s (+0.6%)0.000s (+Infinity% 🔺)2.155s (~)0.197s281.13x
💻 LocalNitro3.410s (+6.4% 🔺)4.034s (+1.7%)0.001s (+140.0% 🔺)4.037s (+1.7%)0.627s151.97x
💻 LocalNext.js (Turbopack)3.725s (+3.2%)4.233s (+1.6%)0.000s (-12.5% 🟢)4.236s (+1.6%)0.511s152.16x
💻 LocalExpress3.802s (+9.2% 🔺)4.368s (+8.3% 🔺)0.001s (+233.3% 🔺)4.372s (+8.3% 🔺)0.570s152.20x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.969s (-25.3% 🟢)5.072s (-23.7% 🟢)0.000s (-62.5% 🟢)5.489s (-22.2% 🟢)1.521s121.00x
▲ VercelNext.js (Turbopack)3.988s5.498s0.000s5.941s1.953s111.01x
▲ VercelExpress5.598s (+32.4% 🔺)7.223s (+32.7% 🔺)0.000s (-31.3% 🟢)7.670s (+30.2% 🔺)2.072s81.41x

🔍 Observability: Nitro | Next.js (Turbopack) | Express

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNext.js (Turbopack)10/21
🐘 PostgresExpress19/21
▲ VercelExpress12/21
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express🐘 Postgres17/21
Next.js (Turbopack)🌐 Redis10/21
Nitro🐘 Postgres15/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)

📋 View full workflow run

CopilotAI 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.

Pull request overview

Adds a workflow-mode optimization to the SWC plugin so that after "use step" class members are stripped, any now-unreferenced private class members are also removed, improving downstream tree-shaking (e.g., dropping Node-only imports kept alive by unused helpers).

Changes:

  • Implement iterative private-member reachability analysis and removal after step-member stripping (class decls + class exprs).
  • Add a new transform fixture validating cascading elimination of TypeScript private members in workflow mode.
  • Document the new optimization and publish a patch changeset.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 5 comments.

Show a summary per file
FileDescription
packages/swc-plugin-workflow/transform/src/lib.rsAdds private-member reference collection + DCE pass after "use step" stripping.
packages/swc-plugin-workflow/transform/tests/fixture/private-member-dce/input.tsNew fixture input covering cascading TS private elimination.
packages/swc-plugin-workflow/transform/tests/fixture/private-member-dce/output-workflow.jsExpected workflow output with TS-private members removed and imports dropped.
packages/swc-plugin-workflow/transform/tests/fixture/private-member-dce/output-step.jsExpected step-mode output (private members retained).
packages/swc-plugin-workflow/transform/tests/fixture/private-member-dce/output-client.jsExpected client-mode output (private members retained).
packages/swc-plugin-workflow/spec.mdDocuments the private-member DCE behavior and example.
.changeset/private-member-dce.mdDeclares a patch release for the SWC plugin change.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +619 to +624
// TS private or any member: `this.foo` — only track when
// the object is `this` (to avoid false positives from
// unrelated member accesses like `obj.foo`)
MemberProp::Ident(ident) => {
if matches!(&*expr.obj, Expr::This(_)) {
self.referenced.insert(ident.sym.to_string());

CopilotAIApr 9, 2026

Copy link

Choose a reason for hiding this comment

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

ClassMemberRefCollector only records MemberProp::Ident accesses when the object is this. In TypeScript, private members are also legally accessed via other instances of the same class (e.g. static compare(a: C, b: C) { return a.x - b.x }). With the current check, those references won’t be collected and the private member can be incorrectly removed, breaking runtime behavior. Consider treating any .ident access as a reference when ident matches a known TS-private member name in the class, regardless of the receiver expression (safe, though more conservative).

Suggested change
// TS private or any member: `this.foo` — only track when
// the object is `this` (to avoid false positives from
// unrelated member accesses like `obj.foo`)
MemberProp::Ident(ident) => {
ifmatches!(&*expr.obj,Expr::This(_)){
self.referenced.insert(ident.sym.to_string());
// TS private or any member access. Track `this.foo` as before,
// and also track `obj.foo` when `foo` is a known TS-private
// member of the current class so same-class accesses like
// `a.x` / `b.x` are not missed.
MemberProp::Ident(ident) => {
let name = ident.sym.to_string();
ifmatches!(&*expr.obj,Expr::This(_)) || self.private_names.contains(&name){
self.referenced.insert(name);

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Fixed. The collector now holds a ts_private_names: HashSet<String> built from the class body. MemberProp::Ident accesses are tracked when the name is in that set, regardless of the receiver expression. This catches a.x / b.x patterns in static methods.

Comment on lines +629 to +630
// Continue visiting children
expr.obj.visit_with(self);

CopilotAIApr 9, 2026

Copy link

Choose a reason for hiding this comment

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

visit_member_expr overrides default traversal but only visits expr.obj. For computed member expressions (obj[expr]), the property expression is not visited, so any this.#x / this.x references inside the computed key won’t be collected and could cause incorrectly eliminating private members. Use expr.visit_children_with(self) (or explicitly visit the computed property expression) to preserve full traversal.

Suggested change
// Continue visiting children
expr.obj.visit_with(self);
// Continue visiting children, including computed property expressions.
expr.visit_children_with(self);

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Fixed. Changed to expr.visit_children_with(self) which visits both the object and property expressions, including computed keys.

Comment on lines +614 to +625
match &expr.prop {
// Native JS private: `this.#foo`
MemberProp::PrivateName(name) => {
self.referenced.insert(name.name.to_string());
}
// TS private or any member: `this.foo` — only track when
// the object is `this` (to avoid false positives from
// unrelated member accesses like `obj.foo`)
MemberProp::Ident(ident) => {
if matches!(&*expr.obj, Expr::This(_)) {
self.referenced.insert(ident.sym.to_string());
}

CopilotAIApr 9, 2026

Copy link

Choose a reason for hiding this comment

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

Native private #foo and public/TS member foo are both stored as the same string ("foo"). If a class contains both foo and #foo, a this.foo access will incorrectly mark #foo as referenced, preventing intended elimination. Consider encoding JS-private names distinctly (e.g. prefix with # or store an enum) so #foo and foo don’t collide.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Fixed. JS native private names are now stored with a # prefix (e.g. "#foo") while TS private names remain unprefixed ("foo"). private_member_name() returns the prefixed form for PrivateMethod/PrivateProp, and visit_member_expr inserts format!("#{}", name) for MemberProp::PrivateName.

Comment on lines +1 to +25
import { WORKFLOW_SERIALIZE, WORKFLOW_DESERIALIZE } from '@workflow/serde';
import { getWorld } from './world.js';
import { importKey } from './encryption.js';

export class Run {
static [WORKFLOW_SERIALIZE](instance: Run) {
return { id: instance.id };
}

static [WORKFLOW_DESERIALIZE](data: { id: string }) {
return new Run(data.id);
}

id: string;

// TS private field — only referenced by stripped methods
private encryptionKeyPromise: Promise<any> | null = null;

// TS private method — only called by stripped getters/methods
private async getEncryptionKey(): Promise<any> {
if (!this.encryptionKeyPromise) {
this.encryptionKeyPromise = importKey(this.id);
}
return this.encryptionKeyPromise;
}

CopilotAIApr 9, 2026

Copy link

Choose a reason for hiding this comment

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

The new DCE logic claims to support native JS private members (#field/#method()), but the fixture only covers TypeScript private. Adding a fixture that uses #private syntax (including a cascading case) would help prevent regressions in the JS-private path.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Added in the second commit: private-member-dce-native/input.js tests JS native #field and #method() DCE including cascading elimination (#encryptionKeyPromise eliminated because #getEncryptionKey is eliminated) and survival of referenced members (#label kept because toString() references it).

Comment on lines +8504 to +8510
// Dead-code-eliminate unreferenced private members
// (same logic as visit_mut_class_decl above)
let referenced =
ClassMemberRefCollector::collect_from_class_body(&class_expr.class.body);
class_expr.class.body.retain(|member| match member {
ClassMember::PrivateMethod(m) => referenced.contains(&m.key.name.to_string()),
ClassMember::PrivateProp(p) => referenced.contains(&p.key.name.to_string()),

CopilotAIApr 9, 2026

Copy link

Choose a reason for hiding this comment

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

The private-member retention filter is duplicated in both visit_mut_class_decl and visit_mut_class_expr. This duplication increases the risk of future divergence/bugs when the logic changes. Consider extracting the retain logic into a shared helper (e.g. fn retain_referenced_private_members(body: &mut Vec<ClassMember>)) used by both visitors.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Fixed. Extracted retain_referenced_private_members(body: &mut Vec<ClassMember>) on ClassMemberRefCollector, called from both visit_mut_class_decl and visit_mut_class_expr.

- Namespace JS native private names with # prefix to avoid collisions
with TS private members of the same name
- Track TS private member accesses on non-this receivers (e.g. a.x in
static methods) by maintaining a set of known TS-private names
- Use visit_children_with for full traversal including computed member
expressions
- Extract retain logic into shared retain_referenced_private_members()
helper used by both visit_mut_class_decl and visit_mut_class_expr

@VaguelySeriousVaguelySerious left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

AI review: no blocking issues

// accesses like `a.x` / `b.x` are not missed.
MemberProp::Ident(ident) => {
let name = ident.sym.to_string();
if matches!(&*expr.obj, Expr::This(_)) || self.ts_private_names.contains(&name) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

AI Review: Note

For MemberProp::Ident, any this.foo access records foo in referenced, not only TS-private members. Retain still keys off private_member_name, so this is mostly extra churn in the set rather than incorrect retention, but it is slightly less precise than filtering to private names only.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Correct - the collector is conservative: it records all this.foo accesses, not just those matching known private names. This is intentional for simplicity. The extra entries in the referenced set are harmless since retain only removes members that private_member_name() identifies as private. Public members are always kept regardless of what's in the set.

});
```

This optimization is critical for SDK classes like `Run` where private helper methods reference Node.js-only imports (encryption, world access, etc.) — eliminating them allows the downstream module-level DCE to also remove those imports from the workflow bundle.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

AI Review: Nit

This section explains the algorithm well. If you want to reduce future issue reports, a short explicit note on unsupported patterns (for example computed this[...] access to private fields) could help set expectations.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Good suggestion. Computed access like this[name] where name happens to be a private member name won't be tracked, which could lead to incorrect elimination. In practice this pattern is very rare for private members (especially in SDK code like Run), but worth noting if we want to document limitations.

@TooTallNate
TooTallNate enabled auto-merge (squash) April 9, 2026 19:22
@TooTallNate
TooTallNate merged commit 66585fd into mainApr 9, 2026
102 of 103 checks passed
@TooTallNate
TooTallNate deleted the fix/swc-private-member-dce branch April 9, 2026 19:27
@ghostghost mentioned this pull request Apr 9, 2026
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

@TooTallNate@VaguelySerious
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(swc): dead code eliminate unreferenced private class members in workflow mode - #1671

Merged
TooTallNate merged 3 commits into
mainfrom
fix/swc-private-member-dce
Apr 9, 2026
Merged

feat(swc): dead code eliminate unreferenced private class members in workflow mode#1671
TooTallNate merged 3 commits into
mainfrom
fix/swc-private-member-dce

Conversation

@TooTallNate

@TooTallNateTooTallNate commented Apr 9, 2026

Copy link
Copy Markdown
Member

Summary

After stripping "use step" methods from a class body in workflow mode, eliminate private members that are no longer referenced by any remaining public member. This applies to both:

  • JS native private: #field, #method()
  • TypeScript private: private field, private method()

The algorithm is iterative — references are seeded from public members, then expanded through surviving private members until a fixed point, enabling cascading elimination.

Example

Input:

exportclassRun{static[WORKFLOW_SERIALIZE](instance){return{id: instance.id};}static[WORKFLOW_DESERIALIZE](data){returnnewRun(data.id);}id: string;privateencryptionKeyPromise: Promise<any>|null=null;privateasyncgetEncryptionKey(){if(!this.encryptionKeyPromise){this.encryptionKeyPromise=importKey(this.id);}returnthis.encryptionKeyPromise;}constructor(id: string){this.id=id;}getvalue(): Promise<any>{'use step';returnthis.getEncryptionKey().then(()=>getWorld().get(this.id));}asynccancel(): Promise<void>{'use step';constkey=awaitthis.getEncryptionKey();awaitgetWorld().cancel(this.id,key);}toString(): string{return`Run(${this.id})`;}}

Workflow mode output:

exportclassRun{static[WORKFLOW_SERIALIZE](instance){return{id: instance.id};}static[WORKFLOW_DESERIALIZE](data){returnnewRun(data.id);}id;// ✅ private encryptionKeyPromise — ELIMINATED (only referenced by getEncryptionKey)// ✅ private getEncryptionKey() — ELIMINATED (only referenced by stripped step methods)constructor(id){this.id=id;}toString(){return`Run(${this.id})`;}}Run.prototype["cancel"]=globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//...");var__step_Run$value=globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//...");Object.defineProperty(Run.prototype,"value",{get(){return__step_Run$value.call(this);},configurable: true,enumerable: false});

The cascading elimination is key: encryptionKeyPromise is only referenced by getEncryptionKey(), which is itself only referenced by the stripped value getter and cancel() method. Both private members are removed, allowing the downstream module-level DCE to also eliminate the importKey and getWorld imports.

Motivation

SDK classes like Run have private helper methods that reference Node.js-only imports (encryption, world access). Without this optimization, those helpers survive into the workflow bundle even though nothing calls them after "use step" bodies are stripped, keeping the Node.js imports alive and preventing tree-shaking.

Test

New fixture: private-member-dce/input.ts with expected outputs for all three modes.

…workflow mode
After stripping 'use step' methods from a class body in workflow mode,
eliminate private members (both JS native #field/#method and TypeScript
private field/private method) that are no longer referenced by any
remaining public member.
The algorithm is iterative: references are seeded from public members,
then expanded through surviving private members until a fixed point,
enabling cascading elimination (e.g. a private field only referenced by
a private method that is itself unreferenced).
CopilotAI review requested due to automatic review settings April 9, 2026 18:09
@TooTallNate
TooTallNate requested a review from a team as a code ownerApril 9, 2026 18:09
@changeset-bot

changeset-botBot commented Apr 9, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 4ea576a

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

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

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 Apr 9, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Apr 9, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production912067979
✅ 💻 Local Development88601821068
✅ 📦 Local Production88601821068
✅ 🐘 Local Postgres88601821068
✅ 🪟 Windows810889
❌ 🌍 Community Worlds1366524225
✅ 📋 Other225042267
Total4012656874764

❌ Failed Tests

🌍 Community Worlds (65 failed)

mongodb (4 failed):

  • hookWorkflow is not resumable via public webhook endpoint | wrun_01KNSR613X7J6NJ3Q7CC1GAXC8
  • webhookWorkflow | wrun_01KNSR6A2N8E4FRA7K4W0WAJ2X
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously | wrun_01KNSRE4P2HFBR1HGDZ60FSZ4A
  • resilient start: addTenWorkflow completes when run_created returns 500 | wrun_01KNSRMTFPHDMEWW1M0C99YMJB

redis (3 failed):

  • hookWorkflow is not resumable via public webhook endpoint | wrun_01KNSR613X7J6NJ3Q7CC1GAXC8
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously | wrun_01KNSRE4P2HFBR1HGDZ60FSZ4A
  • resilient start: addTenWorkflow completes when run_created returns 500 | wrun_01KNSRMTFPHDMEWW1M0C99YMJB

turso (58 failed):

  • addTenWorkflow | wrun_01KNSR4Q12NYS08Q5M8JMBJE2P
  • addTenWorkflow | wrun_01KNSR4Q12NYS08Q5M8JMBJE2P
  • wellKnownAgentWorkflow (.well-known/agent) | wrun_01KNSR6H7RBB0VN5AF50A8K3HK
  • should work with react rendering in step
  • promiseAllWorkflow | wrun_01KNSR50EQH830PRD40KS32Q9Y
  • promiseRaceWorkflow | wrun_01KNSR54Y75RYQDBMAWK470VYP
  • promiseAnyWorkflow | wrun_01KNSR58C03DCWT8KGXWBMNCP6
  • importedStepOnlyWorkflow | wrun_01KNSR6WGHHN8XTHMJ86Q1JS10
  • hookWorkflow | wrun_01KNSR5M9AWG051SF57JDHTN15
  • hookWorkflow is not resumable via public webhook endpoint | wrun_01KNSR613X7J6NJ3Q7CC1GAXC8
  • webhookWorkflow | wrun_01KNSR6A2N8E4FRA7K4W0WAJ2X
  • sleepingWorkflow | wrun_01KNSR6G7XQP06VPNVA5S5SZ7P
  • parallelSleepWorkflow | wrun_01KNSR6W68GVQAECF6EXH5YKW4
  • nullByteWorkflow | wrun_01KNSR6ZZHZA6GX9A995NWGH4W
  • workflowAndStepMetadataWorkflow | wrun_01KNSR722J4AE22DPWYBGR40AA
  • fetchWorkflow | wrun_01KNSR9QTD0RQEWYMKGRM9P6KS
  • promiseRaceStressTestWorkflow | wrun_01KNSR9V7MWTQB99MYWGDBMPV1
  • error handling error propagation workflow errors nested function calls preserve message and stack trace
  • error handling error propagation workflow errors cross-file imports preserve message and stack trace
  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior FatalError fails immediately without retries
  • error handling retry behavior RetryableError respects custom retryAfter delay
  • error handling retry behavior maxRetries=0 disables retries
  • error handling catchability FatalError can be caught and detected with FatalError.is()
  • error handling not registered WorkflowNotRegisteredError fails the run when workflow does not exist
  • error handling not registered StepNotRegisteredError fails the step but workflow can catch it
  • error handling not registered StepNotRegisteredError fails the run when not caught in workflow
  • hookCleanupTestWorkflow - hook token reuse after workflow completion | wrun_01KNSRDFYWY5RDPW5MJW4M2RKP
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously | wrun_01KNSRE4P2HFBR1HGDZ60FSZ4A
  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running | wrun_01KNSREVJW6G5G63ZYP9DTG2CT
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars) | wrun_01KNSRFFZ3HX99QC8VH9X00X19
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument | wrun_01KNSRFS1Y9106Z5PVYGW5SZ8F
  • closureVariableWorkflow - nested step functions with closure variables | wrun_01KNSRFYM8QZ6MKBWEXNSXEA1E
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step | wrun_01KNSRG1102E8CS370B44Y7JJ1
  • health check (queue-based) - workflow and step endpoints respond to health check messages
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly | wrun_01KNSRGH373GH51TWFMF3M6THG
  • Calculator.calculate - static workflow method using static step methods from another class | wrun_01KNSRGPSM4VSQ1R3Z11NG4MZ0
  • AllInOneService.processNumber - static workflow method using sibling static step methods | wrun_01KNSRGXK317X0HN019FNHP541
  • ChainableService.processWithThis - static step methods using this to reference the class | wrun_01KNSRH5K54HKGJC6TW5YDC6P4
  • thisSerializationWorkflow - step function invoked with .call() and .apply() | wrun_01KNSRHCE7T09PPS45FA7D75MK
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE | wrun_01KNSRHM1A0G2BFRKSN1893XHR
  • instanceMethodStepWorkflow - instance methods with "use step" directive | wrun_01KNSRHTZQJP99YC0M9FNWQPM5
  • crossContextSerdeWorkflow - classes defined in step code are deserializable in workflow context | wrun_01KNSRJ68YGFBGXJ7YFR7ZHMV5
  • stepFunctionAsStartArgWorkflow - step function reference passed as start() argument | wrun_01KNSRJFBR9XRH3012W64TDAJ1
  • cancelRun - cancelling a running workflow | wrun_01KNSRJP4YQBW4BDB9ZFHE3JXZ
  • cancelRun via CLI - cancelling a running workflow | wrun_01KNSRK06K5725SZWQJ2DJZZ1K
  • pages router addTenWorkflow via pages router
  • pages router promiseAllWorkflow via pages router
  • pages router sleepingWorkflow via pages router
  • hookWithSleepWorkflow - hook payloads delivered correctly with concurrent sleep | wrun_01KNSRKDA53YRF17ST7YTD02BW
  • sleepInLoopWorkflow - sleep inside loop with steps actually delays each iteration | wrun_01KNSRM3T179X2E69VG894KAX9
  • sleepWithSequentialStepsWorkflow - sequential steps work with concurrent sleep (control) | wrun_01KNSRME2NVXNSBMBR8AND2DKJ
  • importMetaUrlWorkflow - import.meta.url is available in step bundles | wrun_01KNSRMP16CE789X5BG39DBJEA
  • metadataFromHelperWorkflow - getWorkflowMetadata/getStepMetadata work from module-level helper (#1577) | wrun_01KNSRMR5GRYXW87SN5A0AM9TV
  • resilient start: addTenWorkflow completes when run_created returns 500 | wrun_01KNSRMTFPHDMEWW1M0C99YMJB
  • getterStepWorkflow - getter functions with "use step" directive | wrun_01KNSRMXQTQ6GKDFR5NN8DMJRA

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro8207
✅ example8207
✅ express8207
✅ fastify8207
✅ hono8207
✅ nextjs-turbopack8702
✅ nextjs-webpack8702
✅ nitro8207
✅ nuxt8207
✅ sveltekit8207
✅ vite8207
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable75014
✅ express-stable75014
✅ fastify-stable75014
✅ hono-stable75014
✅ nextjs-turbopack-canary62027
✅ nextjs-turbopack-stable8108
✅ nextjs-webpack-canary62027
✅ nextjs-webpack-stable8108
✅ nitro-stable75014
✅ nuxt-stable75014
✅ sveltekit-stable75014
✅ vite-stable75014
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable75014
✅ express-stable75014
✅ fastify-stable75014
✅ hono-stable75014
✅ nextjs-turbopack-canary62027
✅ nextjs-turbopack-stable8108
✅ nextjs-webpack-canary62027
✅ nextjs-webpack-stable8108
✅ nitro-stable75014
✅ nuxt-stable75014
✅ sveltekit-stable75014
✅ vite-stable75014
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable75014
✅ express-stable75014
✅ fastify-stable75014
✅ hono-stable75014
✅ nextjs-turbopack-canary62027
✅ nextjs-turbopack-stable8108
✅ nextjs-webpack-canary62027
✅ nextjs-webpack-stable8108
✅ nitro-stable75014
✅ nuxt-stable75014
✅ sveltekit-stable75014
✅ vite-stable75014
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack8108
❌ 🌍 Community Worlds
AppPassedFailedSkipped
✅ mongodb-dev500
❌ mongodb5848
✅ redis-dev500
❌ redis5938
✅ turso-dev500
❌ turso4588
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable75014
✅ e2e-local-postgres-nest-stable75014
✅ e2e-local-prod-nest-stable75014

📋 View full workflow run

@github-actions

github-actionsBot commented Apr 9, 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🥇 Express0.043s (-1.6%)1.006s (~)0.962s101.00x
💻 LocalNitro0.044s (-4.8%)1.005s (~)0.961s101.01x
🐘 PostgresExpress0.046s (-22.5% 🟢)1.011s (~)0.965s101.07x
💻 LocalNext.js (Turbopack)0.049s (+3.6%)1.005s (~)0.957s101.12x
🌐 RedisNext.js (Turbopack)0.050s (-11.3% 🟢)1.005s (~)0.955s101.16x
🐘 PostgresNext.js (Turbopack)0.058s (-3.0%)1.011s (~)0.954s101.33x
🐘 PostgresNitro0.063s (+2.3%)1.012s (~)0.949s101.46x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express0.231s (-39.8% 🟢)2.203s (-10.5% 🟢)1.972s101.00x
▲ VercelNext.js (Turbopack)0.267s (-33.8% 🟢)2.195s (-8.4% 🟢)1.928s101.16x
▲ VercelNitro0.304s (+10.2% 🔺)2.211s (+4.0%)1.907s101.32x

🔍 Observability: Express | Next.js (Turbopack) | Nitro

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.119s (-3.1%)2.010s (~)0.891s101.00x
🌐 RedisNext.js (Turbopack)1.126s (~)2.007s (~)0.881s101.01x
💻 LocalNext.js (Turbopack)1.126s (~)2.006s (~)0.880s101.01x
💻 LocalNitro1.128s (~)2.005s (~)0.877s101.01x
💻 LocalExpress1.131s (~)2.006s (~)0.875s101.01x
🐘 PostgresNext.js (Turbopack)1.147s (~)2.009s (~)0.862s101.02x
🐘 PostgresNitro1.147s (-0.6%)2.011s (~)0.864s101.02x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.911s (+1.0%)3.886s (+0.8%)1.975s101.00x
▲ VercelNitro2.003s (+6.5% 🔺)3.356s (-10.3% 🟢)1.353s101.05x
▲ VercelNext.js (Turbopack)2.103s (+4.0%)3.881s (+3.0%)1.778s101.10x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express10.694s (-2.1%)11.028s (~)0.334s31.00x
🌐 RedisNext.js (Turbopack)10.779s (+0.7%)11.023s (~)0.244s31.01x
💻 LocalNext.js (Turbopack)10.831s (~)11.025s (~)0.194s31.01x
🐘 PostgresNext.js (Turbopack)10.849s (~)11.023s (~)0.174s31.01x
💻 LocalNitro10.920s (~)11.023s (~)0.103s31.02x
🐘 PostgresNitro10.934s (~)11.023s (~)0.088s31.02x
💻 LocalExpress10.952s (~)11.024s (~)0.072s31.02x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express16.776s (~)18.801s (-1.4%)2.025s21.00x
▲ VercelNext.js (Turbopack)17.133s (-0.8%)19.157s (~)2.024s21.02x
▲ VercelNitro17.627s (-1.4%)18.953s (-5.7% 🟢)1.326s21.05x

🔍 Observability: Express | Next.js (Turbopack) | Nitro

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express14.019s (-3.8%)14.627s (-2.6%)0.608s51.00x
🌐 RedisNext.js (Turbopack)14.257s (+0.9%)15.029s (~)0.772s41.02x
🐘 PostgresNext.js (Turbopack)14.412s (-0.6%)15.023s (~)0.612s41.03x
🐘 PostgresNitro14.702s (~)15.021s (~)0.319s41.05x
💻 LocalNext.js (Turbopack)14.709s (+1.0%)15.030s (~)0.322s41.05x
💻 LocalNitro14.924s (-0.6%)15.028s (-3.2%)0.104s41.06x
💻 LocalExpress14.989s (~)15.030s (~)0.041s41.07x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express33.420s (-3.4%)35.676s (-3.4%)2.256s21.00x
▲ VercelNext.js (Turbopack)34.176s (-3.3%)36.300s (-3.0%)2.124s21.02x
▲ VercelNitro34.324s (+0.8%)35.857s (-1.2%)1.533s21.03x

🔍 Observability: Express | Next.js (Turbopack) | Nitro

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express12.957s (-8.6% 🟢)13.021s (-13.3% 🟢)0.064s71.00x
🌐 RedisNext.js (Turbopack)13.389s (+1.2%)14.025s (~)0.636s71.03x
🐘 PostgresNext.js (Turbopack)13.757s (~)14.019s (~)0.262s71.06x
🐘 PostgresNitro14.192s (-0.9%)15.022s (~)0.829s61.10x
💻 LocalNext.js (Turbopack)16.033s (+2.8%)16.531s (+3.1%)0.498s61.24x
💻 LocalExpress16.441s (-0.7%)17.032s (~)0.591s61.27x
💻 LocalNitro16.531s (~)17.029s (~)0.498s61.28x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)56.887s (-2.0%)58.539s (-2.7%)1.652s21.00x
▲ VercelExpress59.072s (+7.5% 🔺)61.603s (+7.7% 🔺)2.531s21.04x
▲ VercelNitro59.188s (+2.0%)60.723s (~)1.535s21.04x

🔍 Observability: Next.js (Turbopack) | Express | Nitro

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.211s (-8.6% 🟢)2.009s (~)0.798s151.00x
🐘 PostgresNext.js (Turbopack)1.232s (~)2.009s (~)0.777s151.02x
🐘 PostgresNitro1.260s (~)2.009s (~)0.749s151.04x
🌐 RedisNext.js (Turbopack)1.318s (+6.2% 🔺)2.006s (~)0.688s151.09x
💻 LocalExpress1.515s (+0.8%)2.006s (~)0.491s151.25x
💻 LocalNitro1.516s (+2.1%)2.005s (~)0.490s151.25x
💻 LocalNext.js (Turbopack)1.544s (+4.6%)2.006s (~)0.461s151.27x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.253s (-6.7% 🟢)4.180s (+4.1%)1.927s81.00x
▲ VercelNitro2.496s (-15.3% 🟢)3.670s (-19.6% 🟢)1.174s91.11x
▲ VercelNext.js (Turbopack)2.719s (+23.5% 🔺)4.105s (+6.9% 🔺)1.386s81.21x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.300s (-2.9%)3.009s (~)0.708s101.00x
🐘 PostgresNitro2.330s (+0.7%)3.009s (~)0.680s101.01x
🐘 PostgresNext.js (Turbopack)2.385s (-0.7%)3.009s (~)0.624s101.04x
🌐 RedisNext.js (Turbopack)2.564s (+5.3% 🔺)3.008s (~)0.444s101.11x
💻 LocalExpress2.895s (+1.4%)3.453s (+3.3%)0.558s91.26x
💻 LocalNitro2.991s (+6.5% 🔺)3.341s (+11.1% 🔺)0.351s91.30x
💻 LocalNext.js (Turbopack)3.077s (+24.5% 🔺)3.884s (+29.1% 🔺)0.808s81.34x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.670s (-10.7% 🟢)4.008s (-14.7% 🟢)1.338s81.00x
▲ VercelNext.js (Turbopack)2.780s (+3.0%)4.402s (-1.8%)1.622s71.04x
▲ VercelExpress2.879s (+4.4%)4.437s (+2.9%)1.558s71.08x

🔍 Observability: Nitro | Next.js (Turbopack) | Express

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express3.383s (-2.3%)4.011s (~)0.628s81.00x
🐘 PostgresNitro3.472s (-0.5%)4.011s (~)0.538s81.03x
🐘 PostgresNext.js (Turbopack)3.643s (-1.1%)4.009s (~)0.367s81.08x
🌐 RedisNext.js (Turbopack)4.164s (+5.3% 🔺)5.011s (+16.6% 🔺)0.847s61.23x
💻 LocalExpress7.500s (-8.6% 🟢)8.021s (-8.5% 🟢)0.521s42.22x
💻 LocalNitro8.022s (+1.6%)8.773s (+6.1% 🔺)0.752s42.37x
💻 LocalNext.js (Turbopack)8.384s (+47.2% 🔺)9.019s (+50.0% 🔺)0.635s42.48x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.054s (-1.0%)5.367s (+9.7% 🔺)2.313s61.00x
▲ VercelNitro3.295s (+8.8% 🔺)4.508s (-7.1% 🟢)1.213s71.08x
▲ VercelNext.js (Turbopack)3.678s (+1.9%)5.769s (+8.0% 🔺)2.091s61.20x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.204s (-4.3%)2.008s (~)0.804s151.00x
🐘 PostgresNext.js (Turbopack)1.251s (+2.1%)2.009s (~)0.758s151.04x
🐘 PostgresNitro1.264s (~)2.008s (~)0.744s151.05x
🌐 RedisNext.js (Turbopack)1.278s (-0.6%)2.006s (~)0.729s151.06x
💻 LocalExpress1.519s (~)2.006s (~)0.487s151.26x
💻 LocalNitro1.524s (-3.4%)2.006s (~)0.482s151.27x
💻 LocalNext.js (Turbopack)1.556s (~)2.007s (-3.2%)0.451s151.29x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.003s (-4.2%)3.908s (-1.8%)1.905s81.00x
▲ VercelNitro2.006s (-7.6% 🟢)3.636s (-6.7% 🟢)1.630s91.00x
▲ VercelNext.js (Turbopack)2.217s (+6.1% 🔺)3.745s (+5.0% 🔺)1.529s91.11x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro2.329s (~)3.010s (~)0.681s101.00x
🐘 PostgresExpress2.332s (-0.9%)3.008s (~)0.676s101.00x
🐘 PostgresNext.js (Turbopack)2.390s (~)3.011s (~)0.620s101.03x
🌐 RedisNext.js (Turbopack)2.543s (+3.6%)3.007s (~)0.464s101.09x
💻 LocalNext.js (Turbopack)2.807s (-10.8% 🟢)3.341s (-6.3% 🟢)0.533s91.21x
💻 LocalExpress2.938s (-0.9%)3.454s (-3.1%)0.516s91.26x
💻 LocalNitro2.990s (-1.3%)3.677s (-2.2%)0.687s91.28x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.320s (~)4.314s (+9.6% 🔺)1.994s81.00x
▲ VercelNitro2.485s (-3.3%)3.963s (-5.7% 🟢)1.478s81.07x
▲ VercelNext.js (Turbopack)2.779s (+9.3% 🔺)4.314s (+3.7%)1.535s71.20x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro3.453s (~)4.010s (~)0.557s81.00x
🐘 PostgresExpress3.457s (~)4.009s (~)0.553s81.00x
🐘 PostgresNext.js (Turbopack)3.646s (~)4.009s (~)0.363s81.06x
🌐 RedisNext.js (Turbopack)4.169s (+3.5%)5.012s (+9.4% 🔺)0.843s61.21x
💻 LocalExpress8.033s (-6.6% 🟢)8.774s (-2.8%)0.741s42.33x
💻 LocalNitro8.669s (+2.3%)9.026s (~)0.357s42.51x
💻 LocalNext.js (Turbopack)8.695s (+12.2% 🔺)9.271s (+12.1% 🔺)0.576s42.52x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.728s (-4.1%)4.288s (-0.9%)1.560s71.00x
▲ VercelExpress3.170s (+16.4% 🔺)4.654s (+9.8% 🔺)1.484s71.16x
▲ VercelNext.js (Turbopack)3.271s (-13.0% 🟢)5.128s (-7.0% 🟢)1.857s61.20x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.608s (-26.8% 🟢)1.006s (~)0.399s601.00x
🌐 RedisNext.js (Turbopack)0.708s (+11.4% 🔺)1.004s (~)0.296s601.16x
🐘 PostgresNext.js (Turbopack)0.819s (+6.2% 🔺)1.041s (+1.7%)0.222s581.35x
🐘 PostgresNitro0.858s (+2.2%)1.023s (+1.7%)0.165s591.41x
💻 LocalNext.js (Turbopack)0.860s (-2.7%)1.005s (-3.3%)0.145s601.41x
💻 LocalNitro0.980s (-5.4% 🟢)1.115s (-44.4% 🟢)0.135s541.61x
💻 LocalExpress1.016s (+4.1%)1.654s (+48.3% 🔺)0.638s371.67x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro8.903s (-8.6% 🟢)10.501s (-8.5% 🟢)1.599s61.00x
▲ VercelNext.js (Turbopack)9.632s (-23.0% 🟢)11.579s (-17.7% 🟢)1.948s61.08x
▲ VercelExpress10.403s (+2.7%)12.297s (+1.4%)1.893s51.17x

🔍 Observability: Nitro | Next.js (Turbopack) | Express

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.477s (-26.2% 🟢)2.007s (-16.4% 🟢)0.530s451.00x
🌐 RedisNext.js (Turbopack)1.672s (+15.4% 🔺)2.006s (~)0.333s451.13x
🐘 PostgresNitro2.078s (+3.4%)2.944s (+16.1% 🔺)0.866s311.41x
🐘 PostgresNext.js (Turbopack)2.153s (+14.8% 🔺)2.821s (+37.5% 🔺)0.668s321.46x
💻 LocalNext.js (Turbopack)2.700s (~)3.008s (~)0.308s301.83x
💻 LocalNitro3.022s (-1.1%)3.547s (-9.6% 🟢)0.525s262.05x
💻 LocalExpress3.162s (+3.6%)3.967s (+4.3%)0.805s232.14x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro28.495s (-3.9%)30.408s (-3.5%)1.913s31.00x
▲ VercelExpress28.833s (-1.1%)30.922s (-1.2%)2.089s31.01x
▲ VercelNext.js (Turbopack)29.626s (-0.8%)31.356s (-2.6%)1.730s31.04x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express3.109s (-23.3% 🟢)3.790s (-20.7% 🟢)0.681s321.00x
🌐 RedisNext.js (Turbopack)3.352s (+11.2% 🔺)4.008s (+15.7% 🔺)0.656s301.08x
🐘 PostgresNext.js (Turbopack)3.994s (+5.8% 🔺)4.331s (+8.0% 🔺)0.337s281.28x
🐘 PostgresNitro4.191s (~)4.932s (-1.6%)0.741s251.35x
💻 LocalNext.js (Turbopack)8.691s (+2.0%)9.019s (~)0.328s142.80x
💻 LocalExpress9.039s (-1.3%)9.633s (-1.6%)0.594s132.91x
💻 LocalNitro9.104s (+2.1%)9.710s (+3.3%)0.606s132.93x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express73.513s (-14.6% 🟢)75.368s (-14.2% 🟢)1.855s21.00x
▲ VercelNitro75.626s (+6.1% 🔺)77.847s (+6.5% 🔺)2.221s21.03x
▲ VercelNext.js (Turbopack)77.885s (-4.4%)79.559s (-4.2%)1.675s21.06x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.226s (-18.4% 🟢)1.007s (~)0.781s601.00x
🐘 PostgresNext.js (Turbopack)0.251s (+4.8%)1.007s (~)0.757s601.11x
🐘 PostgresNitro0.281s (-3.7%)1.007s (~)0.725s601.24x
🌐 RedisNext.js (Turbopack)0.284s (+7.7% 🔺)1.004s (~)0.720s601.26x
💻 LocalNitro0.588s (+4.9%)1.004s (~)0.417s602.60x
💻 LocalNext.js (Turbopack)0.620s (+20.2% 🔺)1.022s (+1.7%)0.402s592.74x
💻 LocalExpress0.697s (+18.7% 🔺)1.096s (+9.1% 🔺)0.398s553.08x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.544s (-22.1% 🟢)3.163s (-11.6% 🟢)1.620s201.00x
▲ VercelNext.js (Turbopack)1.688s (-5.7% 🟢)3.573s (+3.3%)1.886s181.09x
▲ VercelNitro1.727s (-8.8% 🟢)3.318s (-4.5%)1.591s191.12x

🔍 Observability: Express | Next.js (Turbopack) | Nitro

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.363s (-27.3% 🟢)1.006s (~)0.643s901.00x
🐘 PostgresNitro0.493s (-1.8%)1.007s (~)0.514s901.36x
🐘 PostgresNext.js (Turbopack)0.508s (+4.6%)1.007s (~)0.499s901.40x
🌐 RedisNext.js (Turbopack)1.144s (+2.9%)2.006s (+17.7% 🔺)0.862s453.15x
💻 LocalExpress2.492s (-1.6%)3.009s (~)0.517s306.86x
💻 LocalNitro2.652s (+14.2% 🔺)3.180s (+5.7% 🔺)0.528s297.31x
💻 LocalNext.js (Turbopack)2.676s (+7.7% 🔺)3.076s (+2.2%)0.401s307.37x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.612s (-2.1%)4.132s (-6.5% 🟢)1.519s221.00x
▲ VercelExpress2.738s (-7.0% 🟢)4.568s (-3.6%)1.830s201.05x
▲ VercelNext.js (Turbopack)3.366s (-5.4% 🟢)4.890s (-7.9% 🟢)1.524s191.29x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.583s (-26.2% 🟢)1.007s (~)0.423s1201.00x
🐘 PostgresNitro0.809s (-0.9%)1.009s (~)0.200s1191.39x
🐘 PostgresNext.js (Turbopack)0.847s (+10.4% 🔺)1.043s (+3.6%)0.196s1161.45x
🌐 RedisNext.js (Turbopack)2.677s (-3.5%)3.007s (-0.8%)0.330s404.59x
💻 LocalNitro10.890s (+4.7%)11.485s (+4.1%)0.595s1118.67x
💻 LocalExpress10.935s (~)11.395s (-1.5%)0.460s1118.75x
💻 LocalNext.js (Turbopack)11.065s (+7.4% 🔺)11.572s (+7.4% 🔺)0.507s1118.97x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro6.908s (+9.6% 🔺)8.327s (+3.7%)1.419s151.00x
▲ VercelNext.js (Turbopack)47.028s (+673.5% 🔺)48.733s (+526.0% 🔺)1.705s66.81x
▲ VercelExpress49.426s (+616.1% 🔺)51.745s (+489.1% 🔺)2.320s77.15x

🔍 Observability: Nitro | Next.js (Turbopack) | Express

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.158s (-25.4% 🟢)1.001s (+0.5%)0.001s (-26.7% 🟢)1.009s (~)0.851s101.00x
💻 LocalNext.js (Turbopack)0.170s (+0.5%)1.002s (~)0.012s (+14.7% 🔺)1.017s (~)0.847s101.07x
🐘 PostgresNext.js (Turbopack)0.192s (+0.8%)1.001s (~)0.001s (-15.4% 🟢)1.010s (~)0.818s101.21x
💻 LocalNitro0.207s (-0.7%)1.004s (~)0.012s (+11.5% 🔺)1.017s (~)0.810s101.31x
💻 LocalExpress0.219s (+6.8% 🔺)1.004s (~)0.011s (-8.5% 🟢)1.017s (~)0.798s101.38x
🐘 PostgresNitro0.219s (+2.0%)0.996s (~)0.001s (+30.0% 🔺)1.012s (~)0.792s101.39x
🌐 RedisNext.js (Turbopack)0.242s (+62.9% 🔺)1.001s (~)0.032s (+1776.5% 🔺)1.037s (+3.0%)0.796s101.53x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)1.493s (-11.9% 🟢)2.833s (-11.4% 🟢)0.461s (-51.8% 🟢)3.696s (-20.0% 🟢)2.203s101.00x
▲ VercelExpress1.565s (+3.1%)2.942s (+1.1%)0.642s (-16.1% 🟢)4.033s (-2.6%)2.468s101.05x
▲ VercelNitro1.603s (-5.5% 🟢)2.899s (-11.0% 🟢)0.562s (+7.0% 🔺)3.812s (-9.8% 🟢)2.208s101.07x

🔍 Observability: Next.js (Turbopack) | Express | Nitro

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)0.481s (+14.5% 🔺)1.002s (~)0.003s (+4.8%)1.011s (~)0.530s601.00x
🐘 PostgresExpress0.522s (-16.5% 🟢)1.006s (~)0.004s (-10.2% 🟢)1.022s (~)0.500s591.09x
🐘 PostgresNitro0.629s (-2.0%)1.005s (~)0.004s (-11.2% 🟢)1.022s (~)0.393s591.31x
🐘 PostgresNext.js (Turbopack)0.659s (+9.3% 🔺)1.026s (+1.7%)0.008s (+110.1% 🔺)1.044s (+2.2%)0.385s581.37x
💻 LocalNext.js (Turbopack)0.763s (+13.8% 🔺)1.011s (~)0.009s (-15.6% 🟢)1.116s (+8.9% 🔺)0.353s541.59x
💻 LocalNitro0.815s (-14.6% 🟢)1.012s (~)0.010s (+2.4%)1.111s (-9.5% 🟢)0.296s571.70x
💻 LocalExpress0.847s (+18.5% 🔺)1.010s (~)0.010s (+6.7% 🔺)1.115s (+8.9% 🔺)0.268s541.76x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.092s (+1.5%)5.761s (~)0.242s (-2.7%)6.436s (-0.6%)2.344s101.00x
▲ VercelNext.js (Turbopack)4.186s (-6.5% 🟢)6.079s (-0.6%)0.220s (+9.9% 🔺)6.720s (~)2.534s91.02x
▲ VercelNitro4.499s (+6.2% 🔺)5.768s (+2.8%)0.457s (+71.8% 🔺)6.725s (+5.7% 🔺)2.226s91.10x

🔍 Observability: Express | Next.js (Turbopack) | Nitro

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)0.900s (+1.8%)1.035s (+3.5%)0.000s (+106.9% 🔺)1.039s (+3.4%)0.139s581.00x
🐘 PostgresExpress0.931s (-3.9%)1.146s (-5.9% 🟢)0.000s (+172.2% 🔺)1.159s (-6.0% 🟢)0.228s541.03x
🐘 PostgresNitro0.946s (~)1.220s (+12.0% 🔺)0.000s (-17.3% 🟢)1.232s (+10.3% 🔺)0.286s491.05x
🐘 PostgresNext.js (Turbopack)0.956s (+3.1%)1.177s (+5.6% 🔺)0.000s (+Infinity% 🔺)1.185s (+4.2%)0.229s511.06x
💻 LocalNitro1.218s (+2.4%)2.021s (~)0.000s (-7.1% 🟢)2.023s (~)0.805s301.35x
💻 LocalNext.js (Turbopack)1.256s (~)2.019s (~)0.000s (-11.1% 🟢)2.022s (~)0.767s301.40x
💻 LocalExpress1.444s (+17.6% 🔺)2.022s (~)0.001s (+14.8% 🔺)2.203s (+8.8% 🔺)0.758s281.60x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.826s (~)4.141s (+4.9%)0.000s (+150.0% 🔺)4.585s (+4.0%)1.759s141.00x
▲ VercelNitro2.860s (+7.4% 🔺)4.133s (+4.8%)0.000s (-50.0% 🟢)4.541s (+3.8%)1.681s141.01x
▲ VercelNext.js (Turbopack)3.188s (-28.9% 🟢)4.619s (-22.8% 🟢)0.000s (-100.0% 🟢)5.035s (-21.8% 🟢)1.847s121.13x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

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

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)1.728s (-1.9%)2.035s (+1.7%)0.000s (-50.0% 🟢)2.039s (+1.6%)0.311s301.00x
🐘 PostgresExpress1.739s (-1.1%)2.143s (+3.8%)0.000s (-65.5% 🟢)2.152s (+3.4%)0.413s281.01x
🐘 PostgresNitro1.789s (+2.3%)2.102s (~)0.000s (+Infinity% 🔺)2.113s (-1.0%)0.324s291.04x
🐘 PostgresNext.js (Turbopack)1.958s (+6.4% 🔺)2.147s (+0.6%)0.000s (+Infinity% 🔺)2.155s (~)0.197s281.13x
💻 LocalNitro3.410s (+6.4% 🔺)4.034s (+1.7%)0.001s (+140.0% 🔺)4.037s (+1.7%)0.627s151.97x
💻 LocalNext.js (Turbopack)3.725s (+3.2%)4.233s (+1.6%)0.000s (-12.5% 🟢)4.236s (+1.6%)0.511s152.16x
💻 LocalExpress3.802s (+9.2% 🔺)4.368s (+8.3% 🔺)0.001s (+233.3% 🔺)4.372s (+8.3% 🔺)0.570s152.20x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.969s (-25.3% 🟢)5.072s (-23.7% 🟢)0.000s (-62.5% 🟢)5.489s (-22.2% 🟢)1.521s121.00x
▲ VercelNext.js (Turbopack)3.988s5.498s0.000s5.941s1.953s111.01x
▲ VercelExpress5.598s (+32.4% 🔺)7.223s (+32.7% 🔺)0.000s (-31.3% 🟢)7.670s (+30.2% 🔺)2.072s81.41x

🔍 Observability: Nitro | Next.js (Turbopack) | Express

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNext.js (Turbopack)10/21
🐘 PostgresExpress19/21
▲ VercelExpress12/21
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express🐘 Postgres17/21
Next.js (Turbopack)🌐 Redis10/21
Nitro🐘 Postgres15/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)

📋 View full workflow run

CopilotAI 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.

Pull request overview

Adds a workflow-mode optimization to the SWC plugin so that after "use step" class members are stripped, any now-unreferenced private class members are also removed, improving downstream tree-shaking (e.g., dropping Node-only imports kept alive by unused helpers).

Changes:

  • Implement iterative private-member reachability analysis and removal after step-member stripping (class decls + class exprs).
  • Add a new transform fixture validating cascading elimination of TypeScript private members in workflow mode.
  • Document the new optimization and publish a patch changeset.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 5 comments.

Show a summary per file
FileDescription
packages/swc-plugin-workflow/transform/src/lib.rsAdds private-member reference collection + DCE pass after "use step" stripping.
packages/swc-plugin-workflow/transform/tests/fixture/private-member-dce/input.tsNew fixture input covering cascading TS private elimination.
packages/swc-plugin-workflow/transform/tests/fixture/private-member-dce/output-workflow.jsExpected workflow output with TS-private members removed and imports dropped.
packages/swc-plugin-workflow/transform/tests/fixture/private-member-dce/output-step.jsExpected step-mode output (private members retained).
packages/swc-plugin-workflow/transform/tests/fixture/private-member-dce/output-client.jsExpected client-mode output (private members retained).
packages/swc-plugin-workflow/spec.mdDocuments the private-member DCE behavior and example.
.changeset/private-member-dce.mdDeclares a patch release for the SWC plugin change.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +619 to +624
// TS private or any member: `this.foo` — only track when
// the object is `this` (to avoid false positives from
// unrelated member accesses like `obj.foo`)
MemberProp::Ident(ident) => {
if matches!(&*expr.obj, Expr::This(_)) {
self.referenced.insert(ident.sym.to_string());

CopilotAIApr 9, 2026

Copy link

Choose a reason for hiding this comment

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

ClassMemberRefCollector only records MemberProp::Ident accesses when the object is this. In TypeScript, private members are also legally accessed via other instances of the same class (e.g. static compare(a: C, b: C) { return a.x - b.x }). With the current check, those references won’t be collected and the private member can be incorrectly removed, breaking runtime behavior. Consider treating any .ident access as a reference when ident matches a known TS-private member name in the class, regardless of the receiver expression (safe, though more conservative).

Suggested change
// TS private or any member: `this.foo` — only track when
// the object is `this` (to avoid false positives from
// unrelated member accesses like `obj.foo`)
MemberProp::Ident(ident) => {
ifmatches!(&*expr.obj,Expr::This(_)){
self.referenced.insert(ident.sym.to_string());
// TS private or any member access. Track `this.foo` as before,
// and also track `obj.foo` when `foo` is a known TS-private
// member of the current class so same-class accesses like
// `a.x` / `b.x` are not missed.
MemberProp::Ident(ident) => {
let name = ident.sym.to_string();
ifmatches!(&*expr.obj,Expr::This(_)) || self.private_names.contains(&name){
self.referenced.insert(name);

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Fixed. The collector now holds a ts_private_names: HashSet<String> built from the class body. MemberProp::Ident accesses are tracked when the name is in that set, regardless of the receiver expression. This catches a.x / b.x patterns in static methods.

Comment on lines +629 to +630
// Continue visiting children
expr.obj.visit_with(self);

CopilotAIApr 9, 2026

Copy link

Choose a reason for hiding this comment

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

visit_member_expr overrides default traversal but only visits expr.obj. For computed member expressions (obj[expr]), the property expression is not visited, so any this.#x / this.x references inside the computed key won’t be collected and could cause incorrectly eliminating private members. Use expr.visit_children_with(self) (or explicitly visit the computed property expression) to preserve full traversal.

Suggested change
// Continue visiting children
expr.obj.visit_with(self);
// Continue visiting children, including computed property expressions.
expr.visit_children_with(self);

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Fixed. Changed to expr.visit_children_with(self) which visits both the object and property expressions, including computed keys.

Comment on lines +614 to +625
match &expr.prop {
// Native JS private: `this.#foo`
MemberProp::PrivateName(name) => {
self.referenced.insert(name.name.to_string());
}
// TS private or any member: `this.foo` — only track when
// the object is `this` (to avoid false positives from
// unrelated member accesses like `obj.foo`)
MemberProp::Ident(ident) => {
if matches!(&*expr.obj, Expr::This(_)) {
self.referenced.insert(ident.sym.to_string());
}

CopilotAIApr 9, 2026

Copy link

Choose a reason for hiding this comment

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

Native private #foo and public/TS member foo are both stored as the same string ("foo"). If a class contains both foo and #foo, a this.foo access will incorrectly mark #foo as referenced, preventing intended elimination. Consider encoding JS-private names distinctly (e.g. prefix with # or store an enum) so #foo and foo don’t collide.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Fixed. JS native private names are now stored with a # prefix (e.g. "#foo") while TS private names remain unprefixed ("foo"). private_member_name() returns the prefixed form for PrivateMethod/PrivateProp, and visit_member_expr inserts format!("#{}", name) for MemberProp::PrivateName.

Comment on lines +1 to +25
import { WORKFLOW_SERIALIZE, WORKFLOW_DESERIALIZE } from '@workflow/serde';
import { getWorld } from './world.js';
import { importKey } from './encryption.js';

export class Run {
static [WORKFLOW_SERIALIZE](instance: Run) {
return { id: instance.id };
}

static [WORKFLOW_DESERIALIZE](data: { id: string }) {
return new Run(data.id);
}

id: string;

// TS private field — only referenced by stripped methods
private encryptionKeyPromise: Promise<any> | null = null;

// TS private method — only called by stripped getters/methods
private async getEncryptionKey(): Promise<any> {
if (!this.encryptionKeyPromise) {
this.encryptionKeyPromise = importKey(this.id);
}
return this.encryptionKeyPromise;
}

CopilotAIApr 9, 2026

Copy link

Choose a reason for hiding this comment

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

The new DCE logic claims to support native JS private members (#field/#method()), but the fixture only covers TypeScript private. Adding a fixture that uses #private syntax (including a cascading case) would help prevent regressions in the JS-private path.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Added in the second commit: private-member-dce-native/input.js tests JS native #field and #method() DCE including cascading elimination (#encryptionKeyPromise eliminated because #getEncryptionKey is eliminated) and survival of referenced members (#label kept because toString() references it).

Comment on lines +8504 to +8510
// Dead-code-eliminate unreferenced private members
// (same logic as visit_mut_class_decl above)
let referenced =
ClassMemberRefCollector::collect_from_class_body(&class_expr.class.body);
class_expr.class.body.retain(|member| match member {
ClassMember::PrivateMethod(m) => referenced.contains(&m.key.name.to_string()),
ClassMember::PrivateProp(p) => referenced.contains(&p.key.name.to_string()),

CopilotAIApr 9, 2026

Copy link

Choose a reason for hiding this comment

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

The private-member retention filter is duplicated in both visit_mut_class_decl and visit_mut_class_expr. This duplication increases the risk of future divergence/bugs when the logic changes. Consider extracting the retain logic into a shared helper (e.g. fn retain_referenced_private_members(body: &mut Vec<ClassMember>)) used by both visitors.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Fixed. Extracted retain_referenced_private_members(body: &mut Vec<ClassMember>) on ClassMemberRefCollector, called from both visit_mut_class_decl and visit_mut_class_expr.

- Namespace JS native private names with # prefix to avoid collisions
with TS private members of the same name
- Track TS private member accesses on non-this receivers (e.g. a.x in
static methods) by maintaining a set of known TS-private names
- Use visit_children_with for full traversal including computed member
expressions
- Extract retain logic into shared retain_referenced_private_members()
helper used by both visit_mut_class_decl and visit_mut_class_expr

@VaguelySeriousVaguelySerious left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

AI review: no blocking issues

// accesses like `a.x` / `b.x` are not missed.
MemberProp::Ident(ident) => {
let name = ident.sym.to_string();
if matches!(&*expr.obj, Expr::This(_)) || self.ts_private_names.contains(&name) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

AI Review: Note

For MemberProp::Ident, any this.foo access records foo in referenced, not only TS-private members. Retain still keys off private_member_name, so this is mostly extra churn in the set rather than incorrect retention, but it is slightly less precise than filtering to private names only.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Correct - the collector is conservative: it records all this.foo accesses, not just those matching known private names. This is intentional for simplicity. The extra entries in the referenced set are harmless since retain only removes members that private_member_name() identifies as private. Public members are always kept regardless of what's in the set.

});
```

This optimization is critical for SDK classes like `Run` where private helper methods reference Node.js-only imports (encryption, world access, etc.) — eliminating them allows the downstream module-level DCE to also remove those imports from the workflow bundle.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

AI Review: Nit

This section explains the algorithm well. If you want to reduce future issue reports, a short explicit note on unsupported patterns (for example computed this[...] access to private fields) could help set expectations.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Good suggestion. Computed access like this[name] where name happens to be a private member name won't be tracked, which could lead to incorrect elimination. In practice this pattern is very rare for private members (especially in SDK code like Run), but worth noting if we want to document limitations.

@TooTallNate
TooTallNate enabled auto-merge (squash) April 9, 2026 19:22
@TooTallNate
TooTallNate merged commit 66585fd into mainApr 9, 2026
102 of 103 checks passed
@TooTallNate
TooTallNate deleted the fix/swc-private-member-dce branch April 9, 2026 19:27
@ghostghost mentioned this pull request Apr 9, 2026
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

@TooTallNate@VaguelySerious
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(swc): dead code eliminate unreferenced private class members in workflow mode - #1671

Merged
TooTallNate merged 3 commits into
mainfrom
fix/swc-private-member-dce
Apr 9, 2026
Merged

feat(swc): dead code eliminate unreferenced private class members in workflow mode#1671
TooTallNate merged 3 commits into
mainfrom
fix/swc-private-member-dce

Conversation

@TooTallNate

@TooTallNateTooTallNate commented Apr 9, 2026

Copy link
Copy Markdown
Member

Summary

After stripping "use step" methods from a class body in workflow mode, eliminate private members that are no longer referenced by any remaining public member. This applies to both:

  • JS native private: #field, #method()
  • TypeScript private: private field, private method()

The algorithm is iterative — references are seeded from public members, then expanded through surviving private members until a fixed point, enabling cascading elimination.

Example

Input:

exportclassRun{static[WORKFLOW_SERIALIZE](instance){return{id: instance.id};}static[WORKFLOW_DESERIALIZE](data){returnnewRun(data.id);}id: string;privateencryptionKeyPromise: Promise<any>|null=null;privateasyncgetEncryptionKey(){if(!this.encryptionKeyPromise){this.encryptionKeyPromise=importKey(this.id);}returnthis.encryptionKeyPromise;}constructor(id: string){this.id=id;}getvalue(): Promise<any>{'use step';returnthis.getEncryptionKey().then(()=>getWorld().get(this.id));}asynccancel(): Promise<void>{'use step';constkey=awaitthis.getEncryptionKey();awaitgetWorld().cancel(this.id,key);}toString(): string{return`Run(${this.id})`;}}

Workflow mode output:

exportclassRun{static[WORKFLOW_SERIALIZE](instance){return{id: instance.id};}static[WORKFLOW_DESERIALIZE](data){returnnewRun(data.id);}id;// ✅ private encryptionKeyPromise — ELIMINATED (only referenced by getEncryptionKey)// ✅ private getEncryptionKey() — ELIMINATED (only referenced by stripped step methods)constructor(id){this.id=id;}toString(){return`Run(${this.id})`;}}Run.prototype["cancel"]=globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//...");var__step_Run$value=globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//...");Object.defineProperty(Run.prototype,"value",{get(){return__step_Run$value.call(this);},configurable: true,enumerable: false});

The cascading elimination is key: encryptionKeyPromise is only referenced by getEncryptionKey(), which is itself only referenced by the stripped value getter and cancel() method. Both private members are removed, allowing the downstream module-level DCE to also eliminate the importKey and getWorld imports.

Motivation

SDK classes like Run have private helper methods that reference Node.js-only imports (encryption, world access). Without this optimization, those helpers survive into the workflow bundle even though nothing calls them after "use step" bodies are stripped, keeping the Node.js imports alive and preventing tree-shaking.

Test

New fixture: private-member-dce/input.ts with expected outputs for all three modes.

…workflow mode
After stripping 'use step' methods from a class body in workflow mode,
eliminate private members (both JS native #field/#method and TypeScript
private field/private method) that are no longer referenced by any
remaining public member.
The algorithm is iterative: references are seeded from public members,
then expanded through surviving private members until a fixed point,
enabling cascading elimination (e.g. a private field only referenced by
a private method that is itself unreferenced).
CopilotAI review requested due to automatic review settings April 9, 2026 18:09
@TooTallNate
TooTallNate requested a review from a team as a code ownerApril 9, 2026 18:09
@changeset-bot

changeset-botBot commented Apr 9, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 4ea576a

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

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

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 Apr 9, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Apr 9, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production912067979
✅ 💻 Local Development88601821068
✅ 📦 Local Production88601821068
✅ 🐘 Local Postgres88601821068
✅ 🪟 Windows810889
❌ 🌍 Community Worlds1366524225
✅ 📋 Other225042267
Total4012656874764

❌ Failed Tests

🌍 Community Worlds (65 failed)

mongodb (4 failed):

  • hookWorkflow is not resumable via public webhook endpoint | wrun_01KNSR613X7J6NJ3Q7CC1GAXC8
  • webhookWorkflow | wrun_01KNSR6A2N8E4FRA7K4W0WAJ2X
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously | wrun_01KNSRE4P2HFBR1HGDZ60FSZ4A
  • resilient start: addTenWorkflow completes when run_created returns 500 | wrun_01KNSRMTFPHDMEWW1M0C99YMJB

redis (3 failed):

  • hookWorkflow is not resumable via public webhook endpoint | wrun_01KNSR613X7J6NJ3Q7CC1GAXC8
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously | wrun_01KNSRE4P2HFBR1HGDZ60FSZ4A
  • resilient start: addTenWorkflow completes when run_created returns 500 | wrun_01KNSRMTFPHDMEWW1M0C99YMJB

turso (58 failed):

  • addTenWorkflow | wrun_01KNSR4Q12NYS08Q5M8JMBJE2P
  • addTenWorkflow | wrun_01KNSR4Q12NYS08Q5M8JMBJE2P
  • wellKnownAgentWorkflow (.well-known/agent) | wrun_01KNSR6H7RBB0VN5AF50A8K3HK
  • should work with react rendering in step
  • promiseAllWorkflow | wrun_01KNSR50EQH830PRD40KS32Q9Y
  • promiseRaceWorkflow | wrun_01KNSR54Y75RYQDBMAWK470VYP
  • promiseAnyWorkflow | wrun_01KNSR58C03DCWT8KGXWBMNCP6
  • importedStepOnlyWorkflow | wrun_01KNSR6WGHHN8XTHMJ86Q1JS10
  • hookWorkflow | wrun_01KNSR5M9AWG051SF57JDHTN15
  • hookWorkflow is not resumable via public webhook endpoint | wrun_01KNSR613X7J6NJ3Q7CC1GAXC8
  • webhookWorkflow | wrun_01KNSR6A2N8E4FRA7K4W0WAJ2X
  • sleepingWorkflow | wrun_01KNSR6G7XQP06VPNVA5S5SZ7P
  • parallelSleepWorkflow | wrun_01KNSR6W68GVQAECF6EXH5YKW4
  • nullByteWorkflow | wrun_01KNSR6ZZHZA6GX9A995NWGH4W
  • workflowAndStepMetadataWorkflow | wrun_01KNSR722J4AE22DPWYBGR40AA
  • fetchWorkflow | wrun_01KNSR9QTD0RQEWYMKGRM9P6KS
  • promiseRaceStressTestWorkflow | wrun_01KNSR9V7MWTQB99MYWGDBMPV1
  • error handling error propagation workflow errors nested function calls preserve message and stack trace
  • error handling error propagation workflow errors cross-file imports preserve message and stack trace
  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior FatalError fails immediately without retries
  • error handling retry behavior RetryableError respects custom retryAfter delay
  • error handling retry behavior maxRetries=0 disables retries
  • error handling catchability FatalError can be caught and detected with FatalError.is()
  • error handling not registered WorkflowNotRegisteredError fails the run when workflow does not exist
  • error handling not registered StepNotRegisteredError fails the step but workflow can catch it
  • error handling not registered StepNotRegisteredError fails the run when not caught in workflow
  • hookCleanupTestWorkflow - hook token reuse after workflow completion | wrun_01KNSRDFYWY5RDPW5MJW4M2RKP
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously | wrun_01KNSRE4P2HFBR1HGDZ60FSZ4A
  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running | wrun_01KNSREVJW6G5G63ZYP9DTG2CT
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars) | wrun_01KNSRFFZ3HX99QC8VH9X00X19
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument | wrun_01KNSRFS1Y9106Z5PVYGW5SZ8F
  • closureVariableWorkflow - nested step functions with closure variables | wrun_01KNSRFYM8QZ6MKBWEXNSXEA1E
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step | wrun_01KNSRG1102E8CS370B44Y7JJ1
  • health check (queue-based) - workflow and step endpoints respond to health check messages
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly | wrun_01KNSRGH373GH51TWFMF3M6THG
  • Calculator.calculate - static workflow method using static step methods from another class | wrun_01KNSRGPSM4VSQ1R3Z11NG4MZ0
  • AllInOneService.processNumber - static workflow method using sibling static step methods | wrun_01KNSRGXK317X0HN019FNHP541
  • ChainableService.processWithThis - static step methods using this to reference the class | wrun_01KNSRH5K54HKGJC6TW5YDC6P4
  • thisSerializationWorkflow - step function invoked with .call() and .apply() | wrun_01KNSRHCE7T09PPS45FA7D75MK
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE | wrun_01KNSRHM1A0G2BFRKSN1893XHR
  • instanceMethodStepWorkflow - instance methods with "use step" directive | wrun_01KNSRHTZQJP99YC0M9FNWQPM5
  • crossContextSerdeWorkflow - classes defined in step code are deserializable in workflow context | wrun_01KNSRJ68YGFBGXJ7YFR7ZHMV5
  • stepFunctionAsStartArgWorkflow - step function reference passed as start() argument | wrun_01KNSRJFBR9XRH3012W64TDAJ1
  • cancelRun - cancelling a running workflow | wrun_01KNSRJP4YQBW4BDB9ZFHE3JXZ
  • cancelRun via CLI - cancelling a running workflow | wrun_01KNSRK06K5725SZWQJ2DJZZ1K
  • pages router addTenWorkflow via pages router
  • pages router promiseAllWorkflow via pages router
  • pages router sleepingWorkflow via pages router
  • hookWithSleepWorkflow - hook payloads delivered correctly with concurrent sleep | wrun_01KNSRKDA53YRF17ST7YTD02BW
  • sleepInLoopWorkflow - sleep inside loop with steps actually delays each iteration | wrun_01KNSRM3T179X2E69VG894KAX9
  • sleepWithSequentialStepsWorkflow - sequential steps work with concurrent sleep (control) | wrun_01KNSRME2NVXNSBMBR8AND2DKJ
  • importMetaUrlWorkflow - import.meta.url is available in step bundles | wrun_01KNSRMP16CE789X5BG39DBJEA
  • metadataFromHelperWorkflow - getWorkflowMetadata/getStepMetadata work from module-level helper (#1577) | wrun_01KNSRMR5GRYXW87SN5A0AM9TV
  • resilient start: addTenWorkflow completes when run_created returns 500 | wrun_01KNSRMTFPHDMEWW1M0C99YMJB
  • getterStepWorkflow - getter functions with "use step" directive | wrun_01KNSRMXQTQ6GKDFR5NN8DMJRA

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro8207
✅ example8207
✅ express8207
✅ fastify8207
✅ hono8207
✅ nextjs-turbopack8702
✅ nextjs-webpack8702
✅ nitro8207
✅ nuxt8207
✅ sveltekit8207
✅ vite8207
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable75014
✅ express-stable75014
✅ fastify-stable75014
✅ hono-stable75014
✅ nextjs-turbopack-canary62027
✅ nextjs-turbopack-stable8108
✅ nextjs-webpack-canary62027
✅ nextjs-webpack-stable8108
✅ nitro-stable75014
✅ nuxt-stable75014
✅ sveltekit-stable75014
✅ vite-stable75014
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable75014
✅ express-stable75014
✅ fastify-stable75014
✅ hono-stable75014
✅ nextjs-turbopack-canary62027
✅ nextjs-turbopack-stable8108
✅ nextjs-webpack-canary62027
✅ nextjs-webpack-stable8108
✅ nitro-stable75014
✅ nuxt-stable75014
✅ sveltekit-stable75014
✅ vite-stable75014
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable75014
✅ express-stable75014
✅ fastify-stable75014
✅ hono-stable75014
✅ nextjs-turbopack-canary62027
✅ nextjs-turbopack-stable8108
✅ nextjs-webpack-canary62027
✅ nextjs-webpack-stable8108
✅ nitro-stable75014
✅ nuxt-stable75014
✅ sveltekit-stable75014
✅ vite-stable75014
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack8108
❌ 🌍 Community Worlds
AppPassedFailedSkipped
✅ mongodb-dev500
❌ mongodb5848
✅ redis-dev500
❌ redis5938
✅ turso-dev500
❌ turso4588
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable75014
✅ e2e-local-postgres-nest-stable75014
✅ e2e-local-prod-nest-stable75014

📋 View full workflow run

@github-actions

github-actionsBot commented Apr 9, 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🥇 Express0.043s (-1.6%)1.006s (~)0.962s101.00x
💻 LocalNitro0.044s (-4.8%)1.005s (~)0.961s101.01x
🐘 PostgresExpress0.046s (-22.5% 🟢)1.011s (~)0.965s101.07x
💻 LocalNext.js (Turbopack)0.049s (+3.6%)1.005s (~)0.957s101.12x
🌐 RedisNext.js (Turbopack)0.050s (-11.3% 🟢)1.005s (~)0.955s101.16x
🐘 PostgresNext.js (Turbopack)0.058s (-3.0%)1.011s (~)0.954s101.33x
🐘 PostgresNitro0.063s (+2.3%)1.012s (~)0.949s101.46x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express0.231s (-39.8% 🟢)2.203s (-10.5% 🟢)1.972s101.00x
▲ VercelNext.js (Turbopack)0.267s (-33.8% 🟢)2.195s (-8.4% 🟢)1.928s101.16x
▲ VercelNitro0.304s (+10.2% 🔺)2.211s (+4.0%)1.907s101.32x

🔍 Observability: Express | Next.js (Turbopack) | Nitro

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.119s (-3.1%)2.010s (~)0.891s101.00x
🌐 RedisNext.js (Turbopack)1.126s (~)2.007s (~)0.881s101.01x
💻 LocalNext.js (Turbopack)1.126s (~)2.006s (~)0.880s101.01x
💻 LocalNitro1.128s (~)2.005s (~)0.877s101.01x
💻 LocalExpress1.131s (~)2.006s (~)0.875s101.01x
🐘 PostgresNext.js (Turbopack)1.147s (~)2.009s (~)0.862s101.02x
🐘 PostgresNitro1.147s (-0.6%)2.011s (~)0.864s101.02x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.911s (+1.0%)3.886s (+0.8%)1.975s101.00x
▲ VercelNitro2.003s (+6.5% 🔺)3.356s (-10.3% 🟢)1.353s101.05x
▲ VercelNext.js (Turbopack)2.103s (+4.0%)3.881s (+3.0%)1.778s101.10x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express10.694s (-2.1%)11.028s (~)0.334s31.00x
🌐 RedisNext.js (Turbopack)10.779s (+0.7%)11.023s (~)0.244s31.01x
💻 LocalNext.js (Turbopack)10.831s (~)11.025s (~)0.194s31.01x
🐘 PostgresNext.js (Turbopack)10.849s (~)11.023s (~)0.174s31.01x
💻 LocalNitro10.920s (~)11.023s (~)0.103s31.02x
🐘 PostgresNitro10.934s (~)11.023s (~)0.088s31.02x
💻 LocalExpress10.952s (~)11.024s (~)0.072s31.02x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express16.776s (~)18.801s (-1.4%)2.025s21.00x
▲ VercelNext.js (Turbopack)17.133s (-0.8%)19.157s (~)2.024s21.02x
▲ VercelNitro17.627s (-1.4%)18.953s (-5.7% 🟢)1.326s21.05x

🔍 Observability: Express | Next.js (Turbopack) | Nitro

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express14.019s (-3.8%)14.627s (-2.6%)0.608s51.00x
🌐 RedisNext.js (Turbopack)14.257s (+0.9%)15.029s (~)0.772s41.02x
🐘 PostgresNext.js (Turbopack)14.412s (-0.6%)15.023s (~)0.612s41.03x
🐘 PostgresNitro14.702s (~)15.021s (~)0.319s41.05x
💻 LocalNext.js (Turbopack)14.709s (+1.0%)15.030s (~)0.322s41.05x
💻 LocalNitro14.924s (-0.6%)15.028s (-3.2%)0.104s41.06x
💻 LocalExpress14.989s (~)15.030s (~)0.041s41.07x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express33.420s (-3.4%)35.676s (-3.4%)2.256s21.00x
▲ VercelNext.js (Turbopack)34.176s (-3.3%)36.300s (-3.0%)2.124s21.02x
▲ VercelNitro34.324s (+0.8%)35.857s (-1.2%)1.533s21.03x

🔍 Observability: Express | Next.js (Turbopack) | Nitro

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express12.957s (-8.6% 🟢)13.021s (-13.3% 🟢)0.064s71.00x
🌐 RedisNext.js (Turbopack)13.389s (+1.2%)14.025s (~)0.636s71.03x
🐘 PostgresNext.js (Turbopack)13.757s (~)14.019s (~)0.262s71.06x
🐘 PostgresNitro14.192s (-0.9%)15.022s (~)0.829s61.10x
💻 LocalNext.js (Turbopack)16.033s (+2.8%)16.531s (+3.1%)0.498s61.24x
💻 LocalExpress16.441s (-0.7%)17.032s (~)0.591s61.27x
💻 LocalNitro16.531s (~)17.029s (~)0.498s61.28x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)56.887s (-2.0%)58.539s (-2.7%)1.652s21.00x
▲ VercelExpress59.072s (+7.5% 🔺)61.603s (+7.7% 🔺)2.531s21.04x
▲ VercelNitro59.188s (+2.0%)60.723s (~)1.535s21.04x

🔍 Observability: Next.js (Turbopack) | Express | Nitro

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.211s (-8.6% 🟢)2.009s (~)0.798s151.00x
🐘 PostgresNext.js (Turbopack)1.232s (~)2.009s (~)0.777s151.02x
🐘 PostgresNitro1.260s (~)2.009s (~)0.749s151.04x
🌐 RedisNext.js (Turbopack)1.318s (+6.2% 🔺)2.006s (~)0.688s151.09x
💻 LocalExpress1.515s (+0.8%)2.006s (~)0.491s151.25x
💻 LocalNitro1.516s (+2.1%)2.005s (~)0.490s151.25x
💻 LocalNext.js (Turbopack)1.544s (+4.6%)2.006s (~)0.461s151.27x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.253s (-6.7% 🟢)4.180s (+4.1%)1.927s81.00x
▲ VercelNitro2.496s (-15.3% 🟢)3.670s (-19.6% 🟢)1.174s91.11x
▲ VercelNext.js (Turbopack)2.719s (+23.5% 🔺)4.105s (+6.9% 🔺)1.386s81.21x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.300s (-2.9%)3.009s (~)0.708s101.00x
🐘 PostgresNitro2.330s (+0.7%)3.009s (~)0.680s101.01x
🐘 PostgresNext.js (Turbopack)2.385s (-0.7%)3.009s (~)0.624s101.04x
🌐 RedisNext.js (Turbopack)2.564s (+5.3% 🔺)3.008s (~)0.444s101.11x
💻 LocalExpress2.895s (+1.4%)3.453s (+3.3%)0.558s91.26x
💻 LocalNitro2.991s (+6.5% 🔺)3.341s (+11.1% 🔺)0.351s91.30x
💻 LocalNext.js (Turbopack)3.077s (+24.5% 🔺)3.884s (+29.1% 🔺)0.808s81.34x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.670s (-10.7% 🟢)4.008s (-14.7% 🟢)1.338s81.00x
▲ VercelNext.js (Turbopack)2.780s (+3.0%)4.402s (-1.8%)1.622s71.04x
▲ VercelExpress2.879s (+4.4%)4.437s (+2.9%)1.558s71.08x

🔍 Observability: Nitro | Next.js (Turbopack) | Express

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express3.383s (-2.3%)4.011s (~)0.628s81.00x
🐘 PostgresNitro3.472s (-0.5%)4.011s (~)0.538s81.03x
🐘 PostgresNext.js (Turbopack)3.643s (-1.1%)4.009s (~)0.367s81.08x
🌐 RedisNext.js (Turbopack)4.164s (+5.3% 🔺)5.011s (+16.6% 🔺)0.847s61.23x
💻 LocalExpress7.500s (-8.6% 🟢)8.021s (-8.5% 🟢)0.521s42.22x
💻 LocalNitro8.022s (+1.6%)8.773s (+6.1% 🔺)0.752s42.37x
💻 LocalNext.js (Turbopack)8.384s (+47.2% 🔺)9.019s (+50.0% 🔺)0.635s42.48x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.054s (-1.0%)5.367s (+9.7% 🔺)2.313s61.00x
▲ VercelNitro3.295s (+8.8% 🔺)4.508s (-7.1% 🟢)1.213s71.08x
▲ VercelNext.js (Turbopack)3.678s (+1.9%)5.769s (+8.0% 🔺)2.091s61.20x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.204s (-4.3%)2.008s (~)0.804s151.00x
🐘 PostgresNext.js (Turbopack)1.251s (+2.1%)2.009s (~)0.758s151.04x
🐘 PostgresNitro1.264s (~)2.008s (~)0.744s151.05x
🌐 RedisNext.js (Turbopack)1.278s (-0.6%)2.006s (~)0.729s151.06x
💻 LocalExpress1.519s (~)2.006s (~)0.487s151.26x
💻 LocalNitro1.524s (-3.4%)2.006s (~)0.482s151.27x
💻 LocalNext.js (Turbopack)1.556s (~)2.007s (-3.2%)0.451s151.29x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.003s (-4.2%)3.908s (-1.8%)1.905s81.00x
▲ VercelNitro2.006s (-7.6% 🟢)3.636s (-6.7% 🟢)1.630s91.00x
▲ VercelNext.js (Turbopack)2.217s (+6.1% 🔺)3.745s (+5.0% 🔺)1.529s91.11x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro2.329s (~)3.010s (~)0.681s101.00x
🐘 PostgresExpress2.332s (-0.9%)3.008s (~)0.676s101.00x
🐘 PostgresNext.js (Turbopack)2.390s (~)3.011s (~)0.620s101.03x
🌐 RedisNext.js (Turbopack)2.543s (+3.6%)3.007s (~)0.464s101.09x
💻 LocalNext.js (Turbopack)2.807s (-10.8% 🟢)3.341s (-6.3% 🟢)0.533s91.21x
💻 LocalExpress2.938s (-0.9%)3.454s (-3.1%)0.516s91.26x
💻 LocalNitro2.990s (-1.3%)3.677s (-2.2%)0.687s91.28x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.320s (~)4.314s (+9.6% 🔺)1.994s81.00x
▲ VercelNitro2.485s (-3.3%)3.963s (-5.7% 🟢)1.478s81.07x
▲ VercelNext.js (Turbopack)2.779s (+9.3% 🔺)4.314s (+3.7%)1.535s71.20x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro3.453s (~)4.010s (~)0.557s81.00x
🐘 PostgresExpress3.457s (~)4.009s (~)0.553s81.00x
🐘 PostgresNext.js (Turbopack)3.646s (~)4.009s (~)0.363s81.06x
🌐 RedisNext.js (Turbopack)4.169s (+3.5%)5.012s (+9.4% 🔺)0.843s61.21x
💻 LocalExpress8.033s (-6.6% 🟢)8.774s (-2.8%)0.741s42.33x
💻 LocalNitro8.669s (+2.3%)9.026s (~)0.357s42.51x
💻 LocalNext.js (Turbopack)8.695s (+12.2% 🔺)9.271s (+12.1% 🔺)0.576s42.52x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.728s (-4.1%)4.288s (-0.9%)1.560s71.00x
▲ VercelExpress3.170s (+16.4% 🔺)4.654s (+9.8% 🔺)1.484s71.16x
▲ VercelNext.js (Turbopack)3.271s (-13.0% 🟢)5.128s (-7.0% 🟢)1.857s61.20x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.608s (-26.8% 🟢)1.006s (~)0.399s601.00x
🌐 RedisNext.js (Turbopack)0.708s (+11.4% 🔺)1.004s (~)0.296s601.16x
🐘 PostgresNext.js (Turbopack)0.819s (+6.2% 🔺)1.041s (+1.7%)0.222s581.35x
🐘 PostgresNitro0.858s (+2.2%)1.023s (+1.7%)0.165s591.41x
💻 LocalNext.js (Turbopack)0.860s (-2.7%)1.005s (-3.3%)0.145s601.41x
💻 LocalNitro0.980s (-5.4% 🟢)1.115s (-44.4% 🟢)0.135s541.61x
💻 LocalExpress1.016s (+4.1%)1.654s (+48.3% 🔺)0.638s371.67x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro8.903s (-8.6% 🟢)10.501s (-8.5% 🟢)1.599s61.00x
▲ VercelNext.js (Turbopack)9.632s (-23.0% 🟢)11.579s (-17.7% 🟢)1.948s61.08x
▲ VercelExpress10.403s (+2.7%)12.297s (+1.4%)1.893s51.17x

🔍 Observability: Nitro | Next.js (Turbopack) | Express

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.477s (-26.2% 🟢)2.007s (-16.4% 🟢)0.530s451.00x
🌐 RedisNext.js (Turbopack)1.672s (+15.4% 🔺)2.006s (~)0.333s451.13x
🐘 PostgresNitro2.078s (+3.4%)2.944s (+16.1% 🔺)0.866s311.41x
🐘 PostgresNext.js (Turbopack)2.153s (+14.8% 🔺)2.821s (+37.5% 🔺)0.668s321.46x
💻 LocalNext.js (Turbopack)2.700s (~)3.008s (~)0.308s301.83x
💻 LocalNitro3.022s (-1.1%)3.547s (-9.6% 🟢)0.525s262.05x
💻 LocalExpress3.162s (+3.6%)3.967s (+4.3%)0.805s232.14x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro28.495s (-3.9%)30.408s (-3.5%)1.913s31.00x
▲ VercelExpress28.833s (-1.1%)30.922s (-1.2%)2.089s31.01x
▲ VercelNext.js (Turbopack)29.626s (-0.8%)31.356s (-2.6%)1.730s31.04x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express3.109s (-23.3% 🟢)3.790s (-20.7% 🟢)0.681s321.00x
🌐 RedisNext.js (Turbopack)3.352s (+11.2% 🔺)4.008s (+15.7% 🔺)0.656s301.08x
🐘 PostgresNext.js (Turbopack)3.994s (+5.8% 🔺)4.331s (+8.0% 🔺)0.337s281.28x
🐘 PostgresNitro4.191s (~)4.932s (-1.6%)0.741s251.35x
💻 LocalNext.js (Turbopack)8.691s (+2.0%)9.019s (~)0.328s142.80x
💻 LocalExpress9.039s (-1.3%)9.633s (-1.6%)0.594s132.91x
💻 LocalNitro9.104s (+2.1%)9.710s (+3.3%)0.606s132.93x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express73.513s (-14.6% 🟢)75.368s (-14.2% 🟢)1.855s21.00x
▲ VercelNitro75.626s (+6.1% 🔺)77.847s (+6.5% 🔺)2.221s21.03x
▲ VercelNext.js (Turbopack)77.885s (-4.4%)79.559s (-4.2%)1.675s21.06x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.226s (-18.4% 🟢)1.007s (~)0.781s601.00x
🐘 PostgresNext.js (Turbopack)0.251s (+4.8%)1.007s (~)0.757s601.11x
🐘 PostgresNitro0.281s (-3.7%)1.007s (~)0.725s601.24x
🌐 RedisNext.js (Turbopack)0.284s (+7.7% 🔺)1.004s (~)0.720s601.26x
💻 LocalNitro0.588s (+4.9%)1.004s (~)0.417s602.60x
💻 LocalNext.js (Turbopack)0.620s (+20.2% 🔺)1.022s (+1.7%)0.402s592.74x
💻 LocalExpress0.697s (+18.7% 🔺)1.096s (+9.1% 🔺)0.398s553.08x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.544s (-22.1% 🟢)3.163s (-11.6% 🟢)1.620s201.00x
▲ VercelNext.js (Turbopack)1.688s (-5.7% 🟢)3.573s (+3.3%)1.886s181.09x
▲ VercelNitro1.727s (-8.8% 🟢)3.318s (-4.5%)1.591s191.12x

🔍 Observability: Express | Next.js (Turbopack) | Nitro

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.363s (-27.3% 🟢)1.006s (~)0.643s901.00x
🐘 PostgresNitro0.493s (-1.8%)1.007s (~)0.514s901.36x
🐘 PostgresNext.js (Turbopack)0.508s (+4.6%)1.007s (~)0.499s901.40x
🌐 RedisNext.js (Turbopack)1.144s (+2.9%)2.006s (+17.7% 🔺)0.862s453.15x
💻 LocalExpress2.492s (-1.6%)3.009s (~)0.517s306.86x
💻 LocalNitro2.652s (+14.2% 🔺)3.180s (+5.7% 🔺)0.528s297.31x
💻 LocalNext.js (Turbopack)2.676s (+7.7% 🔺)3.076s (+2.2%)0.401s307.37x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.612s (-2.1%)4.132s (-6.5% 🟢)1.519s221.00x
▲ VercelExpress2.738s (-7.0% 🟢)4.568s (-3.6%)1.830s201.05x
▲ VercelNext.js (Turbopack)3.366s (-5.4% 🟢)4.890s (-7.9% 🟢)1.524s191.29x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.583s (-26.2% 🟢)1.007s (~)0.423s1201.00x
🐘 PostgresNitro0.809s (-0.9%)1.009s (~)0.200s1191.39x
🐘 PostgresNext.js (Turbopack)0.847s (+10.4% 🔺)1.043s (+3.6%)0.196s1161.45x
🌐 RedisNext.js (Turbopack)2.677s (-3.5%)3.007s (-0.8%)0.330s404.59x
💻 LocalNitro10.890s (+4.7%)11.485s (+4.1%)0.595s1118.67x
💻 LocalExpress10.935s (~)11.395s (-1.5%)0.460s1118.75x
💻 LocalNext.js (Turbopack)11.065s (+7.4% 🔺)11.572s (+7.4% 🔺)0.507s1118.97x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro6.908s (+9.6% 🔺)8.327s (+3.7%)1.419s151.00x
▲ VercelNext.js (Turbopack)47.028s (+673.5% 🔺)48.733s (+526.0% 🔺)1.705s66.81x
▲ VercelExpress49.426s (+616.1% 🔺)51.745s (+489.1% 🔺)2.320s77.15x

🔍 Observability: Nitro | Next.js (Turbopack) | Express

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.158s (-25.4% 🟢)1.001s (+0.5%)0.001s (-26.7% 🟢)1.009s (~)0.851s101.00x
💻 LocalNext.js (Turbopack)0.170s (+0.5%)1.002s (~)0.012s (+14.7% 🔺)1.017s (~)0.847s101.07x
🐘 PostgresNext.js (Turbopack)0.192s (+0.8%)1.001s (~)0.001s (-15.4% 🟢)1.010s (~)0.818s101.21x
💻 LocalNitro0.207s (-0.7%)1.004s (~)0.012s (+11.5% 🔺)1.017s (~)0.810s101.31x
💻 LocalExpress0.219s (+6.8% 🔺)1.004s (~)0.011s (-8.5% 🟢)1.017s (~)0.798s101.38x
🐘 PostgresNitro0.219s (+2.0%)0.996s (~)0.001s (+30.0% 🔺)1.012s (~)0.792s101.39x
🌐 RedisNext.js (Turbopack)0.242s (+62.9% 🔺)1.001s (~)0.032s (+1776.5% 🔺)1.037s (+3.0%)0.796s101.53x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)1.493s (-11.9% 🟢)2.833s (-11.4% 🟢)0.461s (-51.8% 🟢)3.696s (-20.0% 🟢)2.203s101.00x
▲ VercelExpress1.565s (+3.1%)2.942s (+1.1%)0.642s (-16.1% 🟢)4.033s (-2.6%)2.468s101.05x
▲ VercelNitro1.603s (-5.5% 🟢)2.899s (-11.0% 🟢)0.562s (+7.0% 🔺)3.812s (-9.8% 🟢)2.208s101.07x

🔍 Observability: Next.js (Turbopack) | Express | Nitro

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)0.481s (+14.5% 🔺)1.002s (~)0.003s (+4.8%)1.011s (~)0.530s601.00x
🐘 PostgresExpress0.522s (-16.5% 🟢)1.006s (~)0.004s (-10.2% 🟢)1.022s (~)0.500s591.09x
🐘 PostgresNitro0.629s (-2.0%)1.005s (~)0.004s (-11.2% 🟢)1.022s (~)0.393s591.31x
🐘 PostgresNext.js (Turbopack)0.659s (+9.3% 🔺)1.026s (+1.7%)0.008s (+110.1% 🔺)1.044s (+2.2%)0.385s581.37x
💻 LocalNext.js (Turbopack)0.763s (+13.8% 🔺)1.011s (~)0.009s (-15.6% 🟢)1.116s (+8.9% 🔺)0.353s541.59x
💻 LocalNitro0.815s (-14.6% 🟢)1.012s (~)0.010s (+2.4%)1.111s (-9.5% 🟢)0.296s571.70x
💻 LocalExpress0.847s (+18.5% 🔺)1.010s (~)0.010s (+6.7% 🔺)1.115s (+8.9% 🔺)0.268s541.76x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.092s (+1.5%)5.761s (~)0.242s (-2.7%)6.436s (-0.6%)2.344s101.00x
▲ VercelNext.js (Turbopack)4.186s (-6.5% 🟢)6.079s (-0.6%)0.220s (+9.9% 🔺)6.720s (~)2.534s91.02x
▲ VercelNitro4.499s (+6.2% 🔺)5.768s (+2.8%)0.457s (+71.8% 🔺)6.725s (+5.7% 🔺)2.226s91.10x

🔍 Observability: Express | Next.js (Turbopack) | Nitro

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)0.900s (+1.8%)1.035s (+3.5%)0.000s (+106.9% 🔺)1.039s (+3.4%)0.139s581.00x
🐘 PostgresExpress0.931s (-3.9%)1.146s (-5.9% 🟢)0.000s (+172.2% 🔺)1.159s (-6.0% 🟢)0.228s541.03x
🐘 PostgresNitro0.946s (~)1.220s (+12.0% 🔺)0.000s (-17.3% 🟢)1.232s (+10.3% 🔺)0.286s491.05x
🐘 PostgresNext.js (Turbopack)0.956s (+3.1%)1.177s (+5.6% 🔺)0.000s (+Infinity% 🔺)1.185s (+4.2%)0.229s511.06x
💻 LocalNitro1.218s (+2.4%)2.021s (~)0.000s (-7.1% 🟢)2.023s (~)0.805s301.35x
💻 LocalNext.js (Turbopack)1.256s (~)2.019s (~)0.000s (-11.1% 🟢)2.022s (~)0.767s301.40x
💻 LocalExpress1.444s (+17.6% 🔺)2.022s (~)0.001s (+14.8% 🔺)2.203s (+8.8% 🔺)0.758s281.60x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.826s (~)4.141s (+4.9%)0.000s (+150.0% 🔺)4.585s (+4.0%)1.759s141.00x
▲ VercelNitro2.860s (+7.4% 🔺)4.133s (+4.8%)0.000s (-50.0% 🟢)4.541s (+3.8%)1.681s141.01x
▲ VercelNext.js (Turbopack)3.188s (-28.9% 🟢)4.619s (-22.8% 🟢)0.000s (-100.0% 🟢)5.035s (-21.8% 🟢)1.847s121.13x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

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

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)1.728s (-1.9%)2.035s (+1.7%)0.000s (-50.0% 🟢)2.039s (+1.6%)0.311s301.00x
🐘 PostgresExpress1.739s (-1.1%)2.143s (+3.8%)0.000s (-65.5% 🟢)2.152s (+3.4%)0.413s281.01x
🐘 PostgresNitro1.789s (+2.3%)2.102s (~)0.000s (+Infinity% 🔺)2.113s (-1.0%)0.324s291.04x
🐘 PostgresNext.js (Turbopack)1.958s (+6.4% 🔺)2.147s (+0.6%)0.000s (+Infinity% 🔺)2.155s (~)0.197s281.13x
💻 LocalNitro3.410s (+6.4% 🔺)4.034s (+1.7%)0.001s (+140.0% 🔺)4.037s (+1.7%)0.627s151.97x
💻 LocalNext.js (Turbopack)3.725s (+3.2%)4.233s (+1.6%)0.000s (-12.5% 🟢)4.236s (+1.6%)0.511s152.16x
💻 LocalExpress3.802s (+9.2% 🔺)4.368s (+8.3% 🔺)0.001s (+233.3% 🔺)4.372s (+8.3% 🔺)0.570s152.20x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.969s (-25.3% 🟢)5.072s (-23.7% 🟢)0.000s (-62.5% 🟢)5.489s (-22.2% 🟢)1.521s121.00x
▲ VercelNext.js (Turbopack)3.988s5.498s0.000s5.941s1.953s111.01x
▲ VercelExpress5.598s (+32.4% 🔺)7.223s (+32.7% 🔺)0.000s (-31.3% 🟢)7.670s (+30.2% 🔺)2.072s81.41x

🔍 Observability: Nitro | Next.js (Turbopack) | Express

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNext.js (Turbopack)10/21
🐘 PostgresExpress19/21
▲ VercelExpress12/21
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express🐘 Postgres17/21
Next.js (Turbopack)🌐 Redis10/21
Nitro🐘 Postgres15/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)

📋 View full workflow run

CopilotAI 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.

Pull request overview

Adds a workflow-mode optimization to the SWC plugin so that after "use step" class members are stripped, any now-unreferenced private class members are also removed, improving downstream tree-shaking (e.g., dropping Node-only imports kept alive by unused helpers).

Changes:

  • Implement iterative private-member reachability analysis and removal after step-member stripping (class decls + class exprs).
  • Add a new transform fixture validating cascading elimination of TypeScript private members in workflow mode.
  • Document the new optimization and publish a patch changeset.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 5 comments.

Show a summary per file
FileDescription
packages/swc-plugin-workflow/transform/src/lib.rsAdds private-member reference collection + DCE pass after "use step" stripping.
packages/swc-plugin-workflow/transform/tests/fixture/private-member-dce/input.tsNew fixture input covering cascading TS private elimination.
packages/swc-plugin-workflow/transform/tests/fixture/private-member-dce/output-workflow.jsExpected workflow output with TS-private members removed and imports dropped.
packages/swc-plugin-workflow/transform/tests/fixture/private-member-dce/output-step.jsExpected step-mode output (private members retained).
packages/swc-plugin-workflow/transform/tests/fixture/private-member-dce/output-client.jsExpected client-mode output (private members retained).
packages/swc-plugin-workflow/spec.mdDocuments the private-member DCE behavior and example.
.changeset/private-member-dce.mdDeclares a patch release for the SWC plugin change.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +619 to +624
// TS private or any member: `this.foo` — only track when
// the object is `this` (to avoid false positives from
// unrelated member accesses like `obj.foo`)
MemberProp::Ident(ident) => {
if matches!(&*expr.obj, Expr::This(_)) {
self.referenced.insert(ident.sym.to_string());

CopilotAIApr 9, 2026

Copy link

Choose a reason for hiding this comment

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

ClassMemberRefCollector only records MemberProp::Ident accesses when the object is this. In TypeScript, private members are also legally accessed via other instances of the same class (e.g. static compare(a: C, b: C) { return a.x - b.x }). With the current check, those references won’t be collected and the private member can be incorrectly removed, breaking runtime behavior. Consider treating any .ident access as a reference when ident matches a known TS-private member name in the class, regardless of the receiver expression (safe, though more conservative).

Suggested change
// TS private or any member: `this.foo` — only track when
// the object is `this` (to avoid false positives from
// unrelated member accesses like `obj.foo`)
MemberProp::Ident(ident) => {
ifmatches!(&*expr.obj,Expr::This(_)){
self.referenced.insert(ident.sym.to_string());
// TS private or any member access. Track `this.foo` as before,
// and also track `obj.foo` when `foo` is a known TS-private
// member of the current class so same-class accesses like
// `a.x` / `b.x` are not missed.
MemberProp::Ident(ident) => {
let name = ident.sym.to_string();
ifmatches!(&*expr.obj,Expr::This(_)) || self.private_names.contains(&name){
self.referenced.insert(name);

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Fixed. The collector now holds a ts_private_names: HashSet<String> built from the class body. MemberProp::Ident accesses are tracked when the name is in that set, regardless of the receiver expression. This catches a.x / b.x patterns in static methods.

Comment on lines +629 to +630
// Continue visiting children
expr.obj.visit_with(self);

CopilotAIApr 9, 2026

Copy link

Choose a reason for hiding this comment

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

visit_member_expr overrides default traversal but only visits expr.obj. For computed member expressions (obj[expr]), the property expression is not visited, so any this.#x / this.x references inside the computed key won’t be collected and could cause incorrectly eliminating private members. Use expr.visit_children_with(self) (or explicitly visit the computed property expression) to preserve full traversal.

Suggested change
// Continue visiting children
expr.obj.visit_with(self);
// Continue visiting children, including computed property expressions.
expr.visit_children_with(self);

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Fixed. Changed to expr.visit_children_with(self) which visits both the object and property expressions, including computed keys.

Comment on lines +614 to +625
match &expr.prop {
// Native JS private: `this.#foo`
MemberProp::PrivateName(name) => {
self.referenced.insert(name.name.to_string());
}
// TS private or any member: `this.foo` — only track when
// the object is `this` (to avoid false positives from
// unrelated member accesses like `obj.foo`)
MemberProp::Ident(ident) => {
if matches!(&*expr.obj, Expr::This(_)) {
self.referenced.insert(ident.sym.to_string());
}

CopilotAIApr 9, 2026

Copy link

Choose a reason for hiding this comment

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

Native private #foo and public/TS member foo are both stored as the same string ("foo"). If a class contains both foo and #foo, a this.foo access will incorrectly mark #foo as referenced, preventing intended elimination. Consider encoding JS-private names distinctly (e.g. prefix with # or store an enum) so #foo and foo don’t collide.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Fixed. JS native private names are now stored with a # prefix (e.g. "#foo") while TS private names remain unprefixed ("foo"). private_member_name() returns the prefixed form for PrivateMethod/PrivateProp, and visit_member_expr inserts format!("#{}", name) for MemberProp::PrivateName.

Comment on lines +1 to +25
import { WORKFLOW_SERIALIZE, WORKFLOW_DESERIALIZE } from '@workflow/serde';
import { getWorld } from './world.js';
import { importKey } from './encryption.js';

export class Run {
static [WORKFLOW_SERIALIZE](instance: Run) {
return { id: instance.id };
}

static [WORKFLOW_DESERIALIZE](data: { id: string }) {
return new Run(data.id);
}

id: string;

// TS private field — only referenced by stripped methods
private encryptionKeyPromise: Promise<any> | null = null;

// TS private method — only called by stripped getters/methods
private async getEncryptionKey(): Promise<any> {
if (!this.encryptionKeyPromise) {
this.encryptionKeyPromise = importKey(this.id);
}
return this.encryptionKeyPromise;
}

CopilotAIApr 9, 2026

Copy link

Choose a reason for hiding this comment

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

The new DCE logic claims to support native JS private members (#field/#method()), but the fixture only covers TypeScript private. Adding a fixture that uses #private syntax (including a cascading case) would help prevent regressions in the JS-private path.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Added in the second commit: private-member-dce-native/input.js tests JS native #field and #method() DCE including cascading elimination (#encryptionKeyPromise eliminated because #getEncryptionKey is eliminated) and survival of referenced members (#label kept because toString() references it).

Comment on lines +8504 to +8510
// Dead-code-eliminate unreferenced private members
// (same logic as visit_mut_class_decl above)
let referenced =
ClassMemberRefCollector::collect_from_class_body(&class_expr.class.body);
class_expr.class.body.retain(|member| match member {
ClassMember::PrivateMethod(m) => referenced.contains(&m.key.name.to_string()),
ClassMember::PrivateProp(p) => referenced.contains(&p.key.name.to_string()),

CopilotAIApr 9, 2026

Copy link

Choose a reason for hiding this comment

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

The private-member retention filter is duplicated in both visit_mut_class_decl and visit_mut_class_expr. This duplication increases the risk of future divergence/bugs when the logic changes. Consider extracting the retain logic into a shared helper (e.g. fn retain_referenced_private_members(body: &mut Vec<ClassMember>)) used by both visitors.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Fixed. Extracted retain_referenced_private_members(body: &mut Vec<ClassMember>) on ClassMemberRefCollector, called from both visit_mut_class_decl and visit_mut_class_expr.

- Namespace JS native private names with # prefix to avoid collisions
with TS private members of the same name
- Track TS private member accesses on non-this receivers (e.g. a.x in
static methods) by maintaining a set of known TS-private names
- Use visit_children_with for full traversal including computed member
expressions
- Extract retain logic into shared retain_referenced_private_members()
helper used by both visit_mut_class_decl and visit_mut_class_expr

@VaguelySeriousVaguelySerious left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

AI review: no blocking issues

// accesses like `a.x` / `b.x` are not missed.
MemberProp::Ident(ident) => {
let name = ident.sym.to_string();
if matches!(&*expr.obj, Expr::This(_)) || self.ts_private_names.contains(&name) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

AI Review: Note

For MemberProp::Ident, any this.foo access records foo in referenced, not only TS-private members. Retain still keys off private_member_name, so this is mostly extra churn in the set rather than incorrect retention, but it is slightly less precise than filtering to private names only.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Correct - the collector is conservative: it records all this.foo accesses, not just those matching known private names. This is intentional for simplicity. The extra entries in the referenced set are harmless since retain only removes members that private_member_name() identifies as private. Public members are always kept regardless of what's in the set.

});
```

This optimization is critical for SDK classes like `Run` where private helper methods reference Node.js-only imports (encryption, world access, etc.) — eliminating them allows the downstream module-level DCE to also remove those imports from the workflow bundle.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

AI Review: Nit

This section explains the algorithm well. If you want to reduce future issue reports, a short explicit note on unsupported patterns (for example computed this[...] access to private fields) could help set expectations.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Good suggestion. Computed access like this[name] where name happens to be a private member name won't be tracked, which could lead to incorrect elimination. In practice this pattern is very rare for private members (especially in SDK code like Run), but worth noting if we want to document limitations.

@TooTallNate
TooTallNate enabled auto-merge (squash) April 9, 2026 19:22
@TooTallNate
TooTallNate merged commit 66585fd into mainApr 9, 2026
102 of 103 checks passed
@TooTallNate
TooTallNate deleted the fix/swc-private-member-dce branch April 9, 2026 19:27
@ghostghost mentioned this pull request Apr 9, 2026
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

@TooTallNate@VaguelySerious
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

feat(swc): dead code eliminate unreferenced private class members in workflow mode - #1671

Merged
TooTallNate merged 3 commits into
mainfrom
fix/swc-private-member-dce
Apr 9, 2026
Merged

feat(swc): dead code eliminate unreferenced private class members in workflow mode#1671
TooTallNate merged 3 commits into
mainfrom
fix/swc-private-member-dce

Conversation

@TooTallNate

@TooTallNateTooTallNate commented Apr 9, 2026

Copy link
Copy Markdown
Member

Summary

After stripping "use step" methods from a class body in workflow mode, eliminate private members that are no longer referenced by any remaining public member. This applies to both:

  • JS native private: #field, #method()
  • TypeScript private: private field, private method()

The algorithm is iterative — references are seeded from public members, then expanded through surviving private members until a fixed point, enabling cascading elimination.

Example

Input:

exportclassRun{static[WORKFLOW_SERIALIZE](instance){return{id: instance.id};}static[WORKFLOW_DESERIALIZE](data){returnnewRun(data.id);}id: string;privateencryptionKeyPromise: Promise<any>|null=null;privateasyncgetEncryptionKey(){if(!this.encryptionKeyPromise){this.encryptionKeyPromise=importKey(this.id);}returnthis.encryptionKeyPromise;}constructor(id: string){this.id=id;}getvalue(): Promise<any>{'use step';returnthis.getEncryptionKey().then(()=>getWorld().get(this.id));}asynccancel(): Promise<void>{'use step';constkey=awaitthis.getEncryptionKey();awaitgetWorld().cancel(this.id,key);}toString(): string{return`Run(${this.id})`;}}

Workflow mode output:

exportclassRun{static[WORKFLOW_SERIALIZE](instance){return{id: instance.id};}static[WORKFLOW_DESERIALIZE](data){returnnewRun(data.id);}id;// ✅ private encryptionKeyPromise — ELIMINATED (only referenced by getEncryptionKey)// ✅ private getEncryptionKey() — ELIMINATED (only referenced by stripped step methods)constructor(id){this.id=id;}toString(){return`Run(${this.id})`;}}Run.prototype["cancel"]=globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//...");var__step_Run$value=globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//...");Object.defineProperty(Run.prototype,"value",{get(){return__step_Run$value.call(this);},configurable: true,enumerable: false});

The cascading elimination is key: encryptionKeyPromise is only referenced by getEncryptionKey(), which is itself only referenced by the stripped value getter and cancel() method. Both private members are removed, allowing the downstream module-level DCE to also eliminate the importKey and getWorld imports.

Motivation

SDK classes like Run have private helper methods that reference Node.js-only imports (encryption, world access). Without this optimization, those helpers survive into the workflow bundle even though nothing calls them after "use step" bodies are stripped, keeping the Node.js imports alive and preventing tree-shaking.

Test

New fixture: private-member-dce/input.ts with expected outputs for all three modes.

…workflow mode
After stripping 'use step' methods from a class body in workflow mode,
eliminate private members (both JS native #field/#method and TypeScript
private field/private method) that are no longer referenced by any
remaining public member.
The algorithm is iterative: references are seeded from public members,
then expanded through surviving private members until a fixed point,
enabling cascading elimination (e.g. a private field only referenced by
a private method that is itself unreferenced).
CopilotAI review requested due to automatic review settings April 9, 2026 18:09
@TooTallNate
TooTallNate requested a review from a team as a code ownerApril 9, 2026 18:09
@changeset-bot

changeset-botBot commented Apr 9, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 4ea576a

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

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

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 Apr 9, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Apr 9, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production912067979
✅ 💻 Local Development88601821068
✅ 📦 Local Production88601821068
✅ 🐘 Local Postgres88601821068
✅ 🪟 Windows810889
❌ 🌍 Community Worlds1366524225
✅ 📋 Other225042267
Total4012656874764

❌ Failed Tests

🌍 Community Worlds (65 failed)

mongodb (4 failed):

  • hookWorkflow is not resumable via public webhook endpoint | wrun_01KNSR613X7J6NJ3Q7CC1GAXC8
  • webhookWorkflow | wrun_01KNSR6A2N8E4FRA7K4W0WAJ2X
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously | wrun_01KNSRE4P2HFBR1HGDZ60FSZ4A
  • resilient start: addTenWorkflow completes when run_created returns 500 | wrun_01KNSRMTFPHDMEWW1M0C99YMJB

redis (3 failed):

  • hookWorkflow is not resumable via public webhook endpoint | wrun_01KNSR613X7J6NJ3Q7CC1GAXC8
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously | wrun_01KNSRE4P2HFBR1HGDZ60FSZ4A
  • resilient start: addTenWorkflow completes when run_created returns 500 | wrun_01KNSRMTFPHDMEWW1M0C99YMJB

turso (58 failed):

  • addTenWorkflow | wrun_01KNSR4Q12NYS08Q5M8JMBJE2P
  • addTenWorkflow | wrun_01KNSR4Q12NYS08Q5M8JMBJE2P
  • wellKnownAgentWorkflow (.well-known/agent) | wrun_01KNSR6H7RBB0VN5AF50A8K3HK
  • should work with react rendering in step
  • promiseAllWorkflow | wrun_01KNSR50EQH830PRD40KS32Q9Y
  • promiseRaceWorkflow | wrun_01KNSR54Y75RYQDBMAWK470VYP
  • promiseAnyWorkflow | wrun_01KNSR58C03DCWT8KGXWBMNCP6
  • importedStepOnlyWorkflow | wrun_01KNSR6WGHHN8XTHMJ86Q1JS10
  • hookWorkflow | wrun_01KNSR5M9AWG051SF57JDHTN15
  • hookWorkflow is not resumable via public webhook endpoint | wrun_01KNSR613X7J6NJ3Q7CC1GAXC8
  • webhookWorkflow | wrun_01KNSR6A2N8E4FRA7K4W0WAJ2X
  • sleepingWorkflow | wrun_01KNSR6G7XQP06VPNVA5S5SZ7P
  • parallelSleepWorkflow | wrun_01KNSR6W68GVQAECF6EXH5YKW4
  • nullByteWorkflow | wrun_01KNSR6ZZHZA6GX9A995NWGH4W
  • workflowAndStepMetadataWorkflow | wrun_01KNSR722J4AE22DPWYBGR40AA
  • fetchWorkflow | wrun_01KNSR9QTD0RQEWYMKGRM9P6KS
  • promiseRaceStressTestWorkflow | wrun_01KNSR9V7MWTQB99MYWGDBMPV1
  • error handling error propagation workflow errors nested function calls preserve message and stack trace
  • error handling error propagation workflow errors cross-file imports preserve message and stack trace
  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior FatalError fails immediately without retries
  • error handling retry behavior RetryableError respects custom retryAfter delay
  • error handling retry behavior maxRetries=0 disables retries
  • error handling catchability FatalError can be caught and detected with FatalError.is()
  • error handling not registered WorkflowNotRegisteredError fails the run when workflow does not exist
  • error handling not registered StepNotRegisteredError fails the step but workflow can catch it
  • error handling not registered StepNotRegisteredError fails the run when not caught in workflow
  • hookCleanupTestWorkflow - hook token reuse after workflow completion | wrun_01KNSRDFYWY5RDPW5MJW4M2RKP
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously | wrun_01KNSRE4P2HFBR1HGDZ60FSZ4A
  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running | wrun_01KNSREVJW6G5G63ZYP9DTG2CT
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars) | wrun_01KNSRFFZ3HX99QC8VH9X00X19
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument | wrun_01KNSRFS1Y9106Z5PVYGW5SZ8F
  • closureVariableWorkflow - nested step functions with closure variables | wrun_01KNSRFYM8QZ6MKBWEXNSXEA1E
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step | wrun_01KNSRG1102E8CS370B44Y7JJ1
  • health check (queue-based) - workflow and step endpoints respond to health check messages
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly | wrun_01KNSRGH373GH51TWFMF3M6THG
  • Calculator.calculate - static workflow method using static step methods from another class | wrun_01KNSRGPSM4VSQ1R3Z11NG4MZ0
  • AllInOneService.processNumber - static workflow method using sibling static step methods | wrun_01KNSRGXK317X0HN019FNHP541
  • ChainableService.processWithThis - static step methods using this to reference the class | wrun_01KNSRH5K54HKGJC6TW5YDC6P4
  • thisSerializationWorkflow - step function invoked with .call() and .apply() | wrun_01KNSRHCE7T09PPS45FA7D75MK
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE | wrun_01KNSRHM1A0G2BFRKSN1893XHR
  • instanceMethodStepWorkflow - instance methods with "use step" directive | wrun_01KNSRHTZQJP99YC0M9FNWQPM5
  • crossContextSerdeWorkflow - classes defined in step code are deserializable in workflow context | wrun_01KNSRJ68YGFBGXJ7YFR7ZHMV5
  • stepFunctionAsStartArgWorkflow - step function reference passed as start() argument | wrun_01KNSRJFBR9XRH3012W64TDAJ1
  • cancelRun - cancelling a running workflow | wrun_01KNSRJP4YQBW4BDB9ZFHE3JXZ
  • cancelRun via CLI - cancelling a running workflow | wrun_01KNSRK06K5725SZWQJ2DJZZ1K
  • pages router addTenWorkflow via pages router
  • pages router promiseAllWorkflow via pages router
  • pages router sleepingWorkflow via pages router
  • hookWithSleepWorkflow - hook payloads delivered correctly with concurrent sleep | wrun_01KNSRKDA53YRF17ST7YTD02BW
  • sleepInLoopWorkflow - sleep inside loop with steps actually delays each iteration | wrun_01KNSRM3T179X2E69VG894KAX9
  • sleepWithSequentialStepsWorkflow - sequential steps work with concurrent sleep (control) | wrun_01KNSRME2NVXNSBMBR8AND2DKJ
  • importMetaUrlWorkflow - import.meta.url is available in step bundles | wrun_01KNSRMP16CE789X5BG39DBJEA
  • metadataFromHelperWorkflow - getWorkflowMetadata/getStepMetadata work from module-level helper (#1577) | wrun_01KNSRMR5GRYXW87SN5A0AM9TV
  • resilient start: addTenWorkflow completes when run_created returns 500 | wrun_01KNSRMTFPHDMEWW1M0C99YMJB
  • getterStepWorkflow - getter functions with "use step" directive | wrun_01KNSRMXQTQ6GKDFR5NN8DMJRA

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro8207
✅ example8207
✅ express8207
✅ fastify8207
✅ hono8207
✅ nextjs-turbopack8702
✅ nextjs-webpack8702
✅ nitro8207
✅ nuxt8207
✅ sveltekit8207
✅ vite8207
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable75014
✅ express-stable75014
✅ fastify-stable75014
✅ hono-stable75014
✅ nextjs-turbopack-canary62027
✅ nextjs-turbopack-stable8108
✅ nextjs-webpack-canary62027
✅ nextjs-webpack-stable8108
✅ nitro-stable75014
✅ nuxt-stable75014
✅ sveltekit-stable75014
✅ vite-stable75014
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable75014
✅ express-stable75014
✅ fastify-stable75014
✅ hono-stable75014
✅ nextjs-turbopack-canary62027
✅ nextjs-turbopack-stable8108
✅ nextjs-webpack-canary62027
✅ nextjs-webpack-stable8108
✅ nitro-stable75014
✅ nuxt-stable75014
✅ sveltekit-stable75014
✅ vite-stable75014
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable75014
✅ express-stable75014
✅ fastify-stable75014
✅ hono-stable75014
✅ nextjs-turbopack-canary62027
✅ nextjs-turbopack-stable8108
✅ nextjs-webpack-canary62027
✅ nextjs-webpack-stable8108
✅ nitro-stable75014
✅ nuxt-stable75014
✅ sveltekit-stable75014
✅ vite-stable75014
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack8108
❌ 🌍 Community Worlds
AppPassedFailedSkipped
✅ mongodb-dev500
❌ mongodb5848
✅ redis-dev500
❌ redis5938
✅ turso-dev500
❌ turso4588
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable75014
✅ e2e-local-postgres-nest-stable75014
✅ e2e-local-prod-nest-stable75014

📋 View full workflow run

@github-actions

github-actionsBot commented Apr 9, 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🥇 Express0.043s (-1.6%)1.006s (~)0.962s101.00x
💻 LocalNitro0.044s (-4.8%)1.005s (~)0.961s101.01x
🐘 PostgresExpress0.046s (-22.5% 🟢)1.011s (~)0.965s101.07x
💻 LocalNext.js (Turbopack)0.049s (+3.6%)1.005s (~)0.957s101.12x
🌐 RedisNext.js (Turbopack)0.050s (-11.3% 🟢)1.005s (~)0.955s101.16x
🐘 PostgresNext.js (Turbopack)0.058s (-3.0%)1.011s (~)0.954s101.33x
🐘 PostgresNitro0.063s (+2.3%)1.012s (~)0.949s101.46x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express0.231s (-39.8% 🟢)2.203s (-10.5% 🟢)1.972s101.00x
▲ VercelNext.js (Turbopack)0.267s (-33.8% 🟢)2.195s (-8.4% 🟢)1.928s101.16x
▲ VercelNitro0.304s (+10.2% 🔺)2.211s (+4.0%)1.907s101.32x

🔍 Observability: Express | Next.js (Turbopack) | Nitro

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.119s (-3.1%)2.010s (~)0.891s101.00x
🌐 RedisNext.js (Turbopack)1.126s (~)2.007s (~)0.881s101.01x
💻 LocalNext.js (Turbopack)1.126s (~)2.006s (~)0.880s101.01x
💻 LocalNitro1.128s (~)2.005s (~)0.877s101.01x
💻 LocalExpress1.131s (~)2.006s (~)0.875s101.01x
🐘 PostgresNext.js (Turbopack)1.147s (~)2.009s (~)0.862s101.02x
🐘 PostgresNitro1.147s (-0.6%)2.011s (~)0.864s101.02x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.911s (+1.0%)3.886s (+0.8%)1.975s101.00x
▲ VercelNitro2.003s (+6.5% 🔺)3.356s (-10.3% 🟢)1.353s101.05x
▲ VercelNext.js (Turbopack)2.103s (+4.0%)3.881s (+3.0%)1.778s101.10x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express10.694s (-2.1%)11.028s (~)0.334s31.00x
🌐 RedisNext.js (Turbopack)10.779s (+0.7%)11.023s (~)0.244s31.01x
💻 LocalNext.js (Turbopack)10.831s (~)11.025s (~)0.194s31.01x
🐘 PostgresNext.js (Turbopack)10.849s (~)11.023s (~)0.174s31.01x
💻 LocalNitro10.920s (~)11.023s (~)0.103s31.02x
🐘 PostgresNitro10.934s (~)11.023s (~)0.088s31.02x
💻 LocalExpress10.952s (~)11.024s (~)0.072s31.02x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express16.776s (~)18.801s (-1.4%)2.025s21.00x
▲ VercelNext.js (Turbopack)17.133s (-0.8%)19.157s (~)2.024s21.02x
▲ VercelNitro17.627s (-1.4%)18.953s (-5.7% 🟢)1.326s21.05x

🔍 Observability: Express | Next.js (Turbopack) | Nitro

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express14.019s (-3.8%)14.627s (-2.6%)0.608s51.00x
🌐 RedisNext.js (Turbopack)14.257s (+0.9%)15.029s (~)0.772s41.02x
🐘 PostgresNext.js (Turbopack)14.412s (-0.6%)15.023s (~)0.612s41.03x
🐘 PostgresNitro14.702s (~)15.021s (~)0.319s41.05x
💻 LocalNext.js (Turbopack)14.709s (+1.0%)15.030s (~)0.322s41.05x
💻 LocalNitro14.924s (-0.6%)15.028s (-3.2%)0.104s41.06x
💻 LocalExpress14.989s (~)15.030s (~)0.041s41.07x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express33.420s (-3.4%)35.676s (-3.4%)2.256s21.00x
▲ VercelNext.js (Turbopack)34.176s (-3.3%)36.300s (-3.0%)2.124s21.02x
▲ VercelNitro34.324s (+0.8%)35.857s (-1.2%)1.533s21.03x

🔍 Observability: Express | Next.js (Turbopack) | Nitro

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express12.957s (-8.6% 🟢)13.021s (-13.3% 🟢)0.064s71.00x
🌐 RedisNext.js (Turbopack)13.389s (+1.2%)14.025s (~)0.636s71.03x
🐘 PostgresNext.js (Turbopack)13.757s (~)14.019s (~)0.262s71.06x
🐘 PostgresNitro14.192s (-0.9%)15.022s (~)0.829s61.10x
💻 LocalNext.js (Turbopack)16.033s (+2.8%)16.531s (+3.1%)0.498s61.24x
💻 LocalExpress16.441s (-0.7%)17.032s (~)0.591s61.27x
💻 LocalNitro16.531s (~)17.029s (~)0.498s61.28x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)56.887s (-2.0%)58.539s (-2.7%)1.652s21.00x
▲ VercelExpress59.072s (+7.5% 🔺)61.603s (+7.7% 🔺)2.531s21.04x
▲ VercelNitro59.188s (+2.0%)60.723s (~)1.535s21.04x

🔍 Observability: Next.js (Turbopack) | Express | Nitro

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.211s (-8.6% 🟢)2.009s (~)0.798s151.00x
🐘 PostgresNext.js (Turbopack)1.232s (~)2.009s (~)0.777s151.02x
🐘 PostgresNitro1.260s (~)2.009s (~)0.749s151.04x
🌐 RedisNext.js (Turbopack)1.318s (+6.2% 🔺)2.006s (~)0.688s151.09x
💻 LocalExpress1.515s (+0.8%)2.006s (~)0.491s151.25x
💻 LocalNitro1.516s (+2.1%)2.005s (~)0.490s151.25x
💻 LocalNext.js (Turbopack)1.544s (+4.6%)2.006s (~)0.461s151.27x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.253s (-6.7% 🟢)4.180s (+4.1%)1.927s81.00x
▲ VercelNitro2.496s (-15.3% 🟢)3.670s (-19.6% 🟢)1.174s91.11x
▲ VercelNext.js (Turbopack)2.719s (+23.5% 🔺)4.105s (+6.9% 🔺)1.386s81.21x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.300s (-2.9%)3.009s (~)0.708s101.00x
🐘 PostgresNitro2.330s (+0.7%)3.009s (~)0.680s101.01x
🐘 PostgresNext.js (Turbopack)2.385s (-0.7%)3.009s (~)0.624s101.04x
🌐 RedisNext.js (Turbopack)2.564s (+5.3% 🔺)3.008s (~)0.444s101.11x
💻 LocalExpress2.895s (+1.4%)3.453s (+3.3%)0.558s91.26x
💻 LocalNitro2.991s (+6.5% 🔺)3.341s (+11.1% 🔺)0.351s91.30x
💻 LocalNext.js (Turbopack)3.077s (+24.5% 🔺)3.884s (+29.1% 🔺)0.808s81.34x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.670s (-10.7% 🟢)4.008s (-14.7% 🟢)1.338s81.00x
▲ VercelNext.js (Turbopack)2.780s (+3.0%)4.402s (-1.8%)1.622s71.04x
▲ VercelExpress2.879s (+4.4%)4.437s (+2.9%)1.558s71.08x

🔍 Observability: Nitro | Next.js (Turbopack) | Express

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express3.383s (-2.3%)4.011s (~)0.628s81.00x
🐘 PostgresNitro3.472s (-0.5%)4.011s (~)0.538s81.03x
🐘 PostgresNext.js (Turbopack)3.643s (-1.1%)4.009s (~)0.367s81.08x
🌐 RedisNext.js (Turbopack)4.164s (+5.3% 🔺)5.011s (+16.6% 🔺)0.847s61.23x
💻 LocalExpress7.500s (-8.6% 🟢)8.021s (-8.5% 🟢)0.521s42.22x
💻 LocalNitro8.022s (+1.6%)8.773s (+6.1% 🔺)0.752s42.37x
💻 LocalNext.js (Turbopack)8.384s (+47.2% 🔺)9.019s (+50.0% 🔺)0.635s42.48x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.054s (-1.0%)5.367s (+9.7% 🔺)2.313s61.00x
▲ VercelNitro3.295s (+8.8% 🔺)4.508s (-7.1% 🟢)1.213s71.08x
▲ VercelNext.js (Turbopack)3.678s (+1.9%)5.769s (+8.0% 🔺)2.091s61.20x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.204s (-4.3%)2.008s (~)0.804s151.00x
🐘 PostgresNext.js (Turbopack)1.251s (+2.1%)2.009s (~)0.758s151.04x
🐘 PostgresNitro1.264s (~)2.008s (~)0.744s151.05x
🌐 RedisNext.js (Turbopack)1.278s (-0.6%)2.006s (~)0.729s151.06x
💻 LocalExpress1.519s (~)2.006s (~)0.487s151.26x
💻 LocalNitro1.524s (-3.4%)2.006s (~)0.482s151.27x
💻 LocalNext.js (Turbopack)1.556s (~)2.007s (-3.2%)0.451s151.29x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.003s (-4.2%)3.908s (-1.8%)1.905s81.00x
▲ VercelNitro2.006s (-7.6% 🟢)3.636s (-6.7% 🟢)1.630s91.00x
▲ VercelNext.js (Turbopack)2.217s (+6.1% 🔺)3.745s (+5.0% 🔺)1.529s91.11x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro2.329s (~)3.010s (~)0.681s101.00x
🐘 PostgresExpress2.332s (-0.9%)3.008s (~)0.676s101.00x
🐘 PostgresNext.js (Turbopack)2.390s (~)3.011s (~)0.620s101.03x
🌐 RedisNext.js (Turbopack)2.543s (+3.6%)3.007s (~)0.464s101.09x
💻 LocalNext.js (Turbopack)2.807s (-10.8% 🟢)3.341s (-6.3% 🟢)0.533s91.21x
💻 LocalExpress2.938s (-0.9%)3.454s (-3.1%)0.516s91.26x
💻 LocalNitro2.990s (-1.3%)3.677s (-2.2%)0.687s91.28x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.320s (~)4.314s (+9.6% 🔺)1.994s81.00x
▲ VercelNitro2.485s (-3.3%)3.963s (-5.7% 🟢)1.478s81.07x
▲ VercelNext.js (Turbopack)2.779s (+9.3% 🔺)4.314s (+3.7%)1.535s71.20x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro3.453s (~)4.010s (~)0.557s81.00x
🐘 PostgresExpress3.457s (~)4.009s (~)0.553s81.00x
🐘 PostgresNext.js (Turbopack)3.646s (~)4.009s (~)0.363s81.06x
🌐 RedisNext.js (Turbopack)4.169s (+3.5%)5.012s (+9.4% 🔺)0.843s61.21x
💻 LocalExpress8.033s (-6.6% 🟢)8.774s (-2.8%)0.741s42.33x
💻 LocalNitro8.669s (+2.3%)9.026s (~)0.357s42.51x
💻 LocalNext.js (Turbopack)8.695s (+12.2% 🔺)9.271s (+12.1% 🔺)0.576s42.52x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.728s (-4.1%)4.288s (-0.9%)1.560s71.00x
▲ VercelExpress3.170s (+16.4% 🔺)4.654s (+9.8% 🔺)1.484s71.16x
▲ VercelNext.js (Turbopack)3.271s (-13.0% 🟢)5.128s (-7.0% 🟢)1.857s61.20x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.608s (-26.8% 🟢)1.006s (~)0.399s601.00x
🌐 RedisNext.js (Turbopack)0.708s (+11.4% 🔺)1.004s (~)0.296s601.16x
🐘 PostgresNext.js (Turbopack)0.819s (+6.2% 🔺)1.041s (+1.7%)0.222s581.35x
🐘 PostgresNitro0.858s (+2.2%)1.023s (+1.7%)0.165s591.41x
💻 LocalNext.js (Turbopack)0.860s (-2.7%)1.005s (-3.3%)0.145s601.41x
💻 LocalNitro0.980s (-5.4% 🟢)1.115s (-44.4% 🟢)0.135s541.61x
💻 LocalExpress1.016s (+4.1%)1.654s (+48.3% 🔺)0.638s371.67x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro8.903s (-8.6% 🟢)10.501s (-8.5% 🟢)1.599s61.00x
▲ VercelNext.js (Turbopack)9.632s (-23.0% 🟢)11.579s (-17.7% 🟢)1.948s61.08x
▲ VercelExpress10.403s (+2.7%)12.297s (+1.4%)1.893s51.17x

🔍 Observability: Nitro | Next.js (Turbopack) | Express

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.477s (-26.2% 🟢)2.007s (-16.4% 🟢)0.530s451.00x
🌐 RedisNext.js (Turbopack)1.672s (+15.4% 🔺)2.006s (~)0.333s451.13x
🐘 PostgresNitro2.078s (+3.4%)2.944s (+16.1% 🔺)0.866s311.41x
🐘 PostgresNext.js (Turbopack)2.153s (+14.8% 🔺)2.821s (+37.5% 🔺)0.668s321.46x
💻 LocalNext.js (Turbopack)2.700s (~)3.008s (~)0.308s301.83x
💻 LocalNitro3.022s (-1.1%)3.547s (-9.6% 🟢)0.525s262.05x
💻 LocalExpress3.162s (+3.6%)3.967s (+4.3%)0.805s232.14x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro28.495s (-3.9%)30.408s (-3.5%)1.913s31.00x
▲ VercelExpress28.833s (-1.1%)30.922s (-1.2%)2.089s31.01x
▲ VercelNext.js (Turbopack)29.626s (-0.8%)31.356s (-2.6%)1.730s31.04x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express3.109s (-23.3% 🟢)3.790s (-20.7% 🟢)0.681s321.00x
🌐 RedisNext.js (Turbopack)3.352s (+11.2% 🔺)4.008s (+15.7% 🔺)0.656s301.08x
🐘 PostgresNext.js (Turbopack)3.994s (+5.8% 🔺)4.331s (+8.0% 🔺)0.337s281.28x
🐘 PostgresNitro4.191s (~)4.932s (-1.6%)0.741s251.35x
💻 LocalNext.js (Turbopack)8.691s (+2.0%)9.019s (~)0.328s142.80x
💻 LocalExpress9.039s (-1.3%)9.633s (-1.6%)0.594s132.91x
💻 LocalNitro9.104s (+2.1%)9.710s (+3.3%)0.606s132.93x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express73.513s (-14.6% 🟢)75.368s (-14.2% 🟢)1.855s21.00x
▲ VercelNitro75.626s (+6.1% 🔺)77.847s (+6.5% 🔺)2.221s21.03x
▲ VercelNext.js (Turbopack)77.885s (-4.4%)79.559s (-4.2%)1.675s21.06x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.226s (-18.4% 🟢)1.007s (~)0.781s601.00x
🐘 PostgresNext.js (Turbopack)0.251s (+4.8%)1.007s (~)0.757s601.11x
🐘 PostgresNitro0.281s (-3.7%)1.007s (~)0.725s601.24x
🌐 RedisNext.js (Turbopack)0.284s (+7.7% 🔺)1.004s (~)0.720s601.26x
💻 LocalNitro0.588s (+4.9%)1.004s (~)0.417s602.60x
💻 LocalNext.js (Turbopack)0.620s (+20.2% 🔺)1.022s (+1.7%)0.402s592.74x
💻 LocalExpress0.697s (+18.7% 🔺)1.096s (+9.1% 🔺)0.398s553.08x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.544s (-22.1% 🟢)3.163s (-11.6% 🟢)1.620s201.00x
▲ VercelNext.js (Turbopack)1.688s (-5.7% 🟢)3.573s (+3.3%)1.886s181.09x
▲ VercelNitro1.727s (-8.8% 🟢)3.318s (-4.5%)1.591s191.12x

🔍 Observability: Express | Next.js (Turbopack) | Nitro

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.363s (-27.3% 🟢)1.006s (~)0.643s901.00x
🐘 PostgresNitro0.493s (-1.8%)1.007s (~)0.514s901.36x
🐘 PostgresNext.js (Turbopack)0.508s (+4.6%)1.007s (~)0.499s901.40x
🌐 RedisNext.js (Turbopack)1.144s (+2.9%)2.006s (+17.7% 🔺)0.862s453.15x
💻 LocalExpress2.492s (-1.6%)3.009s (~)0.517s306.86x
💻 LocalNitro2.652s (+14.2% 🔺)3.180s (+5.7% 🔺)0.528s297.31x
💻 LocalNext.js (Turbopack)2.676s (+7.7% 🔺)3.076s (+2.2%)0.401s307.37x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.612s (-2.1%)4.132s (-6.5% 🟢)1.519s221.00x
▲ VercelExpress2.738s (-7.0% 🟢)4.568s (-3.6%)1.830s201.05x
▲ VercelNext.js (Turbopack)3.366s (-5.4% 🟢)4.890s (-7.9% 🟢)1.524s191.29x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.583s (-26.2% 🟢)1.007s (~)0.423s1201.00x
🐘 PostgresNitro0.809s (-0.9%)1.009s (~)0.200s1191.39x
🐘 PostgresNext.js (Turbopack)0.847s (+10.4% 🔺)1.043s (+3.6%)0.196s1161.45x
🌐 RedisNext.js (Turbopack)2.677s (-3.5%)3.007s (-0.8%)0.330s404.59x
💻 LocalNitro10.890s (+4.7%)11.485s (+4.1%)0.595s1118.67x
💻 LocalExpress10.935s (~)11.395s (-1.5%)0.460s1118.75x
💻 LocalNext.js (Turbopack)11.065s (+7.4% 🔺)11.572s (+7.4% 🔺)0.507s1118.97x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro6.908s (+9.6% 🔺)8.327s (+3.7%)1.419s151.00x
▲ VercelNext.js (Turbopack)47.028s (+673.5% 🔺)48.733s (+526.0% 🔺)1.705s66.81x
▲ VercelExpress49.426s (+616.1% 🔺)51.745s (+489.1% 🔺)2.320s77.15x

🔍 Observability: Nitro | Next.js (Turbopack) | Express

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.158s (-25.4% 🟢)1.001s (+0.5%)0.001s (-26.7% 🟢)1.009s (~)0.851s101.00x
💻 LocalNext.js (Turbopack)0.170s (+0.5%)1.002s (~)0.012s (+14.7% 🔺)1.017s (~)0.847s101.07x
🐘 PostgresNext.js (Turbopack)0.192s (+0.8%)1.001s (~)0.001s (-15.4% 🟢)1.010s (~)0.818s101.21x
💻 LocalNitro0.207s (-0.7%)1.004s (~)0.012s (+11.5% 🔺)1.017s (~)0.810s101.31x
💻 LocalExpress0.219s (+6.8% 🔺)1.004s (~)0.011s (-8.5% 🟢)1.017s (~)0.798s101.38x
🐘 PostgresNitro0.219s (+2.0%)0.996s (~)0.001s (+30.0% 🔺)1.012s (~)0.792s101.39x
🌐 RedisNext.js (Turbopack)0.242s (+62.9% 🔺)1.001s (~)0.032s (+1776.5% 🔺)1.037s (+3.0%)0.796s101.53x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)1.493s (-11.9% 🟢)2.833s (-11.4% 🟢)0.461s (-51.8% 🟢)3.696s (-20.0% 🟢)2.203s101.00x
▲ VercelExpress1.565s (+3.1%)2.942s (+1.1%)0.642s (-16.1% 🟢)4.033s (-2.6%)2.468s101.05x
▲ VercelNitro1.603s (-5.5% 🟢)2.899s (-11.0% 🟢)0.562s (+7.0% 🔺)3.812s (-9.8% 🟢)2.208s101.07x

🔍 Observability: Next.js (Turbopack) | Express | Nitro

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)0.481s (+14.5% 🔺)1.002s (~)0.003s (+4.8%)1.011s (~)0.530s601.00x
🐘 PostgresExpress0.522s (-16.5% 🟢)1.006s (~)0.004s (-10.2% 🟢)1.022s (~)0.500s591.09x
🐘 PostgresNitro0.629s (-2.0%)1.005s (~)0.004s (-11.2% 🟢)1.022s (~)0.393s591.31x
🐘 PostgresNext.js (Turbopack)0.659s (+9.3% 🔺)1.026s (+1.7%)0.008s (+110.1% 🔺)1.044s (+2.2%)0.385s581.37x
💻 LocalNext.js (Turbopack)0.763s (+13.8% 🔺)1.011s (~)0.009s (-15.6% 🟢)1.116s (+8.9% 🔺)0.353s541.59x
💻 LocalNitro0.815s (-14.6% 🟢)1.012s (~)0.010s (+2.4%)1.111s (-9.5% 🟢)0.296s571.70x
💻 LocalExpress0.847s (+18.5% 🔺)1.010s (~)0.010s (+6.7% 🔺)1.115s (+8.9% 🔺)0.268s541.76x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.092s (+1.5%)5.761s (~)0.242s (-2.7%)6.436s (-0.6%)2.344s101.00x
▲ VercelNext.js (Turbopack)4.186s (-6.5% 🟢)6.079s (-0.6%)0.220s (+9.9% 🔺)6.720s (~)2.534s91.02x
▲ VercelNitro4.499s (+6.2% 🔺)5.768s (+2.8%)0.457s (+71.8% 🔺)6.725s (+5.7% 🔺)2.226s91.10x

🔍 Observability: Express | Next.js (Turbopack) | Nitro

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)0.900s (+1.8%)1.035s (+3.5%)0.000s (+106.9% 🔺)1.039s (+3.4%)0.139s581.00x
🐘 PostgresExpress0.931s (-3.9%)1.146s (-5.9% 🟢)0.000s (+172.2% 🔺)1.159s (-6.0% 🟢)0.228s541.03x
🐘 PostgresNitro0.946s (~)1.220s (+12.0% 🔺)0.000s (-17.3% 🟢)1.232s (+10.3% 🔺)0.286s491.05x
🐘 PostgresNext.js (Turbopack)0.956s (+3.1%)1.177s (+5.6% 🔺)0.000s (+Infinity% 🔺)1.185s (+4.2%)0.229s511.06x
💻 LocalNitro1.218s (+2.4%)2.021s (~)0.000s (-7.1% 🟢)2.023s (~)0.805s301.35x
💻 LocalNext.js (Turbopack)1.256s (~)2.019s (~)0.000s (-11.1% 🟢)2.022s (~)0.767s301.40x
💻 LocalExpress1.444s (+17.6% 🔺)2.022s (~)0.001s (+14.8% 🔺)2.203s (+8.8% 🔺)0.758s281.60x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.826s (~)4.141s (+4.9%)0.000s (+150.0% 🔺)4.585s (+4.0%)1.759s141.00x
▲ VercelNitro2.860s (+7.4% 🔺)4.133s (+4.8%)0.000s (-50.0% 🟢)4.541s (+3.8%)1.681s141.01x
▲ VercelNext.js (Turbopack)3.188s (-28.9% 🟢)4.619s (-22.8% 🟢)0.000s (-100.0% 🟢)5.035s (-21.8% 🟢)1.847s121.13x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

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

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)1.728s (-1.9%)2.035s (+1.7%)0.000s (-50.0% 🟢)2.039s (+1.6%)0.311s301.00x
🐘 PostgresExpress1.739s (-1.1%)2.143s (+3.8%)0.000s (-65.5% 🟢)2.152s (+3.4%)0.413s281.01x
🐘 PostgresNitro1.789s (+2.3%)2.102s (~)0.000s (+Infinity% 🔺)2.113s (-1.0%)0.324s291.04x
🐘 PostgresNext.js (Turbopack)1.958s (+6.4% 🔺)2.147s (+0.6%)0.000s (+Infinity% 🔺)2.155s (~)0.197s281.13x
💻 LocalNitro3.410s (+6.4% 🔺)4.034s (+1.7%)0.001s (+140.0% 🔺)4.037s (+1.7%)0.627s151.97x
💻 LocalNext.js (Turbopack)3.725s (+3.2%)4.233s (+1.6%)0.000s (-12.5% 🟢)4.236s (+1.6%)0.511s152.16x
💻 LocalExpress3.802s (+9.2% 🔺)4.368s (+8.3% 🔺)0.001s (+233.3% 🔺)4.372s (+8.3% 🔺)0.570s152.20x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.969s (-25.3% 🟢)5.072s (-23.7% 🟢)0.000s (-62.5% 🟢)5.489s (-22.2% 🟢)1.521s121.00x
▲ VercelNext.js (Turbopack)3.988s5.498s0.000s5.941s1.953s111.01x
▲ VercelExpress5.598s (+32.4% 🔺)7.223s (+32.7% 🔺)0.000s (-31.3% 🟢)7.670s (+30.2% 🔺)2.072s81.41x

🔍 Observability: Nitro | Next.js (Turbopack) | Express

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNext.js (Turbopack)10/21
🐘 PostgresExpress19/21
▲ VercelExpress12/21
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express🐘 Postgres17/21
Next.js (Turbopack)🌐 Redis10/21
Nitro🐘 Postgres15/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)

📋 View full workflow run

CopilotAI 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.

Pull request overview

Adds a workflow-mode optimization to the SWC plugin so that after "use step" class members are stripped, any now-unreferenced private class members are also removed, improving downstream tree-shaking (e.g., dropping Node-only imports kept alive by unused helpers).

Changes:

  • Implement iterative private-member reachability analysis and removal after step-member stripping (class decls + class exprs).
  • Add a new transform fixture validating cascading elimination of TypeScript private members in workflow mode.
  • Document the new optimization and publish a patch changeset.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 5 comments.

Show a summary per file
FileDescription
packages/swc-plugin-workflow/transform/src/lib.rsAdds private-member reference collection + DCE pass after "use step" stripping.
packages/swc-plugin-workflow/transform/tests/fixture/private-member-dce/input.tsNew fixture input covering cascading TS private elimination.
packages/swc-plugin-workflow/transform/tests/fixture/private-member-dce/output-workflow.jsExpected workflow output with TS-private members removed and imports dropped.
packages/swc-plugin-workflow/transform/tests/fixture/private-member-dce/output-step.jsExpected step-mode output (private members retained).
packages/swc-plugin-workflow/transform/tests/fixture/private-member-dce/output-client.jsExpected client-mode output (private members retained).
packages/swc-plugin-workflow/spec.mdDocuments the private-member DCE behavior and example.
.changeset/private-member-dce.mdDeclares a patch release for the SWC plugin change.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +619 to +624
// TS private or any member: `this.foo` — only track when
// the object is `this` (to avoid false positives from
// unrelated member accesses like `obj.foo`)
MemberProp::Ident(ident) => {
if matches!(&*expr.obj, Expr::This(_)) {
self.referenced.insert(ident.sym.to_string());

CopilotAIApr 9, 2026

Copy link

Choose a reason for hiding this comment

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

ClassMemberRefCollector only records MemberProp::Ident accesses when the object is this. In TypeScript, private members are also legally accessed via other instances of the same class (e.g. static compare(a: C, b: C) { return a.x - b.x }). With the current check, those references won’t be collected and the private member can be incorrectly removed, breaking runtime behavior. Consider treating any .ident access as a reference when ident matches a known TS-private member name in the class, regardless of the receiver expression (safe, though more conservative).

Suggested change
// TS private or any member: `this.foo` — only track when
// the object is `this` (to avoid false positives from
// unrelated member accesses like `obj.foo`)
MemberProp::Ident(ident) => {
ifmatches!(&*expr.obj,Expr::This(_)){
self.referenced.insert(ident.sym.to_string());
// TS private or any member access. Track `this.foo` as before,
// and also track `obj.foo` when `foo` is a known TS-private
// member of the current class so same-class accesses like
// `a.x` / `b.x` are not missed.
MemberProp::Ident(ident) => {
let name = ident.sym.to_string();
ifmatches!(&*expr.obj,Expr::This(_)) || self.private_names.contains(&name){
self.referenced.insert(name);

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Fixed. The collector now holds a ts_private_names: HashSet<String> built from the class body. MemberProp::Ident accesses are tracked when the name is in that set, regardless of the receiver expression. This catches a.x / b.x patterns in static methods.

Comment on lines +629 to +630
// Continue visiting children
expr.obj.visit_with(self);

CopilotAIApr 9, 2026

Copy link

Choose a reason for hiding this comment

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

visit_member_expr overrides default traversal but only visits expr.obj. For computed member expressions (obj[expr]), the property expression is not visited, so any this.#x / this.x references inside the computed key won’t be collected and could cause incorrectly eliminating private members. Use expr.visit_children_with(self) (or explicitly visit the computed property expression) to preserve full traversal.

Suggested change
// Continue visiting children
expr.obj.visit_with(self);
// Continue visiting children, including computed property expressions.
expr.visit_children_with(self);

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Fixed. Changed to expr.visit_children_with(self) which visits both the object and property expressions, including computed keys.

Comment on lines +614 to +625
match &expr.prop {
// Native JS private: `this.#foo`
MemberProp::PrivateName(name) => {
self.referenced.insert(name.name.to_string());
}
// TS private or any member: `this.foo` — only track when
// the object is `this` (to avoid false positives from
// unrelated member accesses like `obj.foo`)
MemberProp::Ident(ident) => {
if matches!(&*expr.obj, Expr::This(_)) {
self.referenced.insert(ident.sym.to_string());
}

CopilotAIApr 9, 2026

Copy link

Choose a reason for hiding this comment

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

Native private #foo and public/TS member foo are both stored as the same string ("foo"). If a class contains both foo and #foo, a this.foo access will incorrectly mark #foo as referenced, preventing intended elimination. Consider encoding JS-private names distinctly (e.g. prefix with # or store an enum) so #foo and foo don’t collide.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Fixed. JS native private names are now stored with a # prefix (e.g. "#foo") while TS private names remain unprefixed ("foo"). private_member_name() returns the prefixed form for PrivateMethod/PrivateProp, and visit_member_expr inserts format!("#{}", name) for MemberProp::PrivateName.

Comment on lines +1 to +25
import { WORKFLOW_SERIALIZE, WORKFLOW_DESERIALIZE } from '@workflow/serde';
import { getWorld } from './world.js';
import { importKey } from './encryption.js';

export class Run {
static [WORKFLOW_SERIALIZE](instance: Run) {
return { id: instance.id };
}

static [WORKFLOW_DESERIALIZE](data: { id: string }) {
return new Run(data.id);
}

id: string;

// TS private field — only referenced by stripped methods
private encryptionKeyPromise: Promise<any> | null = null;

// TS private method — only called by stripped getters/methods
private async getEncryptionKey(): Promise<any> {
if (!this.encryptionKeyPromise) {
this.encryptionKeyPromise = importKey(this.id);
}
return this.encryptionKeyPromise;
}

CopilotAIApr 9, 2026

Copy link

Choose a reason for hiding this comment

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

The new DCE logic claims to support native JS private members (#field/#method()), but the fixture only covers TypeScript private. Adding a fixture that uses #private syntax (including a cascading case) would help prevent regressions in the JS-private path.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Added in the second commit: private-member-dce-native/input.js tests JS native #field and #method() DCE including cascading elimination (#encryptionKeyPromise eliminated because #getEncryptionKey is eliminated) and survival of referenced members (#label kept because toString() references it).

Comment on lines +8504 to +8510
// Dead-code-eliminate unreferenced private members
// (same logic as visit_mut_class_decl above)
let referenced =
ClassMemberRefCollector::collect_from_class_body(&class_expr.class.body);
class_expr.class.body.retain(|member| match member {
ClassMember::PrivateMethod(m) => referenced.contains(&m.key.name.to_string()),
ClassMember::PrivateProp(p) => referenced.contains(&p.key.name.to_string()),

CopilotAIApr 9, 2026

Copy link

Choose a reason for hiding this comment

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

The private-member retention filter is duplicated in both visit_mut_class_decl and visit_mut_class_expr. This duplication increases the risk of future divergence/bugs when the logic changes. Consider extracting the retain logic into a shared helper (e.g. fn retain_referenced_private_members(body: &mut Vec<ClassMember>)) used by both visitors.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Fixed. Extracted retain_referenced_private_members(body: &mut Vec<ClassMember>) on ClassMemberRefCollector, called from both visit_mut_class_decl and visit_mut_class_expr.

- Namespace JS native private names with # prefix to avoid collisions
with TS private members of the same name
- Track TS private member accesses on non-this receivers (e.g. a.x in
static methods) by maintaining a set of known TS-private names
- Use visit_children_with for full traversal including computed member
expressions
- Extract retain logic into shared retain_referenced_private_members()
helper used by both visit_mut_class_decl and visit_mut_class_expr

@VaguelySeriousVaguelySerious left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

AI review: no blocking issues

// accesses like `a.x` / `b.x` are not missed.
MemberProp::Ident(ident) => {
let name = ident.sym.to_string();
if matches!(&*expr.obj, Expr::This(_)) || self.ts_private_names.contains(&name) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

AI Review: Note

For MemberProp::Ident, any this.foo access records foo in referenced, not only TS-private members. Retain still keys off private_member_name, so this is mostly extra churn in the set rather than incorrect retention, but it is slightly less precise than filtering to private names only.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Correct - the collector is conservative: it records all this.foo accesses, not just those matching known private names. This is intentional for simplicity. The extra entries in the referenced set are harmless since retain only removes members that private_member_name() identifies as private. Public members are always kept regardless of what's in the set.

});
```

This optimization is critical for SDK classes like `Run` where private helper methods reference Node.js-only imports (encryption, world access, etc.) — eliminating them allows the downstream module-level DCE to also remove those imports from the workflow bundle.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

AI Review: Nit

This section explains the algorithm well. If you want to reduce future issue reports, a short explicit note on unsupported patterns (for example computed this[...] access to private fields) could help set expectations.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Good suggestion. Computed access like this[name] where name happens to be a private member name won't be tracked, which could lead to incorrect elimination. In practice this pattern is very rare for private members (especially in SDK code like Run), but worth noting if we want to document limitations.

@TooTallNate
TooTallNate enabled auto-merge (squash) April 9, 2026 19:22
@TooTallNate
TooTallNate merged commit 66585fd into mainApr 9, 2026
102 of 103 checks passed
@TooTallNate
TooTallNate deleted the fix/swc-private-member-dce branch April 9, 2026 19:27
@ghostghost mentioned this pull request Apr 9, 2026
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

@TooTallNate@VaguelySerious
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(swc): dead code eliminate unreferenced private class members in workflow mode - #1671

Merged
TooTallNate merged 3 commits into
mainfrom
fix/swc-private-member-dce
Apr 9, 2026
Merged

feat(swc): dead code eliminate unreferenced private class members in workflow mode#1671
TooTallNate merged 3 commits into
mainfrom
fix/swc-private-member-dce

Conversation

@TooTallNate

@TooTallNateTooTallNate commented Apr 9, 2026

Copy link
Copy Markdown
Member

Summary

After stripping "use step" methods from a class body in workflow mode, eliminate private members that are no longer referenced by any remaining public member. This applies to both:

  • JS native private: #field, #method()
  • TypeScript private: private field, private method()

The algorithm is iterative — references are seeded from public members, then expanded through surviving private members until a fixed point, enabling cascading elimination.

Example

Input:

exportclassRun{static[WORKFLOW_SERIALIZE](instance){return{id: instance.id};}static[WORKFLOW_DESERIALIZE](data){returnnewRun(data.id);}id: string;privateencryptionKeyPromise: Promise<any>|null=null;privateasyncgetEncryptionKey(){if(!this.encryptionKeyPromise){this.encryptionKeyPromise=importKey(this.id);}returnthis.encryptionKeyPromise;}constructor(id: string){this.id=id;}getvalue(): Promise<any>{'use step';returnthis.getEncryptionKey().then(()=>getWorld().get(this.id));}asynccancel(): Promise<void>{'use step';constkey=awaitthis.getEncryptionKey();awaitgetWorld().cancel(this.id,key);}toString(): string{return`Run(${this.id})`;}}

Workflow mode output:

exportclassRun{static[WORKFLOW_SERIALIZE](instance){return{id: instance.id};}static[WORKFLOW_DESERIALIZE](data){returnnewRun(data.id);}id;// ✅ private encryptionKeyPromise — ELIMINATED (only referenced by getEncryptionKey)// ✅ private getEncryptionKey() — ELIMINATED (only referenced by stripped step methods)constructor(id){this.id=id;}toString(){return`Run(${this.id})`;}}Run.prototype["cancel"]=globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//...");var__step_Run$value=globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//...");Object.defineProperty(Run.prototype,"value",{get(){return__step_Run$value.call(this);},configurable: true,enumerable: false});

The cascading elimination is key: encryptionKeyPromise is only referenced by getEncryptionKey(), which is itself only referenced by the stripped value getter and cancel() method. Both private members are removed, allowing the downstream module-level DCE to also eliminate the importKey and getWorld imports.

Motivation

SDK classes like Run have private helper methods that reference Node.js-only imports (encryption, world access). Without this optimization, those helpers survive into the workflow bundle even though nothing calls them after "use step" bodies are stripped, keeping the Node.js imports alive and preventing tree-shaking.

Test

New fixture: private-member-dce/input.ts with expected outputs for all three modes.

…workflow mode
After stripping 'use step' methods from a class body in workflow mode,
eliminate private members (both JS native #field/#method and TypeScript
private field/private method) that are no longer referenced by any
remaining public member.
The algorithm is iterative: references are seeded from public members,
then expanded through surviving private members until a fixed point,
enabling cascading elimination (e.g. a private field only referenced by
a private method that is itself unreferenced).
CopilotAI review requested due to automatic review settings April 9, 2026 18:09
@TooTallNate
TooTallNate requested a review from a team as a code ownerApril 9, 2026 18:09
@changeset-bot

changeset-botBot commented Apr 9, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 4ea576a

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

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

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 Apr 9, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Apr 9, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production912067979
✅ 💻 Local Development88601821068
✅ 📦 Local Production88601821068
✅ 🐘 Local Postgres88601821068
✅ 🪟 Windows810889
❌ 🌍 Community Worlds1366524225
✅ 📋 Other225042267
Total4012656874764

❌ Failed Tests

🌍 Community Worlds (65 failed)

mongodb (4 failed):

  • hookWorkflow is not resumable via public webhook endpoint | wrun_01KNSR613X7J6NJ3Q7CC1GAXC8
  • webhookWorkflow | wrun_01KNSR6A2N8E4FRA7K4W0WAJ2X
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously | wrun_01KNSRE4P2HFBR1HGDZ60FSZ4A
  • resilient start: addTenWorkflow completes when run_created returns 500 | wrun_01KNSRMTFPHDMEWW1M0C99YMJB

redis (3 failed):

  • hookWorkflow is not resumable via public webhook endpoint | wrun_01KNSR613X7J6NJ3Q7CC1GAXC8
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously | wrun_01KNSRE4P2HFBR1HGDZ60FSZ4A
  • resilient start: addTenWorkflow completes when run_created returns 500 | wrun_01KNSRMTFPHDMEWW1M0C99YMJB

turso (58 failed):

  • addTenWorkflow | wrun_01KNSR4Q12NYS08Q5M8JMBJE2P
  • addTenWorkflow | wrun_01KNSR4Q12NYS08Q5M8JMBJE2P
  • wellKnownAgentWorkflow (.well-known/agent) | wrun_01KNSR6H7RBB0VN5AF50A8K3HK
  • should work with react rendering in step
  • promiseAllWorkflow | wrun_01KNSR50EQH830PRD40KS32Q9Y
  • promiseRaceWorkflow | wrun_01KNSR54Y75RYQDBMAWK470VYP
  • promiseAnyWorkflow | wrun_01KNSR58C03DCWT8KGXWBMNCP6
  • importedStepOnlyWorkflow | wrun_01KNSR6WGHHN8XTHMJ86Q1JS10
  • hookWorkflow | wrun_01KNSR5M9AWG051SF57JDHTN15
  • hookWorkflow is not resumable via public webhook endpoint | wrun_01KNSR613X7J6NJ3Q7CC1GAXC8
  • webhookWorkflow | wrun_01KNSR6A2N8E4FRA7K4W0WAJ2X
  • sleepingWorkflow | wrun_01KNSR6G7XQP06VPNVA5S5SZ7P
  • parallelSleepWorkflow | wrun_01KNSR6W68GVQAECF6EXH5YKW4
  • nullByteWorkflow | wrun_01KNSR6ZZHZA6GX9A995NWGH4W
  • workflowAndStepMetadataWorkflow | wrun_01KNSR722J4AE22DPWYBGR40AA
  • fetchWorkflow | wrun_01KNSR9QTD0RQEWYMKGRM9P6KS
  • promiseRaceStressTestWorkflow | wrun_01KNSR9V7MWTQB99MYWGDBMPV1
  • error handling error propagation workflow errors nested function calls preserve message and stack trace
  • error handling error propagation workflow errors cross-file imports preserve message and stack trace
  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior FatalError fails immediately without retries
  • error handling retry behavior RetryableError respects custom retryAfter delay
  • error handling retry behavior maxRetries=0 disables retries
  • error handling catchability FatalError can be caught and detected with FatalError.is()
  • error handling not registered WorkflowNotRegisteredError fails the run when workflow does not exist
  • error handling not registered StepNotRegisteredError fails the step but workflow can catch it
  • error handling not registered StepNotRegisteredError fails the run when not caught in workflow
  • hookCleanupTestWorkflow - hook token reuse after workflow completion | wrun_01KNSRDFYWY5RDPW5MJW4M2RKP
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously | wrun_01KNSRE4P2HFBR1HGDZ60FSZ4A
  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running | wrun_01KNSREVJW6G5G63ZYP9DTG2CT
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars) | wrun_01KNSRFFZ3HX99QC8VH9X00X19
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument | wrun_01KNSRFS1Y9106Z5PVYGW5SZ8F
  • closureVariableWorkflow - nested step functions with closure variables | wrun_01KNSRFYM8QZ6MKBWEXNSXEA1E
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step | wrun_01KNSRG1102E8CS370B44Y7JJ1
  • health check (queue-based) - workflow and step endpoints respond to health check messages
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly | wrun_01KNSRGH373GH51TWFMF3M6THG
  • Calculator.calculate - static workflow method using static step methods from another class | wrun_01KNSRGPSM4VSQ1R3Z11NG4MZ0
  • AllInOneService.processNumber - static workflow method using sibling static step methods | wrun_01KNSRGXK317X0HN019FNHP541
  • ChainableService.processWithThis - static step methods using this to reference the class | wrun_01KNSRH5K54HKGJC6TW5YDC6P4
  • thisSerializationWorkflow - step function invoked with .call() and .apply() | wrun_01KNSRHCE7T09PPS45FA7D75MK
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE | wrun_01KNSRHM1A0G2BFRKSN1893XHR
  • instanceMethodStepWorkflow - instance methods with "use step" directive | wrun_01KNSRHTZQJP99YC0M9FNWQPM5
  • crossContextSerdeWorkflow - classes defined in step code are deserializable in workflow context | wrun_01KNSRJ68YGFBGXJ7YFR7ZHMV5
  • stepFunctionAsStartArgWorkflow - step function reference passed as start() argument | wrun_01KNSRJFBR9XRH3012W64TDAJ1
  • cancelRun - cancelling a running workflow | wrun_01KNSRJP4YQBW4BDB9ZFHE3JXZ
  • cancelRun via CLI - cancelling a running workflow | wrun_01KNSRK06K5725SZWQJ2DJZZ1K
  • pages router addTenWorkflow via pages router
  • pages router promiseAllWorkflow via pages router
  • pages router sleepingWorkflow via pages router
  • hookWithSleepWorkflow - hook payloads delivered correctly with concurrent sleep | wrun_01KNSRKDA53YRF17ST7YTD02BW
  • sleepInLoopWorkflow - sleep inside loop with steps actually delays each iteration | wrun_01KNSRM3T179X2E69VG894KAX9
  • sleepWithSequentialStepsWorkflow - sequential steps work with concurrent sleep (control) | wrun_01KNSRME2NVXNSBMBR8AND2DKJ
  • importMetaUrlWorkflow - import.meta.url is available in step bundles | wrun_01KNSRMP16CE789X5BG39DBJEA
  • metadataFromHelperWorkflow - getWorkflowMetadata/getStepMetadata work from module-level helper (#1577) | wrun_01KNSRMR5GRYXW87SN5A0AM9TV
  • resilient start: addTenWorkflow completes when run_created returns 500 | wrun_01KNSRMTFPHDMEWW1M0C99YMJB
  • getterStepWorkflow - getter functions with "use step" directive | wrun_01KNSRMXQTQ6GKDFR5NN8DMJRA

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro8207
✅ example8207
✅ express8207
✅ fastify8207
✅ hono8207
✅ nextjs-turbopack8702
✅ nextjs-webpack8702
✅ nitro8207
✅ nuxt8207
✅ sveltekit8207
✅ vite8207
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable75014
✅ express-stable75014
✅ fastify-stable75014
✅ hono-stable75014
✅ nextjs-turbopack-canary62027
✅ nextjs-turbopack-stable8108
✅ nextjs-webpack-canary62027
✅ nextjs-webpack-stable8108
✅ nitro-stable75014
✅ nuxt-stable75014
✅ sveltekit-stable75014
✅ vite-stable75014
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable75014
✅ express-stable75014
✅ fastify-stable75014
✅ hono-stable75014
✅ nextjs-turbopack-canary62027
✅ nextjs-turbopack-stable8108
✅ nextjs-webpack-canary62027
✅ nextjs-webpack-stable8108
✅ nitro-stable75014
✅ nuxt-stable75014
✅ sveltekit-stable75014
✅ vite-stable75014
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable75014
✅ express-stable75014
✅ fastify-stable75014
✅ hono-stable75014
✅ nextjs-turbopack-canary62027
✅ nextjs-turbopack-stable8108
✅ nextjs-webpack-canary62027
✅ nextjs-webpack-stable8108
✅ nitro-stable75014
✅ nuxt-stable75014
✅ sveltekit-stable75014
✅ vite-stable75014
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack8108
❌ 🌍 Community Worlds
AppPassedFailedSkipped
✅ mongodb-dev500
❌ mongodb5848
✅ redis-dev500
❌ redis5938
✅ turso-dev500
❌ turso4588
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable75014
✅ e2e-local-postgres-nest-stable75014
✅ e2e-local-prod-nest-stable75014

📋 View full workflow run

@github-actions

github-actionsBot commented Apr 9, 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🥇 Express0.043s (-1.6%)1.006s (~)0.962s101.00x
💻 LocalNitro0.044s (-4.8%)1.005s (~)0.961s101.01x
🐘 PostgresExpress0.046s (-22.5% 🟢)1.011s (~)0.965s101.07x
💻 LocalNext.js (Turbopack)0.049s (+3.6%)1.005s (~)0.957s101.12x
🌐 RedisNext.js (Turbopack)0.050s (-11.3% 🟢)1.005s (~)0.955s101.16x
🐘 PostgresNext.js (Turbopack)0.058s (-3.0%)1.011s (~)0.954s101.33x
🐘 PostgresNitro0.063s (+2.3%)1.012s (~)0.949s101.46x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express0.231s (-39.8% 🟢)2.203s (-10.5% 🟢)1.972s101.00x
▲ VercelNext.js (Turbopack)0.267s (-33.8% 🟢)2.195s (-8.4% 🟢)1.928s101.16x
▲ VercelNitro0.304s (+10.2% 🔺)2.211s (+4.0%)1.907s101.32x

🔍 Observability: Express | Next.js (Turbopack) | Nitro

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.119s (-3.1%)2.010s (~)0.891s101.00x
🌐 RedisNext.js (Turbopack)1.126s (~)2.007s (~)0.881s101.01x
💻 LocalNext.js (Turbopack)1.126s (~)2.006s (~)0.880s101.01x
💻 LocalNitro1.128s (~)2.005s (~)0.877s101.01x
💻 LocalExpress1.131s (~)2.006s (~)0.875s101.01x
🐘 PostgresNext.js (Turbopack)1.147s (~)2.009s (~)0.862s101.02x
🐘 PostgresNitro1.147s (-0.6%)2.011s (~)0.864s101.02x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.911s (+1.0%)3.886s (+0.8%)1.975s101.00x
▲ VercelNitro2.003s (+6.5% 🔺)3.356s (-10.3% 🟢)1.353s101.05x
▲ VercelNext.js (Turbopack)2.103s (+4.0%)3.881s (+3.0%)1.778s101.10x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express10.694s (-2.1%)11.028s (~)0.334s31.00x
🌐 RedisNext.js (Turbopack)10.779s (+0.7%)11.023s (~)0.244s31.01x
💻 LocalNext.js (Turbopack)10.831s (~)11.025s (~)0.194s31.01x
🐘 PostgresNext.js (Turbopack)10.849s (~)11.023s (~)0.174s31.01x
💻 LocalNitro10.920s (~)11.023s (~)0.103s31.02x
🐘 PostgresNitro10.934s (~)11.023s (~)0.088s31.02x
💻 LocalExpress10.952s (~)11.024s (~)0.072s31.02x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express16.776s (~)18.801s (-1.4%)2.025s21.00x
▲ VercelNext.js (Turbopack)17.133s (-0.8%)19.157s (~)2.024s21.02x
▲ VercelNitro17.627s (-1.4%)18.953s (-5.7% 🟢)1.326s21.05x

🔍 Observability: Express | Next.js (Turbopack) | Nitro

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express14.019s (-3.8%)14.627s (-2.6%)0.608s51.00x
🌐 RedisNext.js (Turbopack)14.257s (+0.9%)15.029s (~)0.772s41.02x
🐘 PostgresNext.js (Turbopack)14.412s (-0.6%)15.023s (~)0.612s41.03x
🐘 PostgresNitro14.702s (~)15.021s (~)0.319s41.05x
💻 LocalNext.js (Turbopack)14.709s (+1.0%)15.030s (~)0.322s41.05x
💻 LocalNitro14.924s (-0.6%)15.028s (-3.2%)0.104s41.06x
💻 LocalExpress14.989s (~)15.030s (~)0.041s41.07x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express33.420s (-3.4%)35.676s (-3.4%)2.256s21.00x
▲ VercelNext.js (Turbopack)34.176s (-3.3%)36.300s (-3.0%)2.124s21.02x
▲ VercelNitro34.324s (+0.8%)35.857s (-1.2%)1.533s21.03x

🔍 Observability: Express | Next.js (Turbopack) | Nitro

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express12.957s (-8.6% 🟢)13.021s (-13.3% 🟢)0.064s71.00x
🌐 RedisNext.js (Turbopack)13.389s (+1.2%)14.025s (~)0.636s71.03x
🐘 PostgresNext.js (Turbopack)13.757s (~)14.019s (~)0.262s71.06x
🐘 PostgresNitro14.192s (-0.9%)15.022s (~)0.829s61.10x
💻 LocalNext.js (Turbopack)16.033s (+2.8%)16.531s (+3.1%)0.498s61.24x
💻 LocalExpress16.441s (-0.7%)17.032s (~)0.591s61.27x
💻 LocalNitro16.531s (~)17.029s (~)0.498s61.28x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)56.887s (-2.0%)58.539s (-2.7%)1.652s21.00x
▲ VercelExpress59.072s (+7.5% 🔺)61.603s (+7.7% 🔺)2.531s21.04x
▲ VercelNitro59.188s (+2.0%)60.723s (~)1.535s21.04x

🔍 Observability: Next.js (Turbopack) | Express | Nitro

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.211s (-8.6% 🟢)2.009s (~)0.798s151.00x
🐘 PostgresNext.js (Turbopack)1.232s (~)2.009s (~)0.777s151.02x
🐘 PostgresNitro1.260s (~)2.009s (~)0.749s151.04x
🌐 RedisNext.js (Turbopack)1.318s (+6.2% 🔺)2.006s (~)0.688s151.09x
💻 LocalExpress1.515s (+0.8%)2.006s (~)0.491s151.25x
💻 LocalNitro1.516s (+2.1%)2.005s (~)0.490s151.25x
💻 LocalNext.js (Turbopack)1.544s (+4.6%)2.006s (~)0.461s151.27x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.253s (-6.7% 🟢)4.180s (+4.1%)1.927s81.00x
▲ VercelNitro2.496s (-15.3% 🟢)3.670s (-19.6% 🟢)1.174s91.11x
▲ VercelNext.js (Turbopack)2.719s (+23.5% 🔺)4.105s (+6.9% 🔺)1.386s81.21x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.300s (-2.9%)3.009s (~)0.708s101.00x
🐘 PostgresNitro2.330s (+0.7%)3.009s (~)0.680s101.01x
🐘 PostgresNext.js (Turbopack)2.385s (-0.7%)3.009s (~)0.624s101.04x
🌐 RedisNext.js (Turbopack)2.564s (+5.3% 🔺)3.008s (~)0.444s101.11x
💻 LocalExpress2.895s (+1.4%)3.453s (+3.3%)0.558s91.26x
💻 LocalNitro2.991s (+6.5% 🔺)3.341s (+11.1% 🔺)0.351s91.30x
💻 LocalNext.js (Turbopack)3.077s (+24.5% 🔺)3.884s (+29.1% 🔺)0.808s81.34x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.670s (-10.7% 🟢)4.008s (-14.7% 🟢)1.338s81.00x
▲ VercelNext.js (Turbopack)2.780s (+3.0%)4.402s (-1.8%)1.622s71.04x
▲ VercelExpress2.879s (+4.4%)4.437s (+2.9%)1.558s71.08x

🔍 Observability: Nitro | Next.js (Turbopack) | Express

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express3.383s (-2.3%)4.011s (~)0.628s81.00x
🐘 PostgresNitro3.472s (-0.5%)4.011s (~)0.538s81.03x
🐘 PostgresNext.js (Turbopack)3.643s (-1.1%)4.009s (~)0.367s81.08x
🌐 RedisNext.js (Turbopack)4.164s (+5.3% 🔺)5.011s (+16.6% 🔺)0.847s61.23x
💻 LocalExpress7.500s (-8.6% 🟢)8.021s (-8.5% 🟢)0.521s42.22x
💻 LocalNitro8.022s (+1.6%)8.773s (+6.1% 🔺)0.752s42.37x
💻 LocalNext.js (Turbopack)8.384s (+47.2% 🔺)9.019s (+50.0% 🔺)0.635s42.48x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.054s (-1.0%)5.367s (+9.7% 🔺)2.313s61.00x
▲ VercelNitro3.295s (+8.8% 🔺)4.508s (-7.1% 🟢)1.213s71.08x
▲ VercelNext.js (Turbopack)3.678s (+1.9%)5.769s (+8.0% 🔺)2.091s61.20x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.204s (-4.3%)2.008s (~)0.804s151.00x
🐘 PostgresNext.js (Turbopack)1.251s (+2.1%)2.009s (~)0.758s151.04x
🐘 PostgresNitro1.264s (~)2.008s (~)0.744s151.05x
🌐 RedisNext.js (Turbopack)1.278s (-0.6%)2.006s (~)0.729s151.06x
💻 LocalExpress1.519s (~)2.006s (~)0.487s151.26x
💻 LocalNitro1.524s (-3.4%)2.006s (~)0.482s151.27x
💻 LocalNext.js (Turbopack)1.556s (~)2.007s (-3.2%)0.451s151.29x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.003s (-4.2%)3.908s (-1.8%)1.905s81.00x
▲ VercelNitro2.006s (-7.6% 🟢)3.636s (-6.7% 🟢)1.630s91.00x
▲ VercelNext.js (Turbopack)2.217s (+6.1% 🔺)3.745s (+5.0% 🔺)1.529s91.11x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro2.329s (~)3.010s (~)0.681s101.00x
🐘 PostgresExpress2.332s (-0.9%)3.008s (~)0.676s101.00x
🐘 PostgresNext.js (Turbopack)2.390s (~)3.011s (~)0.620s101.03x
🌐 RedisNext.js (Turbopack)2.543s (+3.6%)3.007s (~)0.464s101.09x
💻 LocalNext.js (Turbopack)2.807s (-10.8% 🟢)3.341s (-6.3% 🟢)0.533s91.21x
💻 LocalExpress2.938s (-0.9%)3.454s (-3.1%)0.516s91.26x
💻 LocalNitro2.990s (-1.3%)3.677s (-2.2%)0.687s91.28x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.320s (~)4.314s (+9.6% 🔺)1.994s81.00x
▲ VercelNitro2.485s (-3.3%)3.963s (-5.7% 🟢)1.478s81.07x
▲ VercelNext.js (Turbopack)2.779s (+9.3% 🔺)4.314s (+3.7%)1.535s71.20x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro3.453s (~)4.010s (~)0.557s81.00x
🐘 PostgresExpress3.457s (~)4.009s (~)0.553s81.00x
🐘 PostgresNext.js (Turbopack)3.646s (~)4.009s (~)0.363s81.06x
🌐 RedisNext.js (Turbopack)4.169s (+3.5%)5.012s (+9.4% 🔺)0.843s61.21x
💻 LocalExpress8.033s (-6.6% 🟢)8.774s (-2.8%)0.741s42.33x
💻 LocalNitro8.669s (+2.3%)9.026s (~)0.357s42.51x
💻 LocalNext.js (Turbopack)8.695s (+12.2% 🔺)9.271s (+12.1% 🔺)0.576s42.52x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.728s (-4.1%)4.288s (-0.9%)1.560s71.00x
▲ VercelExpress3.170s (+16.4% 🔺)4.654s (+9.8% 🔺)1.484s71.16x
▲ VercelNext.js (Turbopack)3.271s (-13.0% 🟢)5.128s (-7.0% 🟢)1.857s61.20x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.608s (-26.8% 🟢)1.006s (~)0.399s601.00x
🌐 RedisNext.js (Turbopack)0.708s (+11.4% 🔺)1.004s (~)0.296s601.16x
🐘 PostgresNext.js (Turbopack)0.819s (+6.2% 🔺)1.041s (+1.7%)0.222s581.35x
🐘 PostgresNitro0.858s (+2.2%)1.023s (+1.7%)0.165s591.41x
💻 LocalNext.js (Turbopack)0.860s (-2.7%)1.005s (-3.3%)0.145s601.41x
💻 LocalNitro0.980s (-5.4% 🟢)1.115s (-44.4% 🟢)0.135s541.61x
💻 LocalExpress1.016s (+4.1%)1.654s (+48.3% 🔺)0.638s371.67x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro8.903s (-8.6% 🟢)10.501s (-8.5% 🟢)1.599s61.00x
▲ VercelNext.js (Turbopack)9.632s (-23.0% 🟢)11.579s (-17.7% 🟢)1.948s61.08x
▲ VercelExpress10.403s (+2.7%)12.297s (+1.4%)1.893s51.17x

🔍 Observability: Nitro | Next.js (Turbopack) | Express

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.477s (-26.2% 🟢)2.007s (-16.4% 🟢)0.530s451.00x
🌐 RedisNext.js (Turbopack)1.672s (+15.4% 🔺)2.006s (~)0.333s451.13x
🐘 PostgresNitro2.078s (+3.4%)2.944s (+16.1% 🔺)0.866s311.41x
🐘 PostgresNext.js (Turbopack)2.153s (+14.8% 🔺)2.821s (+37.5% 🔺)0.668s321.46x
💻 LocalNext.js (Turbopack)2.700s (~)3.008s (~)0.308s301.83x
💻 LocalNitro3.022s (-1.1%)3.547s (-9.6% 🟢)0.525s262.05x
💻 LocalExpress3.162s (+3.6%)3.967s (+4.3%)0.805s232.14x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro28.495s (-3.9%)30.408s (-3.5%)1.913s31.00x
▲ VercelExpress28.833s (-1.1%)30.922s (-1.2%)2.089s31.01x
▲ VercelNext.js (Turbopack)29.626s (-0.8%)31.356s (-2.6%)1.730s31.04x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express3.109s (-23.3% 🟢)3.790s (-20.7% 🟢)0.681s321.00x
🌐 RedisNext.js (Turbopack)3.352s (+11.2% 🔺)4.008s (+15.7% 🔺)0.656s301.08x
🐘 PostgresNext.js (Turbopack)3.994s (+5.8% 🔺)4.331s (+8.0% 🔺)0.337s281.28x
🐘 PostgresNitro4.191s (~)4.932s (-1.6%)0.741s251.35x
💻 LocalNext.js (Turbopack)8.691s (+2.0%)9.019s (~)0.328s142.80x
💻 LocalExpress9.039s (-1.3%)9.633s (-1.6%)0.594s132.91x
💻 LocalNitro9.104s (+2.1%)9.710s (+3.3%)0.606s132.93x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express73.513s (-14.6% 🟢)75.368s (-14.2% 🟢)1.855s21.00x
▲ VercelNitro75.626s (+6.1% 🔺)77.847s (+6.5% 🔺)2.221s21.03x
▲ VercelNext.js (Turbopack)77.885s (-4.4%)79.559s (-4.2%)1.675s21.06x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.226s (-18.4% 🟢)1.007s (~)0.781s601.00x
🐘 PostgresNext.js (Turbopack)0.251s (+4.8%)1.007s (~)0.757s601.11x
🐘 PostgresNitro0.281s (-3.7%)1.007s (~)0.725s601.24x
🌐 RedisNext.js (Turbopack)0.284s (+7.7% 🔺)1.004s (~)0.720s601.26x
💻 LocalNitro0.588s (+4.9%)1.004s (~)0.417s602.60x
💻 LocalNext.js (Turbopack)0.620s (+20.2% 🔺)1.022s (+1.7%)0.402s592.74x
💻 LocalExpress0.697s (+18.7% 🔺)1.096s (+9.1% 🔺)0.398s553.08x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.544s (-22.1% 🟢)3.163s (-11.6% 🟢)1.620s201.00x
▲ VercelNext.js (Turbopack)1.688s (-5.7% 🟢)3.573s (+3.3%)1.886s181.09x
▲ VercelNitro1.727s (-8.8% 🟢)3.318s (-4.5%)1.591s191.12x

🔍 Observability: Express | Next.js (Turbopack) | Nitro

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.363s (-27.3% 🟢)1.006s (~)0.643s901.00x
🐘 PostgresNitro0.493s (-1.8%)1.007s (~)0.514s901.36x
🐘 PostgresNext.js (Turbopack)0.508s (+4.6%)1.007s (~)0.499s901.40x
🌐 RedisNext.js (Turbopack)1.144s (+2.9%)2.006s (+17.7% 🔺)0.862s453.15x
💻 LocalExpress2.492s (-1.6%)3.009s (~)0.517s306.86x
💻 LocalNitro2.652s (+14.2% 🔺)3.180s (+5.7% 🔺)0.528s297.31x
💻 LocalNext.js (Turbopack)2.676s (+7.7% 🔺)3.076s (+2.2%)0.401s307.37x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.612s (-2.1%)4.132s (-6.5% 🟢)1.519s221.00x
▲ VercelExpress2.738s (-7.0% 🟢)4.568s (-3.6%)1.830s201.05x
▲ VercelNext.js (Turbopack)3.366s (-5.4% 🟢)4.890s (-7.9% 🟢)1.524s191.29x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.583s (-26.2% 🟢)1.007s (~)0.423s1201.00x
🐘 PostgresNitro0.809s (-0.9%)1.009s (~)0.200s1191.39x
🐘 PostgresNext.js (Turbopack)0.847s (+10.4% 🔺)1.043s (+3.6%)0.196s1161.45x
🌐 RedisNext.js (Turbopack)2.677s (-3.5%)3.007s (-0.8%)0.330s404.59x
💻 LocalNitro10.890s (+4.7%)11.485s (+4.1%)0.595s1118.67x
💻 LocalExpress10.935s (~)11.395s (-1.5%)0.460s1118.75x
💻 LocalNext.js (Turbopack)11.065s (+7.4% 🔺)11.572s (+7.4% 🔺)0.507s1118.97x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro6.908s (+9.6% 🔺)8.327s (+3.7%)1.419s151.00x
▲ VercelNext.js (Turbopack)47.028s (+673.5% 🔺)48.733s (+526.0% 🔺)1.705s66.81x
▲ VercelExpress49.426s (+616.1% 🔺)51.745s (+489.1% 🔺)2.320s77.15x

🔍 Observability: Nitro | Next.js (Turbopack) | Express

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.158s (-25.4% 🟢)1.001s (+0.5%)0.001s (-26.7% 🟢)1.009s (~)0.851s101.00x
💻 LocalNext.js (Turbopack)0.170s (+0.5%)1.002s (~)0.012s (+14.7% 🔺)1.017s (~)0.847s101.07x
🐘 PostgresNext.js (Turbopack)0.192s (+0.8%)1.001s (~)0.001s (-15.4% 🟢)1.010s (~)0.818s101.21x
💻 LocalNitro0.207s (-0.7%)1.004s (~)0.012s (+11.5% 🔺)1.017s (~)0.810s101.31x
💻 LocalExpress0.219s (+6.8% 🔺)1.004s (~)0.011s (-8.5% 🟢)1.017s (~)0.798s101.38x
🐘 PostgresNitro0.219s (+2.0%)0.996s (~)0.001s (+30.0% 🔺)1.012s (~)0.792s101.39x
🌐 RedisNext.js (Turbopack)0.242s (+62.9% 🔺)1.001s (~)0.032s (+1776.5% 🔺)1.037s (+3.0%)0.796s101.53x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)1.493s (-11.9% 🟢)2.833s (-11.4% 🟢)0.461s (-51.8% 🟢)3.696s (-20.0% 🟢)2.203s101.00x
▲ VercelExpress1.565s (+3.1%)2.942s (+1.1%)0.642s (-16.1% 🟢)4.033s (-2.6%)2.468s101.05x
▲ VercelNitro1.603s (-5.5% 🟢)2.899s (-11.0% 🟢)0.562s (+7.0% 🔺)3.812s (-9.8% 🟢)2.208s101.07x

🔍 Observability: Next.js (Turbopack) | Express | Nitro

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)0.481s (+14.5% 🔺)1.002s (~)0.003s (+4.8%)1.011s (~)0.530s601.00x
🐘 PostgresExpress0.522s (-16.5% 🟢)1.006s (~)0.004s (-10.2% 🟢)1.022s (~)0.500s591.09x
🐘 PostgresNitro0.629s (-2.0%)1.005s (~)0.004s (-11.2% 🟢)1.022s (~)0.393s591.31x
🐘 PostgresNext.js (Turbopack)0.659s (+9.3% 🔺)1.026s (+1.7%)0.008s (+110.1% 🔺)1.044s (+2.2%)0.385s581.37x
💻 LocalNext.js (Turbopack)0.763s (+13.8% 🔺)1.011s (~)0.009s (-15.6% 🟢)1.116s (+8.9% 🔺)0.353s541.59x
💻 LocalNitro0.815s (-14.6% 🟢)1.012s (~)0.010s (+2.4%)1.111s (-9.5% 🟢)0.296s571.70x
💻 LocalExpress0.847s (+18.5% 🔺)1.010s (~)0.010s (+6.7% 🔺)1.115s (+8.9% 🔺)0.268s541.76x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.092s (+1.5%)5.761s (~)0.242s (-2.7%)6.436s (-0.6%)2.344s101.00x
▲ VercelNext.js (Turbopack)4.186s (-6.5% 🟢)6.079s (-0.6%)0.220s (+9.9% 🔺)6.720s (~)2.534s91.02x
▲ VercelNitro4.499s (+6.2% 🔺)5.768s (+2.8%)0.457s (+71.8% 🔺)6.725s (+5.7% 🔺)2.226s91.10x

🔍 Observability: Express | Next.js (Turbopack) | Nitro

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)0.900s (+1.8%)1.035s (+3.5%)0.000s (+106.9% 🔺)1.039s (+3.4%)0.139s581.00x
🐘 PostgresExpress0.931s (-3.9%)1.146s (-5.9% 🟢)0.000s (+172.2% 🔺)1.159s (-6.0% 🟢)0.228s541.03x
🐘 PostgresNitro0.946s (~)1.220s (+12.0% 🔺)0.000s (-17.3% 🟢)1.232s (+10.3% 🔺)0.286s491.05x
🐘 PostgresNext.js (Turbopack)0.956s (+3.1%)1.177s (+5.6% 🔺)0.000s (+Infinity% 🔺)1.185s (+4.2%)0.229s511.06x
💻 LocalNitro1.218s (+2.4%)2.021s (~)0.000s (-7.1% 🟢)2.023s (~)0.805s301.35x
💻 LocalNext.js (Turbopack)1.256s (~)2.019s (~)0.000s (-11.1% 🟢)2.022s (~)0.767s301.40x
💻 LocalExpress1.444s (+17.6% 🔺)2.022s (~)0.001s (+14.8% 🔺)2.203s (+8.8% 🔺)0.758s281.60x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.826s (~)4.141s (+4.9%)0.000s (+150.0% 🔺)4.585s (+4.0%)1.759s141.00x
▲ VercelNitro2.860s (+7.4% 🔺)4.133s (+4.8%)0.000s (-50.0% 🟢)4.541s (+3.8%)1.681s141.01x
▲ VercelNext.js (Turbopack)3.188s (-28.9% 🟢)4.619s (-22.8% 🟢)0.000s (-100.0% 🟢)5.035s (-21.8% 🟢)1.847s121.13x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

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

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)1.728s (-1.9%)2.035s (+1.7%)0.000s (-50.0% 🟢)2.039s (+1.6%)0.311s301.00x
🐘 PostgresExpress1.739s (-1.1%)2.143s (+3.8%)0.000s (-65.5% 🟢)2.152s (+3.4%)0.413s281.01x
🐘 PostgresNitro1.789s (+2.3%)2.102s (~)0.000s (+Infinity% 🔺)2.113s (-1.0%)0.324s291.04x
🐘 PostgresNext.js (Turbopack)1.958s (+6.4% 🔺)2.147s (+0.6%)0.000s (+Infinity% 🔺)2.155s (~)0.197s281.13x
💻 LocalNitro3.410s (+6.4% 🔺)4.034s (+1.7%)0.001s (+140.0% 🔺)4.037s (+1.7%)0.627s151.97x
💻 LocalNext.js (Turbopack)3.725s (+3.2%)4.233s (+1.6%)0.000s (-12.5% 🟢)4.236s (+1.6%)0.511s152.16x
💻 LocalExpress3.802s (+9.2% 🔺)4.368s (+8.3% 🔺)0.001s (+233.3% 🔺)4.372s (+8.3% 🔺)0.570s152.20x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.969s (-25.3% 🟢)5.072s (-23.7% 🟢)0.000s (-62.5% 🟢)5.489s (-22.2% 🟢)1.521s121.00x
▲ VercelNext.js (Turbopack)3.988s5.498s0.000s5.941s1.953s111.01x
▲ VercelExpress5.598s (+32.4% 🔺)7.223s (+32.7% 🔺)0.000s (-31.3% 🟢)7.670s (+30.2% 🔺)2.072s81.41x

🔍 Observability: Nitro | Next.js (Turbopack) | Express

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNext.js (Turbopack)10/21
🐘 PostgresExpress19/21
▲ VercelExpress12/21
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express🐘 Postgres17/21
Next.js (Turbopack)🌐 Redis10/21
Nitro🐘 Postgres15/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)

📋 View full workflow run

CopilotAI 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.

Pull request overview

Adds a workflow-mode optimization to the SWC plugin so that after "use step" class members are stripped, any now-unreferenced private class members are also removed, improving downstream tree-shaking (e.g., dropping Node-only imports kept alive by unused helpers).

Changes:

  • Implement iterative private-member reachability analysis and removal after step-member stripping (class decls + class exprs).
  • Add a new transform fixture validating cascading elimination of TypeScript private members in workflow mode.
  • Document the new optimization and publish a patch changeset.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 5 comments.

Show a summary per file
FileDescription
packages/swc-plugin-workflow/transform/src/lib.rsAdds private-member reference collection + DCE pass after "use step" stripping.
packages/swc-plugin-workflow/transform/tests/fixture/private-member-dce/input.tsNew fixture input covering cascading TS private elimination.
packages/swc-plugin-workflow/transform/tests/fixture/private-member-dce/output-workflow.jsExpected workflow output with TS-private members removed and imports dropped.
packages/swc-plugin-workflow/transform/tests/fixture/private-member-dce/output-step.jsExpected step-mode output (private members retained).
packages/swc-plugin-workflow/transform/tests/fixture/private-member-dce/output-client.jsExpected client-mode output (private members retained).
packages/swc-plugin-workflow/spec.mdDocuments the private-member DCE behavior and example.
.changeset/private-member-dce.mdDeclares a patch release for the SWC plugin change.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +619 to +624
// TS private or any member: `this.foo` — only track when
// the object is `this` (to avoid false positives from
// unrelated member accesses like `obj.foo`)
MemberProp::Ident(ident) => {
if matches!(&*expr.obj, Expr::This(_)) {
self.referenced.insert(ident.sym.to_string());

CopilotAIApr 9, 2026

Copy link

Choose a reason for hiding this comment

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

ClassMemberRefCollector only records MemberProp::Ident accesses when the object is this. In TypeScript, private members are also legally accessed via other instances of the same class (e.g. static compare(a: C, b: C) { return a.x - b.x }). With the current check, those references won’t be collected and the private member can be incorrectly removed, breaking runtime behavior. Consider treating any .ident access as a reference when ident matches a known TS-private member name in the class, regardless of the receiver expression (safe, though more conservative).

Suggested change
// TS private or any member: `this.foo` — only track when
// the object is `this` (to avoid false positives from
// unrelated member accesses like `obj.foo`)
MemberProp::Ident(ident) => {
ifmatches!(&*expr.obj,Expr::This(_)){
self.referenced.insert(ident.sym.to_string());
// TS private or any member access. Track `this.foo` as before,
// and also track `obj.foo` when `foo` is a known TS-private
// member of the current class so same-class accesses like
// `a.x` / `b.x` are not missed.
MemberProp::Ident(ident) => {
let name = ident.sym.to_string();
ifmatches!(&*expr.obj,Expr::This(_)) || self.private_names.contains(&name){
self.referenced.insert(name);

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Fixed. The collector now holds a ts_private_names: HashSet<String> built from the class body. MemberProp::Ident accesses are tracked when the name is in that set, regardless of the receiver expression. This catches a.x / b.x patterns in static methods.

Comment on lines +629 to +630
// Continue visiting children
expr.obj.visit_with(self);

CopilotAIApr 9, 2026

Copy link

Choose a reason for hiding this comment

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

visit_member_expr overrides default traversal but only visits expr.obj. For computed member expressions (obj[expr]), the property expression is not visited, so any this.#x / this.x references inside the computed key won’t be collected and could cause incorrectly eliminating private members. Use expr.visit_children_with(self) (or explicitly visit the computed property expression) to preserve full traversal.

Suggested change
// Continue visiting children
expr.obj.visit_with(self);
// Continue visiting children, including computed property expressions.
expr.visit_children_with(self);

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Fixed. Changed to expr.visit_children_with(self) which visits both the object and property expressions, including computed keys.

Comment on lines +614 to +625
match &expr.prop {
// Native JS private: `this.#foo`
MemberProp::PrivateName(name) => {
self.referenced.insert(name.name.to_string());
}
// TS private or any member: `this.foo` — only track when
// the object is `this` (to avoid false positives from
// unrelated member accesses like `obj.foo`)
MemberProp::Ident(ident) => {
if matches!(&*expr.obj, Expr::This(_)) {
self.referenced.insert(ident.sym.to_string());
}

CopilotAIApr 9, 2026

Copy link

Choose a reason for hiding this comment

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

Native private #foo and public/TS member foo are both stored as the same string ("foo"). If a class contains both foo and #foo, a this.foo access will incorrectly mark #foo as referenced, preventing intended elimination. Consider encoding JS-private names distinctly (e.g. prefix with # or store an enum) so #foo and foo don’t collide.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Fixed. JS native private names are now stored with a # prefix (e.g. "#foo") while TS private names remain unprefixed ("foo"). private_member_name() returns the prefixed form for PrivateMethod/PrivateProp, and visit_member_expr inserts format!("#{}", name) for MemberProp::PrivateName.

Comment on lines +1 to +25
import { WORKFLOW_SERIALIZE, WORKFLOW_DESERIALIZE } from '@workflow/serde';
import { getWorld } from './world.js';
import { importKey } from './encryption.js';

export class Run {
static [WORKFLOW_SERIALIZE](instance: Run) {
return { id: instance.id };
}

static [WORKFLOW_DESERIALIZE](data: { id: string }) {
return new Run(data.id);
}

id: string;

// TS private field — only referenced by stripped methods
private encryptionKeyPromise: Promise<any> | null = null;

// TS private method — only called by stripped getters/methods
private async getEncryptionKey(): Promise<any> {
if (!this.encryptionKeyPromise) {
this.encryptionKeyPromise = importKey(this.id);
}
return this.encryptionKeyPromise;
}

CopilotAIApr 9, 2026

Copy link

Choose a reason for hiding this comment

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

The new DCE logic claims to support native JS private members (#field/#method()), but the fixture only covers TypeScript private. Adding a fixture that uses #private syntax (including a cascading case) would help prevent regressions in the JS-private path.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Added in the second commit: private-member-dce-native/input.js tests JS native #field and #method() DCE including cascading elimination (#encryptionKeyPromise eliminated because #getEncryptionKey is eliminated) and survival of referenced members (#label kept because toString() references it).

Comment on lines +8504 to +8510
// Dead-code-eliminate unreferenced private members
// (same logic as visit_mut_class_decl above)
let referenced =
ClassMemberRefCollector::collect_from_class_body(&class_expr.class.body);
class_expr.class.body.retain(|member| match member {
ClassMember::PrivateMethod(m) => referenced.contains(&m.key.name.to_string()),
ClassMember::PrivateProp(p) => referenced.contains(&p.key.name.to_string()),

CopilotAIApr 9, 2026

Copy link

Choose a reason for hiding this comment

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

The private-member retention filter is duplicated in both visit_mut_class_decl and visit_mut_class_expr. This duplication increases the risk of future divergence/bugs when the logic changes. Consider extracting the retain logic into a shared helper (e.g. fn retain_referenced_private_members(body: &mut Vec<ClassMember>)) used by both visitors.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Fixed. Extracted retain_referenced_private_members(body: &mut Vec<ClassMember>) on ClassMemberRefCollector, called from both visit_mut_class_decl and visit_mut_class_expr.

- Namespace JS native private names with # prefix to avoid collisions
with TS private members of the same name
- Track TS private member accesses on non-this receivers (e.g. a.x in
static methods) by maintaining a set of known TS-private names
- Use visit_children_with for full traversal including computed member
expressions
- Extract retain logic into shared retain_referenced_private_members()
helper used by both visit_mut_class_decl and visit_mut_class_expr

@VaguelySeriousVaguelySerious left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

AI review: no blocking issues

// accesses like `a.x` / `b.x` are not missed.
MemberProp::Ident(ident) => {
let name = ident.sym.to_string();
if matches!(&*expr.obj, Expr::This(_)) || self.ts_private_names.contains(&name) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

AI Review: Note

For MemberProp::Ident, any this.foo access records foo in referenced, not only TS-private members. Retain still keys off private_member_name, so this is mostly extra churn in the set rather than incorrect retention, but it is slightly less precise than filtering to private names only.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Correct - the collector is conservative: it records all this.foo accesses, not just those matching known private names. This is intentional for simplicity. The extra entries in the referenced set are harmless since retain only removes members that private_member_name() identifies as private. Public members are always kept regardless of what's in the set.

});
```

This optimization is critical for SDK classes like `Run` where private helper methods reference Node.js-only imports (encryption, world access, etc.) — eliminating them allows the downstream module-level DCE to also remove those imports from the workflow bundle.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

AI Review: Nit

This section explains the algorithm well. If you want to reduce future issue reports, a short explicit note on unsupported patterns (for example computed this[...] access to private fields) could help set expectations.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Good suggestion. Computed access like this[name] where name happens to be a private member name won't be tracked, which could lead to incorrect elimination. In practice this pattern is very rare for private members (especially in SDK code like Run), but worth noting if we want to document limitations.

@TooTallNate
TooTallNate enabled auto-merge (squash) April 9, 2026 19:22
@TooTallNate
TooTallNate merged commit 66585fd into mainApr 9, 2026
102 of 103 checks passed
@TooTallNate
TooTallNate deleted the fix/swc-private-member-dce branch April 9, 2026 19:27
@ghostghost mentioned this pull request Apr 9, 2026
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

@TooTallNate@VaguelySerious
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(swc): dead code eliminate unreferenced private class members in workflow mode - #1671

Merged
TooTallNate merged 3 commits into
mainfrom
fix/swc-private-member-dce
Apr 9, 2026
Merged

feat(swc): dead code eliminate unreferenced private class members in workflow mode#1671
TooTallNate merged 3 commits into
mainfrom
fix/swc-private-member-dce

Conversation

@TooTallNate

@TooTallNateTooTallNate commented Apr 9, 2026

Copy link
Copy Markdown
Member

Summary

After stripping "use step" methods from a class body in workflow mode, eliminate private members that are no longer referenced by any remaining public member. This applies to both:

  • JS native private: #field, #method()
  • TypeScript private: private field, private method()

The algorithm is iterative — references are seeded from public members, then expanded through surviving private members until a fixed point, enabling cascading elimination.

Example

Input:

exportclassRun{static[WORKFLOW_SERIALIZE](instance){return{id: instance.id};}static[WORKFLOW_DESERIALIZE](data){returnnewRun(data.id);}id: string;privateencryptionKeyPromise: Promise<any>|null=null;privateasyncgetEncryptionKey(){if(!this.encryptionKeyPromise){this.encryptionKeyPromise=importKey(this.id);}returnthis.encryptionKeyPromise;}constructor(id: string){this.id=id;}getvalue(): Promise<any>{'use step';returnthis.getEncryptionKey().then(()=>getWorld().get(this.id));}asynccancel(): Promise<void>{'use step';constkey=awaitthis.getEncryptionKey();awaitgetWorld().cancel(this.id,key);}toString(): string{return`Run(${this.id})`;}}

Workflow mode output:

exportclassRun{static[WORKFLOW_SERIALIZE](instance){return{id: instance.id};}static[WORKFLOW_DESERIALIZE](data){returnnewRun(data.id);}id;// ✅ private encryptionKeyPromise — ELIMINATED (only referenced by getEncryptionKey)// ✅ private getEncryptionKey() — ELIMINATED (only referenced by stripped step methods)constructor(id){this.id=id;}toString(){return`Run(${this.id})`;}}Run.prototype["cancel"]=globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//...");var__step_Run$value=globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//...");Object.defineProperty(Run.prototype,"value",{get(){return__step_Run$value.call(this);},configurable: true,enumerable: false});

The cascading elimination is key: encryptionKeyPromise is only referenced by getEncryptionKey(), which is itself only referenced by the stripped value getter and cancel() method. Both private members are removed, allowing the downstream module-level DCE to also eliminate the importKey and getWorld imports.

Motivation

SDK classes like Run have private helper methods that reference Node.js-only imports (encryption, world access). Without this optimization, those helpers survive into the workflow bundle even though nothing calls them after "use step" bodies are stripped, keeping the Node.js imports alive and preventing tree-shaking.

Test

New fixture: private-member-dce/input.ts with expected outputs for all three modes.

…workflow mode
After stripping 'use step' methods from a class body in workflow mode,
eliminate private members (both JS native #field/#method and TypeScript
private field/private method) that are no longer referenced by any
remaining public member.
The algorithm is iterative: references are seeded from public members,
then expanded through surviving private members until a fixed point,
enabling cascading elimination (e.g. a private field only referenced by
a private method that is itself unreferenced).
CopilotAI review requested due to automatic review settings April 9, 2026 18:09
@TooTallNate
TooTallNate requested a review from a team as a code ownerApril 9, 2026 18:09
@changeset-bot

changeset-botBot commented Apr 9, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 4ea576a

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

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

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 Apr 9, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Apr 9, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production912067979
✅ 💻 Local Development88601821068
✅ 📦 Local Production88601821068
✅ 🐘 Local Postgres88601821068
✅ 🪟 Windows810889
❌ 🌍 Community Worlds1366524225
✅ 📋 Other225042267
Total4012656874764

❌ Failed Tests

🌍 Community Worlds (65 failed)

mongodb (4 failed):

  • hookWorkflow is not resumable via public webhook endpoint | wrun_01KNSR613X7J6NJ3Q7CC1GAXC8
  • webhookWorkflow | wrun_01KNSR6A2N8E4FRA7K4W0WAJ2X
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously | wrun_01KNSRE4P2HFBR1HGDZ60FSZ4A
  • resilient start: addTenWorkflow completes when run_created returns 500 | wrun_01KNSRMTFPHDMEWW1M0C99YMJB

redis (3 failed):

  • hookWorkflow is not resumable via public webhook endpoint | wrun_01KNSR613X7J6NJ3Q7CC1GAXC8
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously | wrun_01KNSRE4P2HFBR1HGDZ60FSZ4A
  • resilient start: addTenWorkflow completes when run_created returns 500 | wrun_01KNSRMTFPHDMEWW1M0C99YMJB

turso (58 failed):

  • addTenWorkflow | wrun_01KNSR4Q12NYS08Q5M8JMBJE2P
  • addTenWorkflow | wrun_01KNSR4Q12NYS08Q5M8JMBJE2P
  • wellKnownAgentWorkflow (.well-known/agent) | wrun_01KNSR6H7RBB0VN5AF50A8K3HK
  • should work with react rendering in step
  • promiseAllWorkflow | wrun_01KNSR50EQH830PRD40KS32Q9Y
  • promiseRaceWorkflow | wrun_01KNSR54Y75RYQDBMAWK470VYP
  • promiseAnyWorkflow | wrun_01KNSR58C03DCWT8KGXWBMNCP6
  • importedStepOnlyWorkflow | wrun_01KNSR6WGHHN8XTHMJ86Q1JS10
  • hookWorkflow | wrun_01KNSR5M9AWG051SF57JDHTN15
  • hookWorkflow is not resumable via public webhook endpoint | wrun_01KNSR613X7J6NJ3Q7CC1GAXC8
  • webhookWorkflow | wrun_01KNSR6A2N8E4FRA7K4W0WAJ2X
  • sleepingWorkflow | wrun_01KNSR6G7XQP06VPNVA5S5SZ7P
  • parallelSleepWorkflow | wrun_01KNSR6W68GVQAECF6EXH5YKW4
  • nullByteWorkflow | wrun_01KNSR6ZZHZA6GX9A995NWGH4W
  • workflowAndStepMetadataWorkflow | wrun_01KNSR722J4AE22DPWYBGR40AA
  • fetchWorkflow | wrun_01KNSR9QTD0RQEWYMKGRM9P6KS
  • promiseRaceStressTestWorkflow | wrun_01KNSR9V7MWTQB99MYWGDBMPV1
  • error handling error propagation workflow errors nested function calls preserve message and stack trace
  • error handling error propagation workflow errors cross-file imports preserve message and stack trace
  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior FatalError fails immediately without retries
  • error handling retry behavior RetryableError respects custom retryAfter delay
  • error handling retry behavior maxRetries=0 disables retries
  • error handling catchability FatalError can be caught and detected with FatalError.is()
  • error handling not registered WorkflowNotRegisteredError fails the run when workflow does not exist
  • error handling not registered StepNotRegisteredError fails the step but workflow can catch it
  • error handling not registered StepNotRegisteredError fails the run when not caught in workflow
  • hookCleanupTestWorkflow - hook token reuse after workflow completion | wrun_01KNSRDFYWY5RDPW5MJW4M2RKP
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously | wrun_01KNSRE4P2HFBR1HGDZ60FSZ4A
  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running | wrun_01KNSREVJW6G5G63ZYP9DTG2CT
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars) | wrun_01KNSRFFZ3HX99QC8VH9X00X19
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument | wrun_01KNSRFS1Y9106Z5PVYGW5SZ8F
  • closureVariableWorkflow - nested step functions with closure variables | wrun_01KNSRFYM8QZ6MKBWEXNSXEA1E
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step | wrun_01KNSRG1102E8CS370B44Y7JJ1
  • health check (queue-based) - workflow and step endpoints respond to health check messages
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly | wrun_01KNSRGH373GH51TWFMF3M6THG
  • Calculator.calculate - static workflow method using static step methods from another class | wrun_01KNSRGPSM4VSQ1R3Z11NG4MZ0
  • AllInOneService.processNumber - static workflow method using sibling static step methods | wrun_01KNSRGXK317X0HN019FNHP541
  • ChainableService.processWithThis - static step methods using this to reference the class | wrun_01KNSRH5K54HKGJC6TW5YDC6P4
  • thisSerializationWorkflow - step function invoked with .call() and .apply() | wrun_01KNSRHCE7T09PPS45FA7D75MK
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE | wrun_01KNSRHM1A0G2BFRKSN1893XHR
  • instanceMethodStepWorkflow - instance methods with "use step" directive | wrun_01KNSRHTZQJP99YC0M9FNWQPM5
  • crossContextSerdeWorkflow - classes defined in step code are deserializable in workflow context | wrun_01KNSRJ68YGFBGXJ7YFR7ZHMV5
  • stepFunctionAsStartArgWorkflow - step function reference passed as start() argument | wrun_01KNSRJFBR9XRH3012W64TDAJ1
  • cancelRun - cancelling a running workflow | wrun_01KNSRJP4YQBW4BDB9ZFHE3JXZ
  • cancelRun via CLI - cancelling a running workflow | wrun_01KNSRK06K5725SZWQJ2DJZZ1K
  • pages router addTenWorkflow via pages router
  • pages router promiseAllWorkflow via pages router
  • pages router sleepingWorkflow via pages router
  • hookWithSleepWorkflow - hook payloads delivered correctly with concurrent sleep | wrun_01KNSRKDA53YRF17ST7YTD02BW
  • sleepInLoopWorkflow - sleep inside loop with steps actually delays each iteration | wrun_01KNSRM3T179X2E69VG894KAX9
  • sleepWithSequentialStepsWorkflow - sequential steps work with concurrent sleep (control) | wrun_01KNSRME2NVXNSBMBR8AND2DKJ
  • importMetaUrlWorkflow - import.meta.url is available in step bundles | wrun_01KNSRMP16CE789X5BG39DBJEA
  • metadataFromHelperWorkflow - getWorkflowMetadata/getStepMetadata work from module-level helper (#1577) | wrun_01KNSRMR5GRYXW87SN5A0AM9TV
  • resilient start: addTenWorkflow completes when run_created returns 500 | wrun_01KNSRMTFPHDMEWW1M0C99YMJB
  • getterStepWorkflow - getter functions with "use step" directive | wrun_01KNSRMXQTQ6GKDFR5NN8DMJRA

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro8207
✅ example8207
✅ express8207
✅ fastify8207
✅ hono8207
✅ nextjs-turbopack8702
✅ nextjs-webpack8702
✅ nitro8207
✅ nuxt8207
✅ sveltekit8207
✅ vite8207
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable75014
✅ express-stable75014
✅ fastify-stable75014
✅ hono-stable75014
✅ nextjs-turbopack-canary62027
✅ nextjs-turbopack-stable8108
✅ nextjs-webpack-canary62027
✅ nextjs-webpack-stable8108
✅ nitro-stable75014
✅ nuxt-stable75014
✅ sveltekit-stable75014
✅ vite-stable75014
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable75014
✅ express-stable75014
✅ fastify-stable75014
✅ hono-stable75014
✅ nextjs-turbopack-canary62027
✅ nextjs-turbopack-stable8108
✅ nextjs-webpack-canary62027
✅ nextjs-webpack-stable8108
✅ nitro-stable75014
✅ nuxt-stable75014
✅ sveltekit-stable75014
✅ vite-stable75014
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable75014
✅ express-stable75014
✅ fastify-stable75014
✅ hono-stable75014
✅ nextjs-turbopack-canary62027
✅ nextjs-turbopack-stable8108
✅ nextjs-webpack-canary62027
✅ nextjs-webpack-stable8108
✅ nitro-stable75014
✅ nuxt-stable75014
✅ sveltekit-stable75014
✅ vite-stable75014
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack8108
❌ 🌍 Community Worlds
AppPassedFailedSkipped
✅ mongodb-dev500
❌ mongodb5848
✅ redis-dev500
❌ redis5938
✅ turso-dev500
❌ turso4588
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable75014
✅ e2e-local-postgres-nest-stable75014
✅ e2e-local-prod-nest-stable75014

📋 View full workflow run

@github-actions

github-actionsBot commented Apr 9, 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🥇 Express0.043s (-1.6%)1.006s (~)0.962s101.00x
💻 LocalNitro0.044s (-4.8%)1.005s (~)0.961s101.01x
🐘 PostgresExpress0.046s (-22.5% 🟢)1.011s (~)0.965s101.07x
💻 LocalNext.js (Turbopack)0.049s (+3.6%)1.005s (~)0.957s101.12x
🌐 RedisNext.js (Turbopack)0.050s (-11.3% 🟢)1.005s (~)0.955s101.16x
🐘 PostgresNext.js (Turbopack)0.058s (-3.0%)1.011s (~)0.954s101.33x
🐘 PostgresNitro0.063s (+2.3%)1.012s (~)0.949s101.46x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express0.231s (-39.8% 🟢)2.203s (-10.5% 🟢)1.972s101.00x
▲ VercelNext.js (Turbopack)0.267s (-33.8% 🟢)2.195s (-8.4% 🟢)1.928s101.16x
▲ VercelNitro0.304s (+10.2% 🔺)2.211s (+4.0%)1.907s101.32x

🔍 Observability: Express | Next.js (Turbopack) | Nitro

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.119s (-3.1%)2.010s (~)0.891s101.00x
🌐 RedisNext.js (Turbopack)1.126s (~)2.007s (~)0.881s101.01x
💻 LocalNext.js (Turbopack)1.126s (~)2.006s (~)0.880s101.01x
💻 LocalNitro1.128s (~)2.005s (~)0.877s101.01x
💻 LocalExpress1.131s (~)2.006s (~)0.875s101.01x
🐘 PostgresNext.js (Turbopack)1.147s (~)2.009s (~)0.862s101.02x
🐘 PostgresNitro1.147s (-0.6%)2.011s (~)0.864s101.02x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.911s (+1.0%)3.886s (+0.8%)1.975s101.00x
▲ VercelNitro2.003s (+6.5% 🔺)3.356s (-10.3% 🟢)1.353s101.05x
▲ VercelNext.js (Turbopack)2.103s (+4.0%)3.881s (+3.0%)1.778s101.10x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express10.694s (-2.1%)11.028s (~)0.334s31.00x
🌐 RedisNext.js (Turbopack)10.779s (+0.7%)11.023s (~)0.244s31.01x
💻 LocalNext.js (Turbopack)10.831s (~)11.025s (~)0.194s31.01x
🐘 PostgresNext.js (Turbopack)10.849s (~)11.023s (~)0.174s31.01x
💻 LocalNitro10.920s (~)11.023s (~)0.103s31.02x
🐘 PostgresNitro10.934s (~)11.023s (~)0.088s31.02x
💻 LocalExpress10.952s (~)11.024s (~)0.072s31.02x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express16.776s (~)18.801s (-1.4%)2.025s21.00x
▲ VercelNext.js (Turbopack)17.133s (-0.8%)19.157s (~)2.024s21.02x
▲ VercelNitro17.627s (-1.4%)18.953s (-5.7% 🟢)1.326s21.05x

🔍 Observability: Express | Next.js (Turbopack) | Nitro

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express14.019s (-3.8%)14.627s (-2.6%)0.608s51.00x
🌐 RedisNext.js (Turbopack)14.257s (+0.9%)15.029s (~)0.772s41.02x
🐘 PostgresNext.js (Turbopack)14.412s (-0.6%)15.023s (~)0.612s41.03x
🐘 PostgresNitro14.702s (~)15.021s (~)0.319s41.05x
💻 LocalNext.js (Turbopack)14.709s (+1.0%)15.030s (~)0.322s41.05x
💻 LocalNitro14.924s (-0.6%)15.028s (-3.2%)0.104s41.06x
💻 LocalExpress14.989s (~)15.030s (~)0.041s41.07x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express33.420s (-3.4%)35.676s (-3.4%)2.256s21.00x
▲ VercelNext.js (Turbopack)34.176s (-3.3%)36.300s (-3.0%)2.124s21.02x
▲ VercelNitro34.324s (+0.8%)35.857s (-1.2%)1.533s21.03x

🔍 Observability: Express | Next.js (Turbopack) | Nitro

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express12.957s (-8.6% 🟢)13.021s (-13.3% 🟢)0.064s71.00x
🌐 RedisNext.js (Turbopack)13.389s (+1.2%)14.025s (~)0.636s71.03x
🐘 PostgresNext.js (Turbopack)13.757s (~)14.019s (~)0.262s71.06x
🐘 PostgresNitro14.192s (-0.9%)15.022s (~)0.829s61.10x
💻 LocalNext.js (Turbopack)16.033s (+2.8%)16.531s (+3.1%)0.498s61.24x
💻 LocalExpress16.441s (-0.7%)17.032s (~)0.591s61.27x
💻 LocalNitro16.531s (~)17.029s (~)0.498s61.28x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)56.887s (-2.0%)58.539s (-2.7%)1.652s21.00x
▲ VercelExpress59.072s (+7.5% 🔺)61.603s (+7.7% 🔺)2.531s21.04x
▲ VercelNitro59.188s (+2.0%)60.723s (~)1.535s21.04x

🔍 Observability: Next.js (Turbopack) | Express | Nitro

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.211s (-8.6% 🟢)2.009s (~)0.798s151.00x
🐘 PostgresNext.js (Turbopack)1.232s (~)2.009s (~)0.777s151.02x
🐘 PostgresNitro1.260s (~)2.009s (~)0.749s151.04x
🌐 RedisNext.js (Turbopack)1.318s (+6.2% 🔺)2.006s (~)0.688s151.09x
💻 LocalExpress1.515s (+0.8%)2.006s (~)0.491s151.25x
💻 LocalNitro1.516s (+2.1%)2.005s (~)0.490s151.25x
💻 LocalNext.js (Turbopack)1.544s (+4.6%)2.006s (~)0.461s151.27x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.253s (-6.7% 🟢)4.180s (+4.1%)1.927s81.00x
▲ VercelNitro2.496s (-15.3% 🟢)3.670s (-19.6% 🟢)1.174s91.11x
▲ VercelNext.js (Turbopack)2.719s (+23.5% 🔺)4.105s (+6.9% 🔺)1.386s81.21x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.300s (-2.9%)3.009s (~)0.708s101.00x
🐘 PostgresNitro2.330s (+0.7%)3.009s (~)0.680s101.01x
🐘 PostgresNext.js (Turbopack)2.385s (-0.7%)3.009s (~)0.624s101.04x
🌐 RedisNext.js (Turbopack)2.564s (+5.3% 🔺)3.008s (~)0.444s101.11x
💻 LocalExpress2.895s (+1.4%)3.453s (+3.3%)0.558s91.26x
💻 LocalNitro2.991s (+6.5% 🔺)3.341s (+11.1% 🔺)0.351s91.30x
💻 LocalNext.js (Turbopack)3.077s (+24.5% 🔺)3.884s (+29.1% 🔺)0.808s81.34x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.670s (-10.7% 🟢)4.008s (-14.7% 🟢)1.338s81.00x
▲ VercelNext.js (Turbopack)2.780s (+3.0%)4.402s (-1.8%)1.622s71.04x
▲ VercelExpress2.879s (+4.4%)4.437s (+2.9%)1.558s71.08x

🔍 Observability: Nitro | Next.js (Turbopack) | Express

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express3.383s (-2.3%)4.011s (~)0.628s81.00x
🐘 PostgresNitro3.472s (-0.5%)4.011s (~)0.538s81.03x
🐘 PostgresNext.js (Turbopack)3.643s (-1.1%)4.009s (~)0.367s81.08x
🌐 RedisNext.js (Turbopack)4.164s (+5.3% 🔺)5.011s (+16.6% 🔺)0.847s61.23x
💻 LocalExpress7.500s (-8.6% 🟢)8.021s (-8.5% 🟢)0.521s42.22x
💻 LocalNitro8.022s (+1.6%)8.773s (+6.1% 🔺)0.752s42.37x
💻 LocalNext.js (Turbopack)8.384s (+47.2% 🔺)9.019s (+50.0% 🔺)0.635s42.48x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.054s (-1.0%)5.367s (+9.7% 🔺)2.313s61.00x
▲ VercelNitro3.295s (+8.8% 🔺)4.508s (-7.1% 🟢)1.213s71.08x
▲ VercelNext.js (Turbopack)3.678s (+1.9%)5.769s (+8.0% 🔺)2.091s61.20x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.204s (-4.3%)2.008s (~)0.804s151.00x
🐘 PostgresNext.js (Turbopack)1.251s (+2.1%)2.009s (~)0.758s151.04x
🐘 PostgresNitro1.264s (~)2.008s (~)0.744s151.05x
🌐 RedisNext.js (Turbopack)1.278s (-0.6%)2.006s (~)0.729s151.06x
💻 LocalExpress1.519s (~)2.006s (~)0.487s151.26x
💻 LocalNitro1.524s (-3.4%)2.006s (~)0.482s151.27x
💻 LocalNext.js (Turbopack)1.556s (~)2.007s (-3.2%)0.451s151.29x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.003s (-4.2%)3.908s (-1.8%)1.905s81.00x
▲ VercelNitro2.006s (-7.6% 🟢)3.636s (-6.7% 🟢)1.630s91.00x
▲ VercelNext.js (Turbopack)2.217s (+6.1% 🔺)3.745s (+5.0% 🔺)1.529s91.11x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro2.329s (~)3.010s (~)0.681s101.00x
🐘 PostgresExpress2.332s (-0.9%)3.008s (~)0.676s101.00x
🐘 PostgresNext.js (Turbopack)2.390s (~)3.011s (~)0.620s101.03x
🌐 RedisNext.js (Turbopack)2.543s (+3.6%)3.007s (~)0.464s101.09x
💻 LocalNext.js (Turbopack)2.807s (-10.8% 🟢)3.341s (-6.3% 🟢)0.533s91.21x
💻 LocalExpress2.938s (-0.9%)3.454s (-3.1%)0.516s91.26x
💻 LocalNitro2.990s (-1.3%)3.677s (-2.2%)0.687s91.28x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.320s (~)4.314s (+9.6% 🔺)1.994s81.00x
▲ VercelNitro2.485s (-3.3%)3.963s (-5.7% 🟢)1.478s81.07x
▲ VercelNext.js (Turbopack)2.779s (+9.3% 🔺)4.314s (+3.7%)1.535s71.20x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro3.453s (~)4.010s (~)0.557s81.00x
🐘 PostgresExpress3.457s (~)4.009s (~)0.553s81.00x
🐘 PostgresNext.js (Turbopack)3.646s (~)4.009s (~)0.363s81.06x
🌐 RedisNext.js (Turbopack)4.169s (+3.5%)5.012s (+9.4% 🔺)0.843s61.21x
💻 LocalExpress8.033s (-6.6% 🟢)8.774s (-2.8%)0.741s42.33x
💻 LocalNitro8.669s (+2.3%)9.026s (~)0.357s42.51x
💻 LocalNext.js (Turbopack)8.695s (+12.2% 🔺)9.271s (+12.1% 🔺)0.576s42.52x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.728s (-4.1%)4.288s (-0.9%)1.560s71.00x
▲ VercelExpress3.170s (+16.4% 🔺)4.654s (+9.8% 🔺)1.484s71.16x
▲ VercelNext.js (Turbopack)3.271s (-13.0% 🟢)5.128s (-7.0% 🟢)1.857s61.20x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.608s (-26.8% 🟢)1.006s (~)0.399s601.00x
🌐 RedisNext.js (Turbopack)0.708s (+11.4% 🔺)1.004s (~)0.296s601.16x
🐘 PostgresNext.js (Turbopack)0.819s (+6.2% 🔺)1.041s (+1.7%)0.222s581.35x
🐘 PostgresNitro0.858s (+2.2%)1.023s (+1.7%)0.165s591.41x
💻 LocalNext.js (Turbopack)0.860s (-2.7%)1.005s (-3.3%)0.145s601.41x
💻 LocalNitro0.980s (-5.4% 🟢)1.115s (-44.4% 🟢)0.135s541.61x
💻 LocalExpress1.016s (+4.1%)1.654s (+48.3% 🔺)0.638s371.67x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro8.903s (-8.6% 🟢)10.501s (-8.5% 🟢)1.599s61.00x
▲ VercelNext.js (Turbopack)9.632s (-23.0% 🟢)11.579s (-17.7% 🟢)1.948s61.08x
▲ VercelExpress10.403s (+2.7%)12.297s (+1.4%)1.893s51.17x

🔍 Observability: Nitro | Next.js (Turbopack) | Express

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.477s (-26.2% 🟢)2.007s (-16.4% 🟢)0.530s451.00x
🌐 RedisNext.js (Turbopack)1.672s (+15.4% 🔺)2.006s (~)0.333s451.13x
🐘 PostgresNitro2.078s (+3.4%)2.944s (+16.1% 🔺)0.866s311.41x
🐘 PostgresNext.js (Turbopack)2.153s (+14.8% 🔺)2.821s (+37.5% 🔺)0.668s321.46x
💻 LocalNext.js (Turbopack)2.700s (~)3.008s (~)0.308s301.83x
💻 LocalNitro3.022s (-1.1%)3.547s (-9.6% 🟢)0.525s262.05x
💻 LocalExpress3.162s (+3.6%)3.967s (+4.3%)0.805s232.14x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro28.495s (-3.9%)30.408s (-3.5%)1.913s31.00x
▲ VercelExpress28.833s (-1.1%)30.922s (-1.2%)2.089s31.01x
▲ VercelNext.js (Turbopack)29.626s (-0.8%)31.356s (-2.6%)1.730s31.04x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express3.109s (-23.3% 🟢)3.790s (-20.7% 🟢)0.681s321.00x
🌐 RedisNext.js (Turbopack)3.352s (+11.2% 🔺)4.008s (+15.7% 🔺)0.656s301.08x
🐘 PostgresNext.js (Turbopack)3.994s (+5.8% 🔺)4.331s (+8.0% 🔺)0.337s281.28x
🐘 PostgresNitro4.191s (~)4.932s (-1.6%)0.741s251.35x
💻 LocalNext.js (Turbopack)8.691s (+2.0%)9.019s (~)0.328s142.80x
💻 LocalExpress9.039s (-1.3%)9.633s (-1.6%)0.594s132.91x
💻 LocalNitro9.104s (+2.1%)9.710s (+3.3%)0.606s132.93x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express73.513s (-14.6% 🟢)75.368s (-14.2% 🟢)1.855s21.00x
▲ VercelNitro75.626s (+6.1% 🔺)77.847s (+6.5% 🔺)2.221s21.03x
▲ VercelNext.js (Turbopack)77.885s (-4.4%)79.559s (-4.2%)1.675s21.06x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.226s (-18.4% 🟢)1.007s (~)0.781s601.00x
🐘 PostgresNext.js (Turbopack)0.251s (+4.8%)1.007s (~)0.757s601.11x
🐘 PostgresNitro0.281s (-3.7%)1.007s (~)0.725s601.24x
🌐 RedisNext.js (Turbopack)0.284s (+7.7% 🔺)1.004s (~)0.720s601.26x
💻 LocalNitro0.588s (+4.9%)1.004s (~)0.417s602.60x
💻 LocalNext.js (Turbopack)0.620s (+20.2% 🔺)1.022s (+1.7%)0.402s592.74x
💻 LocalExpress0.697s (+18.7% 🔺)1.096s (+9.1% 🔺)0.398s553.08x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.544s (-22.1% 🟢)3.163s (-11.6% 🟢)1.620s201.00x
▲ VercelNext.js (Turbopack)1.688s (-5.7% 🟢)3.573s (+3.3%)1.886s181.09x
▲ VercelNitro1.727s (-8.8% 🟢)3.318s (-4.5%)1.591s191.12x

🔍 Observability: Express | Next.js (Turbopack) | Nitro

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.363s (-27.3% 🟢)1.006s (~)0.643s901.00x
🐘 PostgresNitro0.493s (-1.8%)1.007s (~)0.514s901.36x
🐘 PostgresNext.js (Turbopack)0.508s (+4.6%)1.007s (~)0.499s901.40x
🌐 RedisNext.js (Turbopack)1.144s (+2.9%)2.006s (+17.7% 🔺)0.862s453.15x
💻 LocalExpress2.492s (-1.6%)3.009s (~)0.517s306.86x
💻 LocalNitro2.652s (+14.2% 🔺)3.180s (+5.7% 🔺)0.528s297.31x
💻 LocalNext.js (Turbopack)2.676s (+7.7% 🔺)3.076s (+2.2%)0.401s307.37x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.612s (-2.1%)4.132s (-6.5% 🟢)1.519s221.00x
▲ VercelExpress2.738s (-7.0% 🟢)4.568s (-3.6%)1.830s201.05x
▲ VercelNext.js (Turbopack)3.366s (-5.4% 🟢)4.890s (-7.9% 🟢)1.524s191.29x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.583s (-26.2% 🟢)1.007s (~)0.423s1201.00x
🐘 PostgresNitro0.809s (-0.9%)1.009s (~)0.200s1191.39x
🐘 PostgresNext.js (Turbopack)0.847s (+10.4% 🔺)1.043s (+3.6%)0.196s1161.45x
🌐 RedisNext.js (Turbopack)2.677s (-3.5%)3.007s (-0.8%)0.330s404.59x
💻 LocalNitro10.890s (+4.7%)11.485s (+4.1%)0.595s1118.67x
💻 LocalExpress10.935s (~)11.395s (-1.5%)0.460s1118.75x
💻 LocalNext.js (Turbopack)11.065s (+7.4% 🔺)11.572s (+7.4% 🔺)0.507s1118.97x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro6.908s (+9.6% 🔺)8.327s (+3.7%)1.419s151.00x
▲ VercelNext.js (Turbopack)47.028s (+673.5% 🔺)48.733s (+526.0% 🔺)1.705s66.81x
▲ VercelExpress49.426s (+616.1% 🔺)51.745s (+489.1% 🔺)2.320s77.15x

🔍 Observability: Nitro | Next.js (Turbopack) | Express

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.158s (-25.4% 🟢)1.001s (+0.5%)0.001s (-26.7% 🟢)1.009s (~)0.851s101.00x
💻 LocalNext.js (Turbopack)0.170s (+0.5%)1.002s (~)0.012s (+14.7% 🔺)1.017s (~)0.847s101.07x
🐘 PostgresNext.js (Turbopack)0.192s (+0.8%)1.001s (~)0.001s (-15.4% 🟢)1.010s (~)0.818s101.21x
💻 LocalNitro0.207s (-0.7%)1.004s (~)0.012s (+11.5% 🔺)1.017s (~)0.810s101.31x
💻 LocalExpress0.219s (+6.8% 🔺)1.004s (~)0.011s (-8.5% 🟢)1.017s (~)0.798s101.38x
🐘 PostgresNitro0.219s (+2.0%)0.996s (~)0.001s (+30.0% 🔺)1.012s (~)0.792s101.39x
🌐 RedisNext.js (Turbopack)0.242s (+62.9% 🔺)1.001s (~)0.032s (+1776.5% 🔺)1.037s (+3.0%)0.796s101.53x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)1.493s (-11.9% 🟢)2.833s (-11.4% 🟢)0.461s (-51.8% 🟢)3.696s (-20.0% 🟢)2.203s101.00x
▲ VercelExpress1.565s (+3.1%)2.942s (+1.1%)0.642s (-16.1% 🟢)4.033s (-2.6%)2.468s101.05x
▲ VercelNitro1.603s (-5.5% 🟢)2.899s (-11.0% 🟢)0.562s (+7.0% 🔺)3.812s (-9.8% 🟢)2.208s101.07x

🔍 Observability: Next.js (Turbopack) | Express | Nitro

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)0.481s (+14.5% 🔺)1.002s (~)0.003s (+4.8%)1.011s (~)0.530s601.00x
🐘 PostgresExpress0.522s (-16.5% 🟢)1.006s (~)0.004s (-10.2% 🟢)1.022s (~)0.500s591.09x
🐘 PostgresNitro0.629s (-2.0%)1.005s (~)0.004s (-11.2% 🟢)1.022s (~)0.393s591.31x
🐘 PostgresNext.js (Turbopack)0.659s (+9.3% 🔺)1.026s (+1.7%)0.008s (+110.1% 🔺)1.044s (+2.2%)0.385s581.37x
💻 LocalNext.js (Turbopack)0.763s (+13.8% 🔺)1.011s (~)0.009s (-15.6% 🟢)1.116s (+8.9% 🔺)0.353s541.59x
💻 LocalNitro0.815s (-14.6% 🟢)1.012s (~)0.010s (+2.4%)1.111s (-9.5% 🟢)0.296s571.70x
💻 LocalExpress0.847s (+18.5% 🔺)1.010s (~)0.010s (+6.7% 🔺)1.115s (+8.9% 🔺)0.268s541.76x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.092s (+1.5%)5.761s (~)0.242s (-2.7%)6.436s (-0.6%)2.344s101.00x
▲ VercelNext.js (Turbopack)4.186s (-6.5% 🟢)6.079s (-0.6%)0.220s (+9.9% 🔺)6.720s (~)2.534s91.02x
▲ VercelNitro4.499s (+6.2% 🔺)5.768s (+2.8%)0.457s (+71.8% 🔺)6.725s (+5.7% 🔺)2.226s91.10x

🔍 Observability: Express | Next.js (Turbopack) | Nitro

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)0.900s (+1.8%)1.035s (+3.5%)0.000s (+106.9% 🔺)1.039s (+3.4%)0.139s581.00x
🐘 PostgresExpress0.931s (-3.9%)1.146s (-5.9% 🟢)0.000s (+172.2% 🔺)1.159s (-6.0% 🟢)0.228s541.03x
🐘 PostgresNitro0.946s (~)1.220s (+12.0% 🔺)0.000s (-17.3% 🟢)1.232s (+10.3% 🔺)0.286s491.05x
🐘 PostgresNext.js (Turbopack)0.956s (+3.1%)1.177s (+5.6% 🔺)0.000s (+Infinity% 🔺)1.185s (+4.2%)0.229s511.06x
💻 LocalNitro1.218s (+2.4%)2.021s (~)0.000s (-7.1% 🟢)2.023s (~)0.805s301.35x
💻 LocalNext.js (Turbopack)1.256s (~)2.019s (~)0.000s (-11.1% 🟢)2.022s (~)0.767s301.40x
💻 LocalExpress1.444s (+17.6% 🔺)2.022s (~)0.001s (+14.8% 🔺)2.203s (+8.8% 🔺)0.758s281.60x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.826s (~)4.141s (+4.9%)0.000s (+150.0% 🔺)4.585s (+4.0%)1.759s141.00x
▲ VercelNitro2.860s (+7.4% 🔺)4.133s (+4.8%)0.000s (-50.0% 🟢)4.541s (+3.8%)1.681s141.01x
▲ VercelNext.js (Turbopack)3.188s (-28.9% 🟢)4.619s (-22.8% 🟢)0.000s (-100.0% 🟢)5.035s (-21.8% 🟢)1.847s121.13x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

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

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)1.728s (-1.9%)2.035s (+1.7%)0.000s (-50.0% 🟢)2.039s (+1.6%)0.311s301.00x
🐘 PostgresExpress1.739s (-1.1%)2.143s (+3.8%)0.000s (-65.5% 🟢)2.152s (+3.4%)0.413s281.01x
🐘 PostgresNitro1.789s (+2.3%)2.102s (~)0.000s (+Infinity% 🔺)2.113s (-1.0%)0.324s291.04x
🐘 PostgresNext.js (Turbopack)1.958s (+6.4% 🔺)2.147s (+0.6%)0.000s (+Infinity% 🔺)2.155s (~)0.197s281.13x
💻 LocalNitro3.410s (+6.4% 🔺)4.034s (+1.7%)0.001s (+140.0% 🔺)4.037s (+1.7%)0.627s151.97x
💻 LocalNext.js (Turbopack)3.725s (+3.2%)4.233s (+1.6%)0.000s (-12.5% 🟢)4.236s (+1.6%)0.511s152.16x
💻 LocalExpress3.802s (+9.2% 🔺)4.368s (+8.3% 🔺)0.001s (+233.3% 🔺)4.372s (+8.3% 🔺)0.570s152.20x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.969s (-25.3% 🟢)5.072s (-23.7% 🟢)0.000s (-62.5% 🟢)5.489s (-22.2% 🟢)1.521s121.00x
▲ VercelNext.js (Turbopack)3.988s5.498s0.000s5.941s1.953s111.01x
▲ VercelExpress5.598s (+32.4% 🔺)7.223s (+32.7% 🔺)0.000s (-31.3% 🟢)7.670s (+30.2% 🔺)2.072s81.41x

🔍 Observability: Nitro | Next.js (Turbopack) | Express

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNext.js (Turbopack)10/21
🐘 PostgresExpress19/21
▲ VercelExpress12/21
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express🐘 Postgres17/21
Next.js (Turbopack)🌐 Redis10/21
Nitro🐘 Postgres15/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)

📋 View full workflow run

CopilotAI 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.

Pull request overview

Adds a workflow-mode optimization to the SWC plugin so that after "use step" class members are stripped, any now-unreferenced private class members are also removed, improving downstream tree-shaking (e.g., dropping Node-only imports kept alive by unused helpers).

Changes:

  • Implement iterative private-member reachability analysis and removal after step-member stripping (class decls + class exprs).
  • Add a new transform fixture validating cascading elimination of TypeScript private members in workflow mode.
  • Document the new optimization and publish a patch changeset.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 5 comments.

Show a summary per file
FileDescription
packages/swc-plugin-workflow/transform/src/lib.rsAdds private-member reference collection + DCE pass after "use step" stripping.
packages/swc-plugin-workflow/transform/tests/fixture/private-member-dce/input.tsNew fixture input covering cascading TS private elimination.
packages/swc-plugin-workflow/transform/tests/fixture/private-member-dce/output-workflow.jsExpected workflow output with TS-private members removed and imports dropped.
packages/swc-plugin-workflow/transform/tests/fixture/private-member-dce/output-step.jsExpected step-mode output (private members retained).
packages/swc-plugin-workflow/transform/tests/fixture/private-member-dce/output-client.jsExpected client-mode output (private members retained).
packages/swc-plugin-workflow/spec.mdDocuments the private-member DCE behavior and example.
.changeset/private-member-dce.mdDeclares a patch release for the SWC plugin change.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +619 to +624
// TS private or any member: `this.foo` — only track when
// the object is `this` (to avoid false positives from
// unrelated member accesses like `obj.foo`)
MemberProp::Ident(ident) => {
if matches!(&*expr.obj, Expr::This(_)) {
self.referenced.insert(ident.sym.to_string());

CopilotAIApr 9, 2026

Copy link

Choose a reason for hiding this comment

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

ClassMemberRefCollector only records MemberProp::Ident accesses when the object is this. In TypeScript, private members are also legally accessed via other instances of the same class (e.g. static compare(a: C, b: C) { return a.x - b.x }). With the current check, those references won’t be collected and the private member can be incorrectly removed, breaking runtime behavior. Consider treating any .ident access as a reference when ident matches a known TS-private member name in the class, regardless of the receiver expression (safe, though more conservative).

Suggested change
// TS private or any member: `this.foo` — only track when
// the object is `this` (to avoid false positives from
// unrelated member accesses like `obj.foo`)
MemberProp::Ident(ident) => {
ifmatches!(&*expr.obj,Expr::This(_)){
self.referenced.insert(ident.sym.to_string());
// TS private or any member access. Track `this.foo` as before,
// and also track `obj.foo` when `foo` is a known TS-private
// member of the current class so same-class accesses like
// `a.x` / `b.x` are not missed.
MemberProp::Ident(ident) => {
let name = ident.sym.to_string();
ifmatches!(&*expr.obj,Expr::This(_)) || self.private_names.contains(&name){
self.referenced.insert(name);

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Fixed. The collector now holds a ts_private_names: HashSet<String> built from the class body. MemberProp::Ident accesses are tracked when the name is in that set, regardless of the receiver expression. This catches a.x / b.x patterns in static methods.

Comment on lines +629 to +630
// Continue visiting children
expr.obj.visit_with(self);

CopilotAIApr 9, 2026

Copy link

Choose a reason for hiding this comment

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

visit_member_expr overrides default traversal but only visits expr.obj. For computed member expressions (obj[expr]), the property expression is not visited, so any this.#x / this.x references inside the computed key won’t be collected and could cause incorrectly eliminating private members. Use expr.visit_children_with(self) (or explicitly visit the computed property expression) to preserve full traversal.

Suggested change
// Continue visiting children
expr.obj.visit_with(self);
// Continue visiting children, including computed property expressions.
expr.visit_children_with(self);

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Fixed. Changed to expr.visit_children_with(self) which visits both the object and property expressions, including computed keys.

Comment on lines +614 to +625
match &expr.prop {
// Native JS private: `this.#foo`
MemberProp::PrivateName(name) => {
self.referenced.insert(name.name.to_string());
}
// TS private or any member: `this.foo` — only track when
// the object is `this` (to avoid false positives from
// unrelated member accesses like `obj.foo`)
MemberProp::Ident(ident) => {
if matches!(&*expr.obj, Expr::This(_)) {
self.referenced.insert(ident.sym.to_string());
}

CopilotAIApr 9, 2026

Copy link

Choose a reason for hiding this comment

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

Native private #foo and public/TS member foo are both stored as the same string ("foo"). If a class contains both foo and #foo, a this.foo access will incorrectly mark #foo as referenced, preventing intended elimination. Consider encoding JS-private names distinctly (e.g. prefix with # or store an enum) so #foo and foo don’t collide.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Fixed. JS native private names are now stored with a # prefix (e.g. "#foo") while TS private names remain unprefixed ("foo"). private_member_name() returns the prefixed form for PrivateMethod/PrivateProp, and visit_member_expr inserts format!("#{}", name) for MemberProp::PrivateName.

Comment on lines +1 to +25
import { WORKFLOW_SERIALIZE, WORKFLOW_DESERIALIZE } from '@workflow/serde';
import { getWorld } from './world.js';
import { importKey } from './encryption.js';

export class Run {
static [WORKFLOW_SERIALIZE](instance: Run) {
return { id: instance.id };
}

static [WORKFLOW_DESERIALIZE](data: { id: string }) {
return new Run(data.id);
}

id: string;

// TS private field — only referenced by stripped methods
private encryptionKeyPromise: Promise<any> | null = null;

// TS private method — only called by stripped getters/methods
private async getEncryptionKey(): Promise<any> {
if (!this.encryptionKeyPromise) {
this.encryptionKeyPromise = importKey(this.id);
}
return this.encryptionKeyPromise;
}

CopilotAIApr 9, 2026

Copy link

Choose a reason for hiding this comment

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

The new DCE logic claims to support native JS private members (#field/#method()), but the fixture only covers TypeScript private. Adding a fixture that uses #private syntax (including a cascading case) would help prevent regressions in the JS-private path.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Added in the second commit: private-member-dce-native/input.js tests JS native #field and #method() DCE including cascading elimination (#encryptionKeyPromise eliminated because #getEncryptionKey is eliminated) and survival of referenced members (#label kept because toString() references it).

Comment on lines +8504 to +8510
// Dead-code-eliminate unreferenced private members
// (same logic as visit_mut_class_decl above)
let referenced =
ClassMemberRefCollector::collect_from_class_body(&class_expr.class.body);
class_expr.class.body.retain(|member| match member {
ClassMember::PrivateMethod(m) => referenced.contains(&m.key.name.to_string()),
ClassMember::PrivateProp(p) => referenced.contains(&p.key.name.to_string()),

CopilotAIApr 9, 2026

Copy link

Choose a reason for hiding this comment

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

The private-member retention filter is duplicated in both visit_mut_class_decl and visit_mut_class_expr. This duplication increases the risk of future divergence/bugs when the logic changes. Consider extracting the retain logic into a shared helper (e.g. fn retain_referenced_private_members(body: &mut Vec<ClassMember>)) used by both visitors.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Fixed. Extracted retain_referenced_private_members(body: &mut Vec<ClassMember>) on ClassMemberRefCollector, called from both visit_mut_class_decl and visit_mut_class_expr.

- Namespace JS native private names with # prefix to avoid collisions
with TS private members of the same name
- Track TS private member accesses on non-this receivers (e.g. a.x in
static methods) by maintaining a set of known TS-private names
- Use visit_children_with for full traversal including computed member
expressions
- Extract retain logic into shared retain_referenced_private_members()
helper used by both visit_mut_class_decl and visit_mut_class_expr

@VaguelySeriousVaguelySerious left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

AI review: no blocking issues

// accesses like `a.x` / `b.x` are not missed.
MemberProp::Ident(ident) => {
let name = ident.sym.to_string();
if matches!(&*expr.obj, Expr::This(_)) || self.ts_private_names.contains(&name) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

AI Review: Note

For MemberProp::Ident, any this.foo access records foo in referenced, not only TS-private members. Retain still keys off private_member_name, so this is mostly extra churn in the set rather than incorrect retention, but it is slightly less precise than filtering to private names only.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Correct - the collector is conservative: it records all this.foo accesses, not just those matching known private names. This is intentional for simplicity. The extra entries in the referenced set are harmless since retain only removes members that private_member_name() identifies as private. Public members are always kept regardless of what's in the set.

});
```

This optimization is critical for SDK classes like `Run` where private helper methods reference Node.js-only imports (encryption, world access, etc.) — eliminating them allows the downstream module-level DCE to also remove those imports from the workflow bundle.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

AI Review: Nit

This section explains the algorithm well. If you want to reduce future issue reports, a short explicit note on unsupported patterns (for example computed this[...] access to private fields) could help set expectations.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Good suggestion. Computed access like this[name] where name happens to be a private member name won't be tracked, which could lead to incorrect elimination. In practice this pattern is very rare for private members (especially in SDK code like Run), but worth noting if we want to document limitations.

@TooTallNate
TooTallNate enabled auto-merge (squash) April 9, 2026 19:22
@TooTallNate
TooTallNate merged commit 66585fd into mainApr 9, 2026
102 of 103 checks passed
@TooTallNate
TooTallNate deleted the fix/swc-private-member-dce branch April 9, 2026 19:27
@ghostghost mentioned this pull request Apr 9, 2026
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

@TooTallNate@VaguelySerious
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

feat(swc): dead code eliminate unreferenced private class members in workflow mode - #1671

Merged
TooTallNate merged 3 commits into
mainfrom
fix/swc-private-member-dce
Apr 9, 2026
Merged

feat(swc): dead code eliminate unreferenced private class members in workflow mode#1671
TooTallNate merged 3 commits into
mainfrom
fix/swc-private-member-dce

Conversation

@TooTallNate

@TooTallNateTooTallNate commented Apr 9, 2026

Copy link
Copy Markdown
Member

Summary

After stripping "use step" methods from a class body in workflow mode, eliminate private members that are no longer referenced by any remaining public member. This applies to both:

  • JS native private: #field, #method()
  • TypeScript private: private field, private method()

The algorithm is iterative — references are seeded from public members, then expanded through surviving private members until a fixed point, enabling cascading elimination.

Example

Input:

exportclassRun{static[WORKFLOW_SERIALIZE](instance){return{id: instance.id};}static[WORKFLOW_DESERIALIZE](data){returnnewRun(data.id);}id: string;privateencryptionKeyPromise: Promise<any>|null=null;privateasyncgetEncryptionKey(){if(!this.encryptionKeyPromise){this.encryptionKeyPromise=importKey(this.id);}returnthis.encryptionKeyPromise;}constructor(id: string){this.id=id;}getvalue(): Promise<any>{'use step';returnthis.getEncryptionKey().then(()=>getWorld().get(this.id));}asynccancel(): Promise<void>{'use step';constkey=awaitthis.getEncryptionKey();awaitgetWorld().cancel(this.id,key);}toString(): string{return`Run(${this.id})`;}}

Workflow mode output:

exportclassRun{static[WORKFLOW_SERIALIZE](instance){return{id: instance.id};}static[WORKFLOW_DESERIALIZE](data){returnnewRun(data.id);}id;// ✅ private encryptionKeyPromise — ELIMINATED (only referenced by getEncryptionKey)// ✅ private getEncryptionKey() — ELIMINATED (only referenced by stripped step methods)constructor(id){this.id=id;}toString(){return`Run(${this.id})`;}}Run.prototype["cancel"]=globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//...");var__step_Run$value=globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//...");Object.defineProperty(Run.prototype,"value",{get(){return__step_Run$value.call(this);},configurable: true,enumerable: false});

The cascading elimination is key: encryptionKeyPromise is only referenced by getEncryptionKey(), which is itself only referenced by the stripped value getter and cancel() method. Both private members are removed, allowing the downstream module-level DCE to also eliminate the importKey and getWorld imports.

Motivation

SDK classes like Run have private helper methods that reference Node.js-only imports (encryption, world access). Without this optimization, those helpers survive into the workflow bundle even though nothing calls them after "use step" bodies are stripped, keeping the Node.js imports alive and preventing tree-shaking.

Test

New fixture: private-member-dce/input.ts with expected outputs for all three modes.

…workflow mode
After stripping 'use step' methods from a class body in workflow mode,
eliminate private members (both JS native #field/#method and TypeScript
private field/private method) that are no longer referenced by any
remaining public member.
The algorithm is iterative: references are seeded from public members,
then expanded through surviving private members until a fixed point,
enabling cascading elimination (e.g. a private field only referenced by
a private method that is itself unreferenced).
CopilotAI review requested due to automatic review settings April 9, 2026 18:09
@TooTallNate
TooTallNate requested a review from a team as a code ownerApril 9, 2026 18:09
@changeset-bot

changeset-botBot commented Apr 9, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 4ea576a

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

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

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 Apr 9, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Apr 9, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production912067979
✅ 💻 Local Development88601821068
✅ 📦 Local Production88601821068
✅ 🐘 Local Postgres88601821068
✅ 🪟 Windows810889
❌ 🌍 Community Worlds1366524225
✅ 📋 Other225042267
Total4012656874764

❌ Failed Tests

🌍 Community Worlds (65 failed)

mongodb (4 failed):

  • hookWorkflow is not resumable via public webhook endpoint | wrun_01KNSR613X7J6NJ3Q7CC1GAXC8
  • webhookWorkflow | wrun_01KNSR6A2N8E4FRA7K4W0WAJ2X
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously | wrun_01KNSRE4P2HFBR1HGDZ60FSZ4A
  • resilient start: addTenWorkflow completes when run_created returns 500 | wrun_01KNSRMTFPHDMEWW1M0C99YMJB

redis (3 failed):

  • hookWorkflow is not resumable via public webhook endpoint | wrun_01KNSR613X7J6NJ3Q7CC1GAXC8
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously | wrun_01KNSRE4P2HFBR1HGDZ60FSZ4A
  • resilient start: addTenWorkflow completes when run_created returns 500 | wrun_01KNSRMTFPHDMEWW1M0C99YMJB

turso (58 failed):

  • addTenWorkflow | wrun_01KNSR4Q12NYS08Q5M8JMBJE2P
  • addTenWorkflow | wrun_01KNSR4Q12NYS08Q5M8JMBJE2P
  • wellKnownAgentWorkflow (.well-known/agent) | wrun_01KNSR6H7RBB0VN5AF50A8K3HK
  • should work with react rendering in step
  • promiseAllWorkflow | wrun_01KNSR50EQH830PRD40KS32Q9Y
  • promiseRaceWorkflow | wrun_01KNSR54Y75RYQDBMAWK470VYP
  • promiseAnyWorkflow | wrun_01KNSR58C03DCWT8KGXWBMNCP6
  • importedStepOnlyWorkflow | wrun_01KNSR6WGHHN8XTHMJ86Q1JS10
  • hookWorkflow | wrun_01KNSR5M9AWG051SF57JDHTN15
  • hookWorkflow is not resumable via public webhook endpoint | wrun_01KNSR613X7J6NJ3Q7CC1GAXC8
  • webhookWorkflow | wrun_01KNSR6A2N8E4FRA7K4W0WAJ2X
  • sleepingWorkflow | wrun_01KNSR6G7XQP06VPNVA5S5SZ7P
  • parallelSleepWorkflow | wrun_01KNSR6W68GVQAECF6EXH5YKW4
  • nullByteWorkflow | wrun_01KNSR6ZZHZA6GX9A995NWGH4W
  • workflowAndStepMetadataWorkflow | wrun_01KNSR722J4AE22DPWYBGR40AA
  • fetchWorkflow | wrun_01KNSR9QTD0RQEWYMKGRM9P6KS
  • promiseRaceStressTestWorkflow | wrun_01KNSR9V7MWTQB99MYWGDBMPV1
  • error handling error propagation workflow errors nested function calls preserve message and stack trace
  • error handling error propagation workflow errors cross-file imports preserve message and stack trace
  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior FatalError fails immediately without retries
  • error handling retry behavior RetryableError respects custom retryAfter delay
  • error handling retry behavior maxRetries=0 disables retries
  • error handling catchability FatalError can be caught and detected with FatalError.is()
  • error handling not registered WorkflowNotRegisteredError fails the run when workflow does not exist
  • error handling not registered StepNotRegisteredError fails the step but workflow can catch it
  • error handling not registered StepNotRegisteredError fails the run when not caught in workflow
  • hookCleanupTestWorkflow - hook token reuse after workflow completion | wrun_01KNSRDFYWY5RDPW5MJW4M2RKP
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously | wrun_01KNSRE4P2HFBR1HGDZ60FSZ4A
  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running | wrun_01KNSREVJW6G5G63ZYP9DTG2CT
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars) | wrun_01KNSRFFZ3HX99QC8VH9X00X19
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument | wrun_01KNSRFS1Y9106Z5PVYGW5SZ8F
  • closureVariableWorkflow - nested step functions with closure variables | wrun_01KNSRFYM8QZ6MKBWEXNSXEA1E
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step | wrun_01KNSRG1102E8CS370B44Y7JJ1
  • health check (queue-based) - workflow and step endpoints respond to health check messages
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly | wrun_01KNSRGH373GH51TWFMF3M6THG
  • Calculator.calculate - static workflow method using static step methods from another class | wrun_01KNSRGPSM4VSQ1R3Z11NG4MZ0
  • AllInOneService.processNumber - static workflow method using sibling static step methods | wrun_01KNSRGXK317X0HN019FNHP541
  • ChainableService.processWithThis - static step methods using this to reference the class | wrun_01KNSRH5K54HKGJC6TW5YDC6P4
  • thisSerializationWorkflow - step function invoked with .call() and .apply() | wrun_01KNSRHCE7T09PPS45FA7D75MK
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE | wrun_01KNSRHM1A0G2BFRKSN1893XHR
  • instanceMethodStepWorkflow - instance methods with "use step" directive | wrun_01KNSRHTZQJP99YC0M9FNWQPM5
  • crossContextSerdeWorkflow - classes defined in step code are deserializable in workflow context | wrun_01KNSRJ68YGFBGXJ7YFR7ZHMV5
  • stepFunctionAsStartArgWorkflow - step function reference passed as start() argument | wrun_01KNSRJFBR9XRH3012W64TDAJ1
  • cancelRun - cancelling a running workflow | wrun_01KNSRJP4YQBW4BDB9ZFHE3JXZ
  • cancelRun via CLI - cancelling a running workflow | wrun_01KNSRK06K5725SZWQJ2DJZZ1K
  • pages router addTenWorkflow via pages router
  • pages router promiseAllWorkflow via pages router
  • pages router sleepingWorkflow via pages router
  • hookWithSleepWorkflow - hook payloads delivered correctly with concurrent sleep | wrun_01KNSRKDA53YRF17ST7YTD02BW
  • sleepInLoopWorkflow - sleep inside loop with steps actually delays each iteration | wrun_01KNSRM3T179X2E69VG894KAX9
  • sleepWithSequentialStepsWorkflow - sequential steps work with concurrent sleep (control) | wrun_01KNSRME2NVXNSBMBR8AND2DKJ
  • importMetaUrlWorkflow - import.meta.url is available in step bundles | wrun_01KNSRMP16CE789X5BG39DBJEA
  • metadataFromHelperWorkflow - getWorkflowMetadata/getStepMetadata work from module-level helper (#1577) | wrun_01KNSRMR5GRYXW87SN5A0AM9TV
  • resilient start: addTenWorkflow completes when run_created returns 500 | wrun_01KNSRMTFPHDMEWW1M0C99YMJB
  • getterStepWorkflow - getter functions with "use step" directive | wrun_01KNSRMXQTQ6GKDFR5NN8DMJRA

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro8207
✅ example8207
✅ express8207
✅ fastify8207
✅ hono8207
✅ nextjs-turbopack8702
✅ nextjs-webpack8702
✅ nitro8207
✅ nuxt8207
✅ sveltekit8207
✅ vite8207
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable75014
✅ express-stable75014
✅ fastify-stable75014
✅ hono-stable75014
✅ nextjs-turbopack-canary62027
✅ nextjs-turbopack-stable8108
✅ nextjs-webpack-canary62027
✅ nextjs-webpack-stable8108
✅ nitro-stable75014
✅ nuxt-stable75014
✅ sveltekit-stable75014
✅ vite-stable75014
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable75014
✅ express-stable75014
✅ fastify-stable75014
✅ hono-stable75014
✅ nextjs-turbopack-canary62027
✅ nextjs-turbopack-stable8108
✅ nextjs-webpack-canary62027
✅ nextjs-webpack-stable8108
✅ nitro-stable75014
✅ nuxt-stable75014
✅ sveltekit-stable75014
✅ vite-stable75014
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable75014
✅ express-stable75014
✅ fastify-stable75014
✅ hono-stable75014
✅ nextjs-turbopack-canary62027
✅ nextjs-turbopack-stable8108
✅ nextjs-webpack-canary62027
✅ nextjs-webpack-stable8108
✅ nitro-stable75014
✅ nuxt-stable75014
✅ sveltekit-stable75014
✅ vite-stable75014
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack8108
❌ 🌍 Community Worlds
AppPassedFailedSkipped
✅ mongodb-dev500
❌ mongodb5848
✅ redis-dev500
❌ redis5938
✅ turso-dev500
❌ turso4588
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable75014
✅ e2e-local-postgres-nest-stable75014
✅ e2e-local-prod-nest-stable75014

📋 View full workflow run

@github-actions

github-actionsBot commented Apr 9, 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🥇 Express0.043s (-1.6%)1.006s (~)0.962s101.00x
💻 LocalNitro0.044s (-4.8%)1.005s (~)0.961s101.01x
🐘 PostgresExpress0.046s (-22.5% 🟢)1.011s (~)0.965s101.07x
💻 LocalNext.js (Turbopack)0.049s (+3.6%)1.005s (~)0.957s101.12x
🌐 RedisNext.js (Turbopack)0.050s (-11.3% 🟢)1.005s (~)0.955s101.16x
🐘 PostgresNext.js (Turbopack)0.058s (-3.0%)1.011s (~)0.954s101.33x
🐘 PostgresNitro0.063s (+2.3%)1.012s (~)0.949s101.46x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express0.231s (-39.8% 🟢)2.203s (-10.5% 🟢)1.972s101.00x
▲ VercelNext.js (Turbopack)0.267s (-33.8% 🟢)2.195s (-8.4% 🟢)1.928s101.16x
▲ VercelNitro0.304s (+10.2% 🔺)2.211s (+4.0%)1.907s101.32x

🔍 Observability: Express | Next.js (Turbopack) | Nitro

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.119s (-3.1%)2.010s (~)0.891s101.00x
🌐 RedisNext.js (Turbopack)1.126s (~)2.007s (~)0.881s101.01x
💻 LocalNext.js (Turbopack)1.126s (~)2.006s (~)0.880s101.01x
💻 LocalNitro1.128s (~)2.005s (~)0.877s101.01x
💻 LocalExpress1.131s (~)2.006s (~)0.875s101.01x
🐘 PostgresNext.js (Turbopack)1.147s (~)2.009s (~)0.862s101.02x
🐘 PostgresNitro1.147s (-0.6%)2.011s (~)0.864s101.02x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.911s (+1.0%)3.886s (+0.8%)1.975s101.00x
▲ VercelNitro2.003s (+6.5% 🔺)3.356s (-10.3% 🟢)1.353s101.05x
▲ VercelNext.js (Turbopack)2.103s (+4.0%)3.881s (+3.0%)1.778s101.10x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express10.694s (-2.1%)11.028s (~)0.334s31.00x
🌐 RedisNext.js (Turbopack)10.779s (+0.7%)11.023s (~)0.244s31.01x
💻 LocalNext.js (Turbopack)10.831s (~)11.025s (~)0.194s31.01x
🐘 PostgresNext.js (Turbopack)10.849s (~)11.023s (~)0.174s31.01x
💻 LocalNitro10.920s (~)11.023s (~)0.103s31.02x
🐘 PostgresNitro10.934s (~)11.023s (~)0.088s31.02x
💻 LocalExpress10.952s (~)11.024s (~)0.072s31.02x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express16.776s (~)18.801s (-1.4%)2.025s21.00x
▲ VercelNext.js (Turbopack)17.133s (-0.8%)19.157s (~)2.024s21.02x
▲ VercelNitro17.627s (-1.4%)18.953s (-5.7% 🟢)1.326s21.05x

🔍 Observability: Express | Next.js (Turbopack) | Nitro

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express14.019s (-3.8%)14.627s (-2.6%)0.608s51.00x
🌐 RedisNext.js (Turbopack)14.257s (+0.9%)15.029s (~)0.772s41.02x
🐘 PostgresNext.js (Turbopack)14.412s (-0.6%)15.023s (~)0.612s41.03x
🐘 PostgresNitro14.702s (~)15.021s (~)0.319s41.05x
💻 LocalNext.js (Turbopack)14.709s (+1.0%)15.030s (~)0.322s41.05x
💻 LocalNitro14.924s (-0.6%)15.028s (-3.2%)0.104s41.06x
💻 LocalExpress14.989s (~)15.030s (~)0.041s41.07x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express33.420s (-3.4%)35.676s (-3.4%)2.256s21.00x
▲ VercelNext.js (Turbopack)34.176s (-3.3%)36.300s (-3.0%)2.124s21.02x
▲ VercelNitro34.324s (+0.8%)35.857s (-1.2%)1.533s21.03x

🔍 Observability: Express | Next.js (Turbopack) | Nitro

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express12.957s (-8.6% 🟢)13.021s (-13.3% 🟢)0.064s71.00x
🌐 RedisNext.js (Turbopack)13.389s (+1.2%)14.025s (~)0.636s71.03x
🐘 PostgresNext.js (Turbopack)13.757s (~)14.019s (~)0.262s71.06x
🐘 PostgresNitro14.192s (-0.9%)15.022s (~)0.829s61.10x
💻 LocalNext.js (Turbopack)16.033s (+2.8%)16.531s (+3.1%)0.498s61.24x
💻 LocalExpress16.441s (-0.7%)17.032s (~)0.591s61.27x
💻 LocalNitro16.531s (~)17.029s (~)0.498s61.28x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)56.887s (-2.0%)58.539s (-2.7%)1.652s21.00x
▲ VercelExpress59.072s (+7.5% 🔺)61.603s (+7.7% 🔺)2.531s21.04x
▲ VercelNitro59.188s (+2.0%)60.723s (~)1.535s21.04x

🔍 Observability: Next.js (Turbopack) | Express | Nitro

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.211s (-8.6% 🟢)2.009s (~)0.798s151.00x
🐘 PostgresNext.js (Turbopack)1.232s (~)2.009s (~)0.777s151.02x
🐘 PostgresNitro1.260s (~)2.009s (~)0.749s151.04x
🌐 RedisNext.js (Turbopack)1.318s (+6.2% 🔺)2.006s (~)0.688s151.09x
💻 LocalExpress1.515s (+0.8%)2.006s (~)0.491s151.25x
💻 LocalNitro1.516s (+2.1%)2.005s (~)0.490s151.25x
💻 LocalNext.js (Turbopack)1.544s (+4.6%)2.006s (~)0.461s151.27x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.253s (-6.7% 🟢)4.180s (+4.1%)1.927s81.00x
▲ VercelNitro2.496s (-15.3% 🟢)3.670s (-19.6% 🟢)1.174s91.11x
▲ VercelNext.js (Turbopack)2.719s (+23.5% 🔺)4.105s (+6.9% 🔺)1.386s81.21x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.300s (-2.9%)3.009s (~)0.708s101.00x
🐘 PostgresNitro2.330s (+0.7%)3.009s (~)0.680s101.01x
🐘 PostgresNext.js (Turbopack)2.385s (-0.7%)3.009s (~)0.624s101.04x
🌐 RedisNext.js (Turbopack)2.564s (+5.3% 🔺)3.008s (~)0.444s101.11x
💻 LocalExpress2.895s (+1.4%)3.453s (+3.3%)0.558s91.26x
💻 LocalNitro2.991s (+6.5% 🔺)3.341s (+11.1% 🔺)0.351s91.30x
💻 LocalNext.js (Turbopack)3.077s (+24.5% 🔺)3.884s (+29.1% 🔺)0.808s81.34x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.670s (-10.7% 🟢)4.008s (-14.7% 🟢)1.338s81.00x
▲ VercelNext.js (Turbopack)2.780s (+3.0%)4.402s (-1.8%)1.622s71.04x
▲ VercelExpress2.879s (+4.4%)4.437s (+2.9%)1.558s71.08x

🔍 Observability: Nitro | Next.js (Turbopack) | Express

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express3.383s (-2.3%)4.011s (~)0.628s81.00x
🐘 PostgresNitro3.472s (-0.5%)4.011s (~)0.538s81.03x
🐘 PostgresNext.js (Turbopack)3.643s (-1.1%)4.009s (~)0.367s81.08x
🌐 RedisNext.js (Turbopack)4.164s (+5.3% 🔺)5.011s (+16.6% 🔺)0.847s61.23x
💻 LocalExpress7.500s (-8.6% 🟢)8.021s (-8.5% 🟢)0.521s42.22x
💻 LocalNitro8.022s (+1.6%)8.773s (+6.1% 🔺)0.752s42.37x
💻 LocalNext.js (Turbopack)8.384s (+47.2% 🔺)9.019s (+50.0% 🔺)0.635s42.48x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.054s (-1.0%)5.367s (+9.7% 🔺)2.313s61.00x
▲ VercelNitro3.295s (+8.8% 🔺)4.508s (-7.1% 🟢)1.213s71.08x
▲ VercelNext.js (Turbopack)3.678s (+1.9%)5.769s (+8.0% 🔺)2.091s61.20x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.204s (-4.3%)2.008s (~)0.804s151.00x
🐘 PostgresNext.js (Turbopack)1.251s (+2.1%)2.009s (~)0.758s151.04x
🐘 PostgresNitro1.264s (~)2.008s (~)0.744s151.05x
🌐 RedisNext.js (Turbopack)1.278s (-0.6%)2.006s (~)0.729s151.06x
💻 LocalExpress1.519s (~)2.006s (~)0.487s151.26x
💻 LocalNitro1.524s (-3.4%)2.006s (~)0.482s151.27x
💻 LocalNext.js (Turbopack)1.556s (~)2.007s (-3.2%)0.451s151.29x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.003s (-4.2%)3.908s (-1.8%)1.905s81.00x
▲ VercelNitro2.006s (-7.6% 🟢)3.636s (-6.7% 🟢)1.630s91.00x
▲ VercelNext.js (Turbopack)2.217s (+6.1% 🔺)3.745s (+5.0% 🔺)1.529s91.11x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro2.329s (~)3.010s (~)0.681s101.00x
🐘 PostgresExpress2.332s (-0.9%)3.008s (~)0.676s101.00x
🐘 PostgresNext.js (Turbopack)2.390s (~)3.011s (~)0.620s101.03x
🌐 RedisNext.js (Turbopack)2.543s (+3.6%)3.007s (~)0.464s101.09x
💻 LocalNext.js (Turbopack)2.807s (-10.8% 🟢)3.341s (-6.3% 🟢)0.533s91.21x
💻 LocalExpress2.938s (-0.9%)3.454s (-3.1%)0.516s91.26x
💻 LocalNitro2.990s (-1.3%)3.677s (-2.2%)0.687s91.28x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.320s (~)4.314s (+9.6% 🔺)1.994s81.00x
▲ VercelNitro2.485s (-3.3%)3.963s (-5.7% 🟢)1.478s81.07x
▲ VercelNext.js (Turbopack)2.779s (+9.3% 🔺)4.314s (+3.7%)1.535s71.20x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro3.453s (~)4.010s (~)0.557s81.00x
🐘 PostgresExpress3.457s (~)4.009s (~)0.553s81.00x
🐘 PostgresNext.js (Turbopack)3.646s (~)4.009s (~)0.363s81.06x
🌐 RedisNext.js (Turbopack)4.169s (+3.5%)5.012s (+9.4% 🔺)0.843s61.21x
💻 LocalExpress8.033s (-6.6% 🟢)8.774s (-2.8%)0.741s42.33x
💻 LocalNitro8.669s (+2.3%)9.026s (~)0.357s42.51x
💻 LocalNext.js (Turbopack)8.695s (+12.2% 🔺)9.271s (+12.1% 🔺)0.576s42.52x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.728s (-4.1%)4.288s (-0.9%)1.560s71.00x
▲ VercelExpress3.170s (+16.4% 🔺)4.654s (+9.8% 🔺)1.484s71.16x
▲ VercelNext.js (Turbopack)3.271s (-13.0% 🟢)5.128s (-7.0% 🟢)1.857s61.20x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.608s (-26.8% 🟢)1.006s (~)0.399s601.00x
🌐 RedisNext.js (Turbopack)0.708s (+11.4% 🔺)1.004s (~)0.296s601.16x
🐘 PostgresNext.js (Turbopack)0.819s (+6.2% 🔺)1.041s (+1.7%)0.222s581.35x
🐘 PostgresNitro0.858s (+2.2%)1.023s (+1.7%)0.165s591.41x
💻 LocalNext.js (Turbopack)0.860s (-2.7%)1.005s (-3.3%)0.145s601.41x
💻 LocalNitro0.980s (-5.4% 🟢)1.115s (-44.4% 🟢)0.135s541.61x
💻 LocalExpress1.016s (+4.1%)1.654s (+48.3% 🔺)0.638s371.67x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro8.903s (-8.6% 🟢)10.501s (-8.5% 🟢)1.599s61.00x
▲ VercelNext.js (Turbopack)9.632s (-23.0% 🟢)11.579s (-17.7% 🟢)1.948s61.08x
▲ VercelExpress10.403s (+2.7%)12.297s (+1.4%)1.893s51.17x

🔍 Observability: Nitro | Next.js (Turbopack) | Express

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.477s (-26.2% 🟢)2.007s (-16.4% 🟢)0.530s451.00x
🌐 RedisNext.js (Turbopack)1.672s (+15.4% 🔺)2.006s (~)0.333s451.13x
🐘 PostgresNitro2.078s (+3.4%)2.944s (+16.1% 🔺)0.866s311.41x
🐘 PostgresNext.js (Turbopack)2.153s (+14.8% 🔺)2.821s (+37.5% 🔺)0.668s321.46x
💻 LocalNext.js (Turbopack)2.700s (~)3.008s (~)0.308s301.83x
💻 LocalNitro3.022s (-1.1%)3.547s (-9.6% 🟢)0.525s262.05x
💻 LocalExpress3.162s (+3.6%)3.967s (+4.3%)0.805s232.14x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro28.495s (-3.9%)30.408s (-3.5%)1.913s31.00x
▲ VercelExpress28.833s (-1.1%)30.922s (-1.2%)2.089s31.01x
▲ VercelNext.js (Turbopack)29.626s (-0.8%)31.356s (-2.6%)1.730s31.04x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express3.109s (-23.3% 🟢)3.790s (-20.7% 🟢)0.681s321.00x
🌐 RedisNext.js (Turbopack)3.352s (+11.2% 🔺)4.008s (+15.7% 🔺)0.656s301.08x
🐘 PostgresNext.js (Turbopack)3.994s (+5.8% 🔺)4.331s (+8.0% 🔺)0.337s281.28x
🐘 PostgresNitro4.191s (~)4.932s (-1.6%)0.741s251.35x
💻 LocalNext.js (Turbopack)8.691s (+2.0%)9.019s (~)0.328s142.80x
💻 LocalExpress9.039s (-1.3%)9.633s (-1.6%)0.594s132.91x
💻 LocalNitro9.104s (+2.1%)9.710s (+3.3%)0.606s132.93x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express73.513s (-14.6% 🟢)75.368s (-14.2% 🟢)1.855s21.00x
▲ VercelNitro75.626s (+6.1% 🔺)77.847s (+6.5% 🔺)2.221s21.03x
▲ VercelNext.js (Turbopack)77.885s (-4.4%)79.559s (-4.2%)1.675s21.06x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.226s (-18.4% 🟢)1.007s (~)0.781s601.00x
🐘 PostgresNext.js (Turbopack)0.251s (+4.8%)1.007s (~)0.757s601.11x
🐘 PostgresNitro0.281s (-3.7%)1.007s (~)0.725s601.24x
🌐 RedisNext.js (Turbopack)0.284s (+7.7% 🔺)1.004s (~)0.720s601.26x
💻 LocalNitro0.588s (+4.9%)1.004s (~)0.417s602.60x
💻 LocalNext.js (Turbopack)0.620s (+20.2% 🔺)1.022s (+1.7%)0.402s592.74x
💻 LocalExpress0.697s (+18.7% 🔺)1.096s (+9.1% 🔺)0.398s553.08x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.544s (-22.1% 🟢)3.163s (-11.6% 🟢)1.620s201.00x
▲ VercelNext.js (Turbopack)1.688s (-5.7% 🟢)3.573s (+3.3%)1.886s181.09x
▲ VercelNitro1.727s (-8.8% 🟢)3.318s (-4.5%)1.591s191.12x

🔍 Observability: Express | Next.js (Turbopack) | Nitro

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.363s (-27.3% 🟢)1.006s (~)0.643s901.00x
🐘 PostgresNitro0.493s (-1.8%)1.007s (~)0.514s901.36x
🐘 PostgresNext.js (Turbopack)0.508s (+4.6%)1.007s (~)0.499s901.40x
🌐 RedisNext.js (Turbopack)1.144s (+2.9%)2.006s (+17.7% 🔺)0.862s453.15x
💻 LocalExpress2.492s (-1.6%)3.009s (~)0.517s306.86x
💻 LocalNitro2.652s (+14.2% 🔺)3.180s (+5.7% 🔺)0.528s297.31x
💻 LocalNext.js (Turbopack)2.676s (+7.7% 🔺)3.076s (+2.2%)0.401s307.37x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.612s (-2.1%)4.132s (-6.5% 🟢)1.519s221.00x
▲ VercelExpress2.738s (-7.0% 🟢)4.568s (-3.6%)1.830s201.05x
▲ VercelNext.js (Turbopack)3.366s (-5.4% 🟢)4.890s (-7.9% 🟢)1.524s191.29x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.583s (-26.2% 🟢)1.007s (~)0.423s1201.00x
🐘 PostgresNitro0.809s (-0.9%)1.009s (~)0.200s1191.39x
🐘 PostgresNext.js (Turbopack)0.847s (+10.4% 🔺)1.043s (+3.6%)0.196s1161.45x
🌐 RedisNext.js (Turbopack)2.677s (-3.5%)3.007s (-0.8%)0.330s404.59x
💻 LocalNitro10.890s (+4.7%)11.485s (+4.1%)0.595s1118.67x
💻 LocalExpress10.935s (~)11.395s (-1.5%)0.460s1118.75x
💻 LocalNext.js (Turbopack)11.065s (+7.4% 🔺)11.572s (+7.4% 🔺)0.507s1118.97x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro6.908s (+9.6% 🔺)8.327s (+3.7%)1.419s151.00x
▲ VercelNext.js (Turbopack)47.028s (+673.5% 🔺)48.733s (+526.0% 🔺)1.705s66.81x
▲ VercelExpress49.426s (+616.1% 🔺)51.745s (+489.1% 🔺)2.320s77.15x

🔍 Observability: Nitro | Next.js (Turbopack) | Express

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.158s (-25.4% 🟢)1.001s (+0.5%)0.001s (-26.7% 🟢)1.009s (~)0.851s101.00x
💻 LocalNext.js (Turbopack)0.170s (+0.5%)1.002s (~)0.012s (+14.7% 🔺)1.017s (~)0.847s101.07x
🐘 PostgresNext.js (Turbopack)0.192s (+0.8%)1.001s (~)0.001s (-15.4% 🟢)1.010s (~)0.818s101.21x
💻 LocalNitro0.207s (-0.7%)1.004s (~)0.012s (+11.5% 🔺)1.017s (~)0.810s101.31x
💻 LocalExpress0.219s (+6.8% 🔺)1.004s (~)0.011s (-8.5% 🟢)1.017s (~)0.798s101.38x
🐘 PostgresNitro0.219s (+2.0%)0.996s (~)0.001s (+30.0% 🔺)1.012s (~)0.792s101.39x
🌐 RedisNext.js (Turbopack)0.242s (+62.9% 🔺)1.001s (~)0.032s (+1776.5% 🔺)1.037s (+3.0%)0.796s101.53x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)1.493s (-11.9% 🟢)2.833s (-11.4% 🟢)0.461s (-51.8% 🟢)3.696s (-20.0% 🟢)2.203s101.00x
▲ VercelExpress1.565s (+3.1%)2.942s (+1.1%)0.642s (-16.1% 🟢)4.033s (-2.6%)2.468s101.05x
▲ VercelNitro1.603s (-5.5% 🟢)2.899s (-11.0% 🟢)0.562s (+7.0% 🔺)3.812s (-9.8% 🟢)2.208s101.07x

🔍 Observability: Next.js (Turbopack) | Express | Nitro

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)0.481s (+14.5% 🔺)1.002s (~)0.003s (+4.8%)1.011s (~)0.530s601.00x
🐘 PostgresExpress0.522s (-16.5% 🟢)1.006s (~)0.004s (-10.2% 🟢)1.022s (~)0.500s591.09x
🐘 PostgresNitro0.629s (-2.0%)1.005s (~)0.004s (-11.2% 🟢)1.022s (~)0.393s591.31x
🐘 PostgresNext.js (Turbopack)0.659s (+9.3% 🔺)1.026s (+1.7%)0.008s (+110.1% 🔺)1.044s (+2.2%)0.385s581.37x
💻 LocalNext.js (Turbopack)0.763s (+13.8% 🔺)1.011s (~)0.009s (-15.6% 🟢)1.116s (+8.9% 🔺)0.353s541.59x
💻 LocalNitro0.815s (-14.6% 🟢)1.012s (~)0.010s (+2.4%)1.111s (-9.5% 🟢)0.296s571.70x
💻 LocalExpress0.847s (+18.5% 🔺)1.010s (~)0.010s (+6.7% 🔺)1.115s (+8.9% 🔺)0.268s541.76x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.092s (+1.5%)5.761s (~)0.242s (-2.7%)6.436s (-0.6%)2.344s101.00x
▲ VercelNext.js (Turbopack)4.186s (-6.5% 🟢)6.079s (-0.6%)0.220s (+9.9% 🔺)6.720s (~)2.534s91.02x
▲ VercelNitro4.499s (+6.2% 🔺)5.768s (+2.8%)0.457s (+71.8% 🔺)6.725s (+5.7% 🔺)2.226s91.10x

🔍 Observability: Express | Next.js (Turbopack) | Nitro

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)0.900s (+1.8%)1.035s (+3.5%)0.000s (+106.9% 🔺)1.039s (+3.4%)0.139s581.00x
🐘 PostgresExpress0.931s (-3.9%)1.146s (-5.9% 🟢)0.000s (+172.2% 🔺)1.159s (-6.0% 🟢)0.228s541.03x
🐘 PostgresNitro0.946s (~)1.220s (+12.0% 🔺)0.000s (-17.3% 🟢)1.232s (+10.3% 🔺)0.286s491.05x
🐘 PostgresNext.js (Turbopack)0.956s (+3.1%)1.177s (+5.6% 🔺)0.000s (+Infinity% 🔺)1.185s (+4.2%)0.229s511.06x
💻 LocalNitro1.218s (+2.4%)2.021s (~)0.000s (-7.1% 🟢)2.023s (~)0.805s301.35x
💻 LocalNext.js (Turbopack)1.256s (~)2.019s (~)0.000s (-11.1% 🟢)2.022s (~)0.767s301.40x
💻 LocalExpress1.444s (+17.6% 🔺)2.022s (~)0.001s (+14.8% 🔺)2.203s (+8.8% 🔺)0.758s281.60x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.826s (~)4.141s (+4.9%)0.000s (+150.0% 🔺)4.585s (+4.0%)1.759s141.00x
▲ VercelNitro2.860s (+7.4% 🔺)4.133s (+4.8%)0.000s (-50.0% 🟢)4.541s (+3.8%)1.681s141.01x
▲ VercelNext.js (Turbopack)3.188s (-28.9% 🟢)4.619s (-22.8% 🟢)0.000s (-100.0% 🟢)5.035s (-21.8% 🟢)1.847s121.13x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

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

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)1.728s (-1.9%)2.035s (+1.7%)0.000s (-50.0% 🟢)2.039s (+1.6%)0.311s301.00x
🐘 PostgresExpress1.739s (-1.1%)2.143s (+3.8%)0.000s (-65.5% 🟢)2.152s (+3.4%)0.413s281.01x
🐘 PostgresNitro1.789s (+2.3%)2.102s (~)0.000s (+Infinity% 🔺)2.113s (-1.0%)0.324s291.04x
🐘 PostgresNext.js (Turbopack)1.958s (+6.4% 🔺)2.147s (+0.6%)0.000s (+Infinity% 🔺)2.155s (~)0.197s281.13x
💻 LocalNitro3.410s (+6.4% 🔺)4.034s (+1.7%)0.001s (+140.0% 🔺)4.037s (+1.7%)0.627s151.97x
💻 LocalNext.js (Turbopack)3.725s (+3.2%)4.233s (+1.6%)0.000s (-12.5% 🟢)4.236s (+1.6%)0.511s152.16x
💻 LocalExpress3.802s (+9.2% 🔺)4.368s (+8.3% 🔺)0.001s (+233.3% 🔺)4.372s (+8.3% 🔺)0.570s152.20x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.969s (-25.3% 🟢)5.072s (-23.7% 🟢)0.000s (-62.5% 🟢)5.489s (-22.2% 🟢)1.521s121.00x
▲ VercelNext.js (Turbopack)3.988s5.498s0.000s5.941s1.953s111.01x
▲ VercelExpress5.598s (+32.4% 🔺)7.223s (+32.7% 🔺)0.000s (-31.3% 🟢)7.670s (+30.2% 🔺)2.072s81.41x

🔍 Observability: Nitro | Next.js (Turbopack) | Express

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNext.js (Turbopack)10/21
🐘 PostgresExpress19/21
▲ VercelExpress12/21
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express🐘 Postgres17/21
Next.js (Turbopack)🌐 Redis10/21
Nitro🐘 Postgres15/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)

📋 View full workflow run

CopilotAI 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.

Pull request overview

Adds a workflow-mode optimization to the SWC plugin so that after "use step" class members are stripped, any now-unreferenced private class members are also removed, improving downstream tree-shaking (e.g., dropping Node-only imports kept alive by unused helpers).

Changes:

  • Implement iterative private-member reachability analysis and removal after step-member stripping (class decls + class exprs).
  • Add a new transform fixture validating cascading elimination of TypeScript private members in workflow mode.
  • Document the new optimization and publish a patch changeset.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 5 comments.

Show a summary per file
FileDescription
packages/swc-plugin-workflow/transform/src/lib.rsAdds private-member reference collection + DCE pass after "use step" stripping.
packages/swc-plugin-workflow/transform/tests/fixture/private-member-dce/input.tsNew fixture input covering cascading TS private elimination.
packages/swc-plugin-workflow/transform/tests/fixture/private-member-dce/output-workflow.jsExpected workflow output with TS-private members removed and imports dropped.
packages/swc-plugin-workflow/transform/tests/fixture/private-member-dce/output-step.jsExpected step-mode output (private members retained).
packages/swc-plugin-workflow/transform/tests/fixture/private-member-dce/output-client.jsExpected client-mode output (private members retained).
packages/swc-plugin-workflow/spec.mdDocuments the private-member DCE behavior and example.
.changeset/private-member-dce.mdDeclares a patch release for the SWC plugin change.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +619 to +624
// TS private or any member: `this.foo` — only track when
// the object is `this` (to avoid false positives from
// unrelated member accesses like `obj.foo`)
MemberProp::Ident(ident) => {
if matches!(&*expr.obj, Expr::This(_)) {
self.referenced.insert(ident.sym.to_string());

CopilotAIApr 9, 2026

Copy link

Choose a reason for hiding this comment

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

ClassMemberRefCollector only records MemberProp::Ident accesses when the object is this. In TypeScript, private members are also legally accessed via other instances of the same class (e.g. static compare(a: C, b: C) { return a.x - b.x }). With the current check, those references won’t be collected and the private member can be incorrectly removed, breaking runtime behavior. Consider treating any .ident access as a reference when ident matches a known TS-private member name in the class, regardless of the receiver expression (safe, though more conservative).

Suggested change
// TS private or any member: `this.foo` — only track when
// the object is `this` (to avoid false positives from
// unrelated member accesses like `obj.foo`)
MemberProp::Ident(ident) => {
ifmatches!(&*expr.obj,Expr::This(_)){
self.referenced.insert(ident.sym.to_string());
// TS private or any member access. Track `this.foo` as before,
// and also track `obj.foo` when `foo` is a known TS-private
// member of the current class so same-class accesses like
// `a.x` / `b.x` are not missed.
MemberProp::Ident(ident) => {
let name = ident.sym.to_string();
ifmatches!(&*expr.obj,Expr::This(_)) || self.private_names.contains(&name){
self.referenced.insert(name);

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Fixed. The collector now holds a ts_private_names: HashSet<String> built from the class body. MemberProp::Ident accesses are tracked when the name is in that set, regardless of the receiver expression. This catches a.x / b.x patterns in static methods.

Comment on lines +629 to +630
// Continue visiting children
expr.obj.visit_with(self);

CopilotAIApr 9, 2026

Copy link

Choose a reason for hiding this comment

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

visit_member_expr overrides default traversal but only visits expr.obj. For computed member expressions (obj[expr]), the property expression is not visited, so any this.#x / this.x references inside the computed key won’t be collected and could cause incorrectly eliminating private members. Use expr.visit_children_with(self) (or explicitly visit the computed property expression) to preserve full traversal.

Suggested change
// Continue visiting children
expr.obj.visit_with(self);
// Continue visiting children, including computed property expressions.
expr.visit_children_with(self);

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Fixed. Changed to expr.visit_children_with(self) which visits both the object and property expressions, including computed keys.

Comment on lines +614 to +625
match &expr.prop {
// Native JS private: `this.#foo`
MemberProp::PrivateName(name) => {
self.referenced.insert(name.name.to_string());
}
// TS private or any member: `this.foo` — only track when
// the object is `this` (to avoid false positives from
// unrelated member accesses like `obj.foo`)
MemberProp::Ident(ident) => {
if matches!(&*expr.obj, Expr::This(_)) {
self.referenced.insert(ident.sym.to_string());
}

CopilotAIApr 9, 2026

Copy link

Choose a reason for hiding this comment

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

Native private #foo and public/TS member foo are both stored as the same string ("foo"). If a class contains both foo and #foo, a this.foo access will incorrectly mark #foo as referenced, preventing intended elimination. Consider encoding JS-private names distinctly (e.g. prefix with # or store an enum) so #foo and foo don’t collide.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Fixed. JS native private names are now stored with a # prefix (e.g. "#foo") while TS private names remain unprefixed ("foo"). private_member_name() returns the prefixed form for PrivateMethod/PrivateProp, and visit_member_expr inserts format!("#{}", name) for MemberProp::PrivateName.

Comment on lines +1 to +25
import { WORKFLOW_SERIALIZE, WORKFLOW_DESERIALIZE } from '@workflow/serde';
import { getWorld } from './world.js';
import { importKey } from './encryption.js';

export class Run {
static [WORKFLOW_SERIALIZE](instance: Run) {
return { id: instance.id };
}

static [WORKFLOW_DESERIALIZE](data: { id: string }) {
return new Run(data.id);
}

id: string;

// TS private field — only referenced by stripped methods
private encryptionKeyPromise: Promise<any> | null = null;

// TS private method — only called by stripped getters/methods
private async getEncryptionKey(): Promise<any> {
if (!this.encryptionKeyPromise) {
this.encryptionKeyPromise = importKey(this.id);
}
return this.encryptionKeyPromise;
}

CopilotAIApr 9, 2026

Copy link

Choose a reason for hiding this comment

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

The new DCE logic claims to support native JS private members (#field/#method()), but the fixture only covers TypeScript private. Adding a fixture that uses #private syntax (including a cascading case) would help prevent regressions in the JS-private path.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Added in the second commit: private-member-dce-native/input.js tests JS native #field and #method() DCE including cascading elimination (#encryptionKeyPromise eliminated because #getEncryptionKey is eliminated) and survival of referenced members (#label kept because toString() references it).

Comment on lines +8504 to +8510
// Dead-code-eliminate unreferenced private members
// (same logic as visit_mut_class_decl above)
let referenced =
ClassMemberRefCollector::collect_from_class_body(&class_expr.class.body);
class_expr.class.body.retain(|member| match member {
ClassMember::PrivateMethod(m) => referenced.contains(&m.key.name.to_string()),
ClassMember::PrivateProp(p) => referenced.contains(&p.key.name.to_string()),

CopilotAIApr 9, 2026

Copy link

Choose a reason for hiding this comment

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

The private-member retention filter is duplicated in both visit_mut_class_decl and visit_mut_class_expr. This duplication increases the risk of future divergence/bugs when the logic changes. Consider extracting the retain logic into a shared helper (e.g. fn retain_referenced_private_members(body: &mut Vec<ClassMember>)) used by both visitors.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Fixed. Extracted retain_referenced_private_members(body: &mut Vec<ClassMember>) on ClassMemberRefCollector, called from both visit_mut_class_decl and visit_mut_class_expr.

- Namespace JS native private names with # prefix to avoid collisions
with TS private members of the same name
- Track TS private member accesses on non-this receivers (e.g. a.x in
static methods) by maintaining a set of known TS-private names
- Use visit_children_with for full traversal including computed member
expressions
- Extract retain logic into shared retain_referenced_private_members()
helper used by both visit_mut_class_decl and visit_mut_class_expr

@VaguelySeriousVaguelySerious left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

AI review: no blocking issues

// accesses like `a.x` / `b.x` are not missed.
MemberProp::Ident(ident) => {
let name = ident.sym.to_string();
if matches!(&*expr.obj, Expr::This(_)) || self.ts_private_names.contains(&name) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

AI Review: Note

For MemberProp::Ident, any this.foo access records foo in referenced, not only TS-private members. Retain still keys off private_member_name, so this is mostly extra churn in the set rather than incorrect retention, but it is slightly less precise than filtering to private names only.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Correct - the collector is conservative: it records all this.foo accesses, not just those matching known private names. This is intentional for simplicity. The extra entries in the referenced set are harmless since retain only removes members that private_member_name() identifies as private. Public members are always kept regardless of what's in the set.

});
```

This optimization is critical for SDK classes like `Run` where private helper methods reference Node.js-only imports (encryption, world access, etc.) — eliminating them allows the downstream module-level DCE to also remove those imports from the workflow bundle.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

AI Review: Nit

This section explains the algorithm well. If you want to reduce future issue reports, a short explicit note on unsupported patterns (for example computed this[...] access to private fields) could help set expectations.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Good suggestion. Computed access like this[name] where name happens to be a private member name won't be tracked, which could lead to incorrect elimination. In practice this pattern is very rare for private members (especially in SDK code like Run), but worth noting if we want to document limitations.

@TooTallNate
TooTallNate enabled auto-merge (squash) April 9, 2026 19:22
@TooTallNate
TooTallNate merged commit 66585fd into mainApr 9, 2026
102 of 103 checks passed
@TooTallNate
TooTallNate deleted the fix/swc-private-member-dce branch April 9, 2026 19:27
@ghostghost mentioned this pull request Apr 9, 2026
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

@TooTallNate@VaguelySerious