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
Copy link
Copy Markdown
Member

@TooTallNate TooTallNate commented Apr 9, 2026

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:

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

  id: string;
  private encryptionKeyPromise: Promise<any> | null = null;

  private async getEncryptionKey() {
    if (!this.encryptionKeyPromise) {
      this.encryptionKeyPromise = importKey(this.id);
    }
    return this.encryptionKeyPromise;
  }

  constructor(id: string) { this.id = id; }

  get value(): Promise<any> {
    'use step';
    return this.getEncryptionKey().then(() => getWorld().get(this.id));
  }

  async cancel(): Promise<void> {
    'use step';
    const key = await this.getEncryptionKey();
    await getWorld().cancel(this.id, key);
  }

  toString(): string { return `Run(${this.id})`; }
}

Workflow mode output:

export class Run {
  static [WORKFLOW_SERIALIZE](instance) { return { id: instance.id }; }
  static [WORKFLOW_DESERIALIZE](data) { return new Run(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).
Copilot AI review requested due to automatic review settings April 9, 2026 18:09
@TooTallNate TooTallNate requested a review from a team as a code owner April 9, 2026 18:09
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 9, 2026

🦋 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
Name Type
@workflow/swc-plugin Patch
@workflow/astro Patch
@workflow/builders Patch
@workflow/cli Patch
@workflow/nest Patch
@workflow/next Patch
@workflow/nitro Patch
@workflow/rollup Patch
@workflow/sveltekit Patch
workflow Patch
@workflow/vite Patch
@workflow/vitest Patch
@workflow/world-testing Patch
@workflow/nuxt Patch
@workflow/ai Patch
@workflow/core Patch
@workflow/web-shared Patch

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
Copy link
Copy Markdown
Contributor

vercel Bot commented Apr 9, 2026

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

Project Deployment Actions Updated (UTC)
example-nextjs-workflow-turbopack Ready Ready Preview, Comment Apr 9, 2026 6:30pm
example-nextjs-workflow-webpack Ready Ready Preview, Comment Apr 9, 2026 6:30pm
example-workflow Ready Ready Preview, Comment Apr 9, 2026 6:30pm
workbench-astro-workflow Ready Ready Preview, Comment Apr 9, 2026 6:30pm
workbench-express-workflow Ready Ready Preview, Comment Apr 9, 2026 6:30pm
workbench-fastify-workflow Ready Ready Preview, Comment Apr 9, 2026 6:30pm
workbench-hono-workflow Ready Ready Preview, Comment Apr 9, 2026 6:30pm
workbench-nitro-workflow Ready Ready Preview, Comment Apr 9, 2026 6:30pm
workbench-nuxt-workflow Ready Ready Preview, Comment Apr 9, 2026 6:30pm
workbench-sveltekit-workflow Ready Ready Preview, Comment Apr 9, 2026 6:30pm
workbench-vite-workflow Ready Ready Preview, Comment Apr 9, 2026 6:30pm
workflow-docs Ready Ready Preview, Comment, Open in v0 Apr 9, 2026 6:30pm
workflow-swc-playground Ready Ready Preview, Comment Apr 9, 2026 6:30pm

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 9, 2026

🧪 E2E Test Results

Some tests failed

Summary

Passed Failed Skipped Total
✅ ▲ Vercel Production 912 0 67 979
✅ 💻 Local Development 886 0 182 1068
✅ 📦 Local Production 886 0 182 1068
✅ 🐘 Local Postgres 886 0 182 1068
✅ 🪟 Windows 81 0 8 89
❌ 🌍 Community Worlds 136 65 24 225
✅ 📋 Other 225 0 42 267
Total 4012 65 687 4764

❌ 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
App Passed Failed Skipped
✅ astro 82 0 7
✅ example 82 0 7
✅ express 82 0 7
✅ fastify 82 0 7
✅ hono 82 0 7
✅ nextjs-turbopack 87 0 2
✅ nextjs-webpack 87 0 2
✅ nitro 82 0 7
✅ nuxt 82 0 7
✅ sveltekit 82 0 7
✅ vite 82 0 7
✅ 💻 Local Development
App Passed Failed Skipped
✅ astro-stable 75 0 14
✅ express-stable 75 0 14
✅ fastify-stable 75 0 14
✅ hono-stable 75 0 14
✅ nextjs-turbopack-canary 62 0 27
✅ nextjs-turbopack-stable 81 0 8
✅ nextjs-webpack-canary 62 0 27
✅ nextjs-webpack-stable 81 0 8
✅ nitro-stable 75 0 14
✅ nuxt-stable 75 0 14
✅ sveltekit-stable 75 0 14
✅ vite-stable 75 0 14
✅ 📦 Local Production
App Passed Failed Skipped
✅ astro-stable 75 0 14
✅ express-stable 75 0 14
✅ fastify-stable 75 0 14
✅ hono-stable 75 0 14
✅ nextjs-turbopack-canary 62 0 27
✅ nextjs-turbopack-stable 81 0 8
✅ nextjs-webpack-canary 62 0 27
✅ nextjs-webpack-stable 81 0 8
✅ nitro-stable 75 0 14
✅ nuxt-stable 75 0 14
✅ sveltekit-stable 75 0 14
✅ vite-stable 75 0 14
✅ 🐘 Local Postgres
App Passed Failed Skipped
✅ astro-stable 75 0 14
✅ express-stable 75 0 14
✅ fastify-stable 75 0 14
✅ hono-stable 75 0 14
✅ nextjs-turbopack-canary 62 0 27
✅ nextjs-turbopack-stable 81 0 8
✅ nextjs-webpack-canary 62 0 27
✅ nextjs-webpack-stable 81 0 8
✅ nitro-stable 75 0 14
✅ nuxt-stable 75 0 14
✅ sveltekit-stable 75 0 14
✅ vite-stable 75 0 14
✅ 🪟 Windows
App Passed Failed Skipped
✅ nextjs-turbopack 81 0 8
❌ 🌍 Community Worlds
App Passed Failed Skipped
✅ mongodb-dev 5 0 0
❌ mongodb 58 4 8
✅ redis-dev 5 0 0
❌ redis 59 3 8
✅ turso-dev 5 0 0
❌ turso 4 58 8
✅ 📋 Other
App Passed Failed Skipped
✅ e2e-local-dev-nest-stable 75 0 14
✅ e2e-local-postgres-nest-stable 75 0 14
✅ e2e-local-prod-nest-stable 75 0 14

📋 View full workflow run

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 9, 2026

📊 Benchmark Results

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

workflow with no steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Express 0.043s (-1.6%) 1.006s (~) 0.962s 10 1.00x
💻 Local Nitro 0.044s (-4.8%) 1.005s (~) 0.961s 10 1.01x
🐘 Postgres Express 0.046s (-22.5% 🟢) 1.011s (~) 0.965s 10 1.07x
💻 Local Next.js (Turbopack) 0.049s (+3.6%) 1.005s (~) 0.957s 10 1.12x
🌐 Redis Next.js (Turbopack) 0.050s (-11.3% 🟢) 1.005s (~) 0.955s 10 1.16x
🐘 Postgres Next.js (Turbopack) 0.058s (-3.0%) 1.011s (~) 0.954s 10 1.33x
🐘 Postgres Nitro 0.063s (+2.3%) 1.012s (~) 0.949s 10 1.46x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 0.231s (-39.8% 🟢) 2.203s (-10.5% 🟢) 1.972s 10 1.00x
▲ Vercel Next.js (Turbopack) 0.267s (-33.8% 🟢) 2.195s (-8.4% 🟢) 1.928s 10 1.16x
▲ Vercel Nitro 0.304s (+10.2% 🔺) 2.211s (+4.0%) 1.907s 10 1.32x

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

workflow with 1 step

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 1.119s (-3.1%) 2.010s (~) 0.891s 10 1.00x
🌐 Redis Next.js (Turbopack) 1.126s (~) 2.007s (~) 0.881s 10 1.01x
💻 Local Next.js (Turbopack) 1.126s (~) 2.006s (~) 0.880s 10 1.01x
💻 Local Nitro 1.128s (~) 2.005s (~) 0.877s 10 1.01x
💻 Local Express 1.131s (~) 2.006s (~) 0.875s 10 1.01x
🐘 Postgres Next.js (Turbopack) 1.147s (~) 2.009s (~) 0.862s 10 1.02x
🐘 Postgres Nitro 1.147s (-0.6%) 2.011s (~) 0.864s 10 1.02x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 1.911s (+1.0%) 3.886s (+0.8%) 1.975s 10 1.00x
▲ Vercel Nitro 2.003s (+6.5% 🔺) 3.356s (-10.3% 🟢) 1.353s 10 1.05x
▲ Vercel Next.js (Turbopack) 2.103s (+4.0%) 3.881s (+3.0%) 1.778s 10 1.10x

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

workflow with 10 sequential steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 10.694s (-2.1%) 11.028s (~) 0.334s 3 1.00x
🌐 Redis Next.js (Turbopack) 10.779s (+0.7%) 11.023s (~) 0.244s 3 1.01x
💻 Local Next.js (Turbopack) 10.831s (~) 11.025s (~) 0.194s 3 1.01x
🐘 Postgres Next.js (Turbopack) 10.849s (~) 11.023s (~) 0.174s 3 1.01x
💻 Local Nitro 10.920s (~) 11.023s (~) 0.103s 3 1.02x
🐘 Postgres Nitro 10.934s (~) 11.023s (~) 0.088s 3 1.02x
💻 Local Express 10.952s (~) 11.024s (~) 0.072s 3 1.02x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 16.776s (~) 18.801s (-1.4%) 2.025s 2 1.00x
▲ Vercel Next.js (Turbopack) 17.133s (-0.8%) 19.157s (~) 2.024s 2 1.02x
▲ Vercel Nitro 17.627s (-1.4%) 18.953s (-5.7% 🟢) 1.326s 2 1.05x

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

workflow with 25 sequential steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 14.019s (-3.8%) 14.627s (-2.6%) 0.608s 5 1.00x
🌐 Redis Next.js (Turbopack) 14.257s (+0.9%) 15.029s (~) 0.772s 4 1.02x
🐘 Postgres Next.js (Turbopack) 14.412s (-0.6%) 15.023s (~) 0.612s 4 1.03x
🐘 Postgres Nitro 14.702s (~) 15.021s (~) 0.319s 4 1.05x
💻 Local Next.js (Turbopack) 14.709s (+1.0%) 15.030s (~) 0.322s 4 1.05x
💻 Local Nitro 14.924s (-0.6%) 15.028s (-3.2%) 0.104s 4 1.06x
💻 Local Express 14.989s (~) 15.030s (~) 0.041s 4 1.07x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 33.420s (-3.4%) 35.676s (-3.4%) 2.256s 2 1.00x
▲ Vercel Next.js (Turbopack) 34.176s (-3.3%) 36.300s (-3.0%) 2.124s 2 1.02x
▲ Vercel Nitro 34.324s (+0.8%) 35.857s (-1.2%) 1.533s 2 1.03x

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

workflow with 50 sequential steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 12.957s (-8.6% 🟢) 13.021s (-13.3% 🟢) 0.064s 7 1.00x
🌐 Redis Next.js (Turbopack) 13.389s (+1.2%) 14.025s (~) 0.636s 7 1.03x
🐘 Postgres Next.js (Turbopack) 13.757s (~) 14.019s (~) 0.262s 7 1.06x
🐘 Postgres Nitro 14.192s (-0.9%) 15.022s (~) 0.829s 6 1.10x
💻 Local Next.js (Turbopack) 16.033s (+2.8%) 16.531s (+3.1%) 0.498s 6 1.24x
💻 Local Express 16.441s (-0.7%) 17.032s (~) 0.591s 6 1.27x
💻 Local Nitro 16.531s (~) 17.029s (~) 0.498s 6 1.28x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 56.887s (-2.0%) 58.539s (-2.7%) 1.652s 2 1.00x
▲ Vercel Express 59.072s (+7.5% 🔺) 61.603s (+7.7% 🔺) 2.531s 2 1.04x
▲ Vercel Nitro 59.188s (+2.0%) 60.723s (~) 1.535s 2 1.04x

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

Promise.all with 10 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 1.211s (-8.6% 🟢) 2.009s (~) 0.798s 15 1.00x
🐘 Postgres Next.js (Turbopack) 1.232s (~) 2.009s (~) 0.777s 15 1.02x
🐘 Postgres Nitro 1.260s (~) 2.009s (~) 0.749s 15 1.04x
🌐 Redis Next.js (Turbopack) 1.318s (+6.2% 🔺) 2.006s (~) 0.688s 15 1.09x
💻 Local Express 1.515s (+0.8%) 2.006s (~) 0.491s 15 1.25x
💻 Local Nitro 1.516s (+2.1%) 2.005s (~) 0.490s 15 1.25x
💻 Local Next.js (Turbopack) 1.544s (+4.6%) 2.006s (~) 0.461s 15 1.27x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 2.253s (-6.7% 🟢) 4.180s (+4.1%) 1.927s 8 1.00x
▲ Vercel Nitro 2.496s (-15.3% 🟢) 3.670s (-19.6% 🟢) 1.174s 9 1.11x
▲ Vercel Next.js (Turbopack) 2.719s (+23.5% 🔺) 4.105s (+6.9% 🔺) 1.386s 8 1.21x

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

Promise.all with 25 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 2.300s (-2.9%) 3.009s (~) 0.708s 10 1.00x
🐘 Postgres Nitro 2.330s (+0.7%) 3.009s (~) 0.680s 10 1.01x
🐘 Postgres Next.js (Turbopack) 2.385s (-0.7%) 3.009s (~) 0.624s 10 1.04x
🌐 Redis Next.js (Turbopack) 2.564s (+5.3% 🔺) 3.008s (~) 0.444s 10 1.11x
💻 Local Express 2.895s (+1.4%) 3.453s (+3.3%) 0.558s 9 1.26x
💻 Local Nitro 2.991s (+6.5% 🔺) 3.341s (+11.1% 🔺) 0.351s 9 1.30x
💻 Local Next.js (Turbopack) 3.077s (+24.5% 🔺) 3.884s (+29.1% 🔺) 0.808s 8 1.34x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 2.670s (-10.7% 🟢) 4.008s (-14.7% 🟢) 1.338s 8 1.00x
▲ Vercel Next.js (Turbopack) 2.780s (+3.0%) 4.402s (-1.8%) 1.622s 7 1.04x
▲ Vercel Express 2.879s (+4.4%) 4.437s (+2.9%) 1.558s 7 1.08x

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

Promise.all with 50 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 3.383s (-2.3%) 4.011s (~) 0.628s 8 1.00x
🐘 Postgres Nitro 3.472s (-0.5%) 4.011s (~) 0.538s 8 1.03x
🐘 Postgres Next.js (Turbopack) 3.643s (-1.1%) 4.009s (~) 0.367s 8 1.08x
🌐 Redis Next.js (Turbopack) 4.164s (+5.3% 🔺) 5.011s (+16.6% 🔺) 0.847s 6 1.23x
💻 Local Express 7.500s (-8.6% 🟢) 8.021s (-8.5% 🟢) 0.521s 4 2.22x
💻 Local Nitro 8.022s (+1.6%) 8.773s (+6.1% 🔺) 0.752s 4 2.37x
💻 Local Next.js (Turbopack) 8.384s (+47.2% 🔺) 9.019s (+50.0% 🔺) 0.635s 4 2.48x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 3.054s (-1.0%) 5.367s (+9.7% 🔺) 2.313s 6 1.00x
▲ Vercel Nitro 3.295s (+8.8% 🔺) 4.508s (-7.1% 🟢) 1.213s 7 1.08x
▲ Vercel Next.js (Turbopack) 3.678s (+1.9%) 5.769s (+8.0% 🔺) 2.091s 6 1.20x

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

Promise.race with 10 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 1.204s (-4.3%) 2.008s (~) 0.804s 15 1.00x
🐘 Postgres Next.js (Turbopack) 1.251s (+2.1%) 2.009s (~) 0.758s 15 1.04x
🐘 Postgres Nitro 1.264s (~) 2.008s (~) 0.744s 15 1.05x
🌐 Redis Next.js (Turbopack) 1.278s (-0.6%) 2.006s (~) 0.729s 15 1.06x
💻 Local Express 1.519s (~) 2.006s (~) 0.487s 15 1.26x
💻 Local Nitro 1.524s (-3.4%) 2.006s (~) 0.482s 15 1.27x
💻 Local Next.js (Turbopack) 1.556s (~) 2.007s (-3.2%) 0.451s 15 1.29x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 2.003s (-4.2%) 3.908s (-1.8%) 1.905s 8 1.00x
▲ Vercel Nitro 2.006s (-7.6% 🟢) 3.636s (-6.7% 🟢) 1.630s 9 1.00x
▲ Vercel Next.js (Turbopack) 2.217s (+6.1% 🔺) 3.745s (+5.0% 🔺) 1.529s 9 1.11x

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

Promise.race with 25 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 2.329s (~) 3.010s (~) 0.681s 10 1.00x
🐘 Postgres Express 2.332s (-0.9%) 3.008s (~) 0.676s 10 1.00x
🐘 Postgres Next.js (Turbopack) 2.390s (~) 3.011s (~) 0.620s 10 1.03x
🌐 Redis Next.js (Turbopack) 2.543s (+3.6%) 3.007s (~) 0.464s 10 1.09x
💻 Local Next.js (Turbopack) 2.807s (-10.8% 🟢) 3.341s (-6.3% 🟢) 0.533s 9 1.21x
💻 Local Express 2.938s (-0.9%) 3.454s (-3.1%) 0.516s 9 1.26x
💻 Local Nitro 2.990s (-1.3%) 3.677s (-2.2%) 0.687s 9 1.28x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 2.320s (~) 4.314s (+9.6% 🔺) 1.994s 8 1.00x
▲ Vercel Nitro 2.485s (-3.3%) 3.963s (-5.7% 🟢) 1.478s 8 1.07x
▲ Vercel Next.js (Turbopack) 2.779s (+9.3% 🔺) 4.314s (+3.7%) 1.535s 7 1.20x

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

Promise.race with 50 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 3.453s (~) 4.010s (~) 0.557s 8 1.00x
🐘 Postgres Express 3.457s (~) 4.009s (~) 0.553s 8 1.00x
🐘 Postgres Next.js (Turbopack) 3.646s (~) 4.009s (~) 0.363s 8 1.06x
🌐 Redis Next.js (Turbopack) 4.169s (+3.5%) 5.012s (+9.4% 🔺) 0.843s 6 1.21x
💻 Local Express 8.033s (-6.6% 🟢) 8.774s (-2.8%) 0.741s 4 2.33x
💻 Local Nitro 8.669s (+2.3%) 9.026s (~) 0.357s 4 2.51x
💻 Local Next.js (Turbopack) 8.695s (+12.2% 🔺) 9.271s (+12.1% 🔺) 0.576s 4 2.52x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 2.728s (-4.1%) 4.288s (-0.9%) 1.560s 7 1.00x
▲ Vercel Express 3.170s (+16.4% 🔺) 4.654s (+9.8% 🔺) 1.484s 7 1.16x
▲ Vercel Next.js (Turbopack) 3.271s (-13.0% 🟢) 5.128s (-7.0% 🟢) 1.857s 6 1.20x

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

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 0.608s (-26.8% 🟢) 1.006s (~) 0.399s 60 1.00x
🌐 Redis Next.js (Turbopack) 0.708s (+11.4% 🔺) 1.004s (~) 0.296s 60 1.16x
🐘 Postgres Next.js (Turbopack) 0.819s (+6.2% 🔺) 1.041s (+1.7%) 0.222s 58 1.35x
🐘 Postgres Nitro 0.858s (+2.2%) 1.023s (+1.7%) 0.165s 59 1.41x
💻 Local Next.js (Turbopack) 0.860s (-2.7%) 1.005s (-3.3%) 0.145s 60 1.41x
💻 Local Nitro 0.980s (-5.4% 🟢) 1.115s (-44.4% 🟢) 0.135s 54 1.61x
💻 Local Express 1.016s (+4.1%) 1.654s (+48.3% 🔺) 0.638s 37 1.67x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 8.903s (-8.6% 🟢) 10.501s (-8.5% 🟢) 1.599s 6 1.00x
▲ Vercel Next.js (Turbopack) 9.632s (-23.0% 🟢) 11.579s (-17.7% 🟢) 1.948s 6 1.08x
▲ Vercel Express 10.403s (+2.7%) 12.297s (+1.4%) 1.893s 5 1.17x

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

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 1.477s (-26.2% 🟢) 2.007s (-16.4% 🟢) 0.530s 45 1.00x
🌐 Redis Next.js (Turbopack) 1.672s (+15.4% 🔺) 2.006s (~) 0.333s 45 1.13x
🐘 Postgres Nitro 2.078s (+3.4%) 2.944s (+16.1% 🔺) 0.866s 31 1.41x
🐘 Postgres Next.js (Turbopack) 2.153s (+14.8% 🔺) 2.821s (+37.5% 🔺) 0.668s 32 1.46x
💻 Local Next.js (Turbopack) 2.700s (~) 3.008s (~) 0.308s 30 1.83x
💻 Local Nitro 3.022s (-1.1%) 3.547s (-9.6% 🟢) 0.525s 26 2.05x
💻 Local Express 3.162s (+3.6%) 3.967s (+4.3%) 0.805s 23 2.14x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 28.495s (-3.9%) 30.408s (-3.5%) 1.913s 3 1.00x
▲ Vercel Express 28.833s (-1.1%) 30.922s (-1.2%) 2.089s 3 1.01x
▲ Vercel Next.js (Turbopack) 29.626s (-0.8%) 31.356s (-2.6%) 1.730s 3 1.04x

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

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 3.109s (-23.3% 🟢) 3.790s (-20.7% 🟢) 0.681s 32 1.00x
🌐 Redis Next.js (Turbopack) 3.352s (+11.2% 🔺) 4.008s (+15.7% 🔺) 0.656s 30 1.08x
🐘 Postgres Next.js (Turbopack) 3.994s (+5.8% 🔺) 4.331s (+8.0% 🔺) 0.337s 28 1.28x
🐘 Postgres Nitro 4.191s (~) 4.932s (-1.6%) 0.741s 25 1.35x
💻 Local Next.js (Turbopack) 8.691s (+2.0%) 9.019s (~) 0.328s 14 2.80x
💻 Local Express 9.039s (-1.3%) 9.633s (-1.6%) 0.594s 13 2.91x
💻 Local Nitro 9.104s (+2.1%) 9.710s (+3.3%) 0.606s 13 2.93x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 73.513s (-14.6% 🟢) 75.368s (-14.2% 🟢) 1.855s 2 1.00x
▲ Vercel Nitro 75.626s (+6.1% 🔺) 77.847s (+6.5% 🔺) 2.221s 2 1.03x
▲ Vercel Next.js (Turbopack) 77.885s (-4.4%) 79.559s (-4.2%) 1.675s 2 1.06x

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

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 0.226s (-18.4% 🟢) 1.007s (~) 0.781s 60 1.00x
🐘 Postgres Next.js (Turbopack) 0.251s (+4.8%) 1.007s (~) 0.757s 60 1.11x
🐘 Postgres Nitro 0.281s (-3.7%) 1.007s (~) 0.725s 60 1.24x
🌐 Redis Next.js (Turbopack) 0.284s (+7.7% 🔺) 1.004s (~) 0.720s 60 1.26x
💻 Local Nitro 0.588s (+4.9%) 1.004s (~) 0.417s 60 2.60x
💻 Local Next.js (Turbopack) 0.620s (+20.2% 🔺) 1.022s (+1.7%) 0.402s 59 2.74x
💻 Local Express 0.697s (+18.7% 🔺) 1.096s (+9.1% 🔺) 0.398s 55 3.08x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 1.544s (-22.1% 🟢) 3.163s (-11.6% 🟢) 1.620s 20 1.00x
▲ Vercel Next.js (Turbopack) 1.688s (-5.7% 🟢) 3.573s (+3.3%) 1.886s 18 1.09x
▲ Vercel Nitro 1.727s (-8.8% 🟢) 3.318s (-4.5%) 1.591s 19 1.12x

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

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 0.363s (-27.3% 🟢) 1.006s (~) 0.643s 90 1.00x
🐘 Postgres Nitro 0.493s (-1.8%) 1.007s (~) 0.514s 90 1.36x
🐘 Postgres Next.js (Turbopack) 0.508s (+4.6%) 1.007s (~) 0.499s 90 1.40x
🌐 Redis Next.js (Turbopack) 1.144s (+2.9%) 2.006s (+17.7% 🔺) 0.862s 45 3.15x
💻 Local Express 2.492s (-1.6%) 3.009s (~) 0.517s 30 6.86x
💻 Local Nitro 2.652s (+14.2% 🔺) 3.180s (+5.7% 🔺) 0.528s 29 7.31x
💻 Local Next.js (Turbopack) 2.676s (+7.7% 🔺) 3.076s (+2.2%) 0.401s 30 7.37x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 2.612s (-2.1%) 4.132s (-6.5% 🟢) 1.519s 22 1.00x
▲ Vercel Express 2.738s (-7.0% 🟢) 4.568s (-3.6%) 1.830s 20 1.05x
▲ Vercel Next.js (Turbopack) 3.366s (-5.4% 🟢) 4.890s (-7.9% 🟢) 1.524s 19 1.29x

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

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 0.583s (-26.2% 🟢) 1.007s (~) 0.423s 120 1.00x
🐘 Postgres Nitro 0.809s (-0.9%) 1.009s (~) 0.200s 119 1.39x
🐘 Postgres Next.js (Turbopack) 0.847s (+10.4% 🔺) 1.043s (+3.6%) 0.196s 116 1.45x
🌐 Redis Next.js (Turbopack) 2.677s (-3.5%) 3.007s (-0.8%) 0.330s 40 4.59x
💻 Local Nitro 10.890s (+4.7%) 11.485s (+4.1%) 0.595s 11 18.67x
💻 Local Express 10.935s (~) 11.395s (-1.5%) 0.460s 11 18.75x
💻 Local Next.js (Turbopack) 11.065s (+7.4% 🔺) 11.572s (+7.4% 🔺) 0.507s 11 18.97x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 6.908s (+9.6% 🔺) 8.327s (+3.7%) 1.419s 15 1.00x
▲ Vercel Next.js (Turbopack) 47.028s (+673.5% 🔺) 48.733s (+526.0% 🔺) 1.705s 6 6.81x
▲ Vercel Express 49.426s (+616.1% 🔺) 51.745s (+489.1% 🔺) 2.320s 7 7.15x

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

Stream Benchmarks (includes TTFB metrics)
workflow with stream

💻 Local Development

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 0.158s (-25.4% 🟢) 1.001s (+0.5%) 0.001s (-26.7% 🟢) 1.009s (~) 0.851s 10 1.00x
💻 Local Next.js (Turbopack) 0.170s (+0.5%) 1.002s (~) 0.012s (+14.7% 🔺) 1.017s (~) 0.847s 10 1.07x
🐘 Postgres Next.js (Turbopack) 0.192s (+0.8%) 1.001s (~) 0.001s (-15.4% 🟢) 1.010s (~) 0.818s 10 1.21x
💻 Local Nitro 0.207s (-0.7%) 1.004s (~) 0.012s (+11.5% 🔺) 1.017s (~) 0.810s 10 1.31x
💻 Local Express 0.219s (+6.8% 🔺) 1.004s (~) 0.011s (-8.5% 🟢) 1.017s (~) 0.798s 10 1.38x
🐘 Postgres Nitro 0.219s (+2.0%) 0.996s (~) 0.001s (+30.0% 🔺) 1.012s (~) 0.792s 10 1.39x
🌐 Redis Next.js (Turbopack) 0.242s (+62.9% 🔺) 1.001s (~) 0.032s (+1776.5% 🔺) 1.037s (+3.0%) 0.796s 10 1.53x

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 1.493s (-11.9% 🟢) 2.833s (-11.4% 🟢) 0.461s (-51.8% 🟢) 3.696s (-20.0% 🟢) 2.203s 10 1.00x
▲ Vercel Express 1.565s (+3.1%) 2.942s (+1.1%) 0.642s (-16.1% 🟢) 4.033s (-2.6%) 2.468s 10 1.05x
▲ Vercel Nitro 1.603s (-5.5% 🟢) 2.899s (-11.0% 🟢) 0.562s (+7.0% 🔺) 3.812s (-9.8% 🟢) 2.208s 10 1.07x

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

stream pipeline with 5 transform steps (1MB)

💻 Local Development

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
🌐 Redis 🥇 Next.js (Turbopack) 0.481s (+14.5% 🔺) 1.002s (~) 0.003s (+4.8%) 1.011s (~) 0.530s 60 1.00x
🐘 Postgres Express 0.522s (-16.5% 🟢) 1.006s (~) 0.004s (-10.2% 🟢) 1.022s (~) 0.500s 59 1.09x
🐘 Postgres Nitro 0.629s (-2.0%) 1.005s (~) 0.004s (-11.2% 🟢) 1.022s (~) 0.393s 59 1.31x
🐘 Postgres Next.js (Turbopack) 0.659s (+9.3% 🔺) 1.026s (+1.7%) 0.008s (+110.1% 🔺) 1.044s (+2.2%) 0.385s 58 1.37x
💻 Local Next.js (Turbopack) 0.763s (+13.8% 🔺) 1.011s (~) 0.009s (-15.6% 🟢) 1.116s (+8.9% 🔺) 0.353s 54 1.59x
💻 Local Nitro 0.815s (-14.6% 🟢) 1.012s (~) 0.010s (+2.4%) 1.111s (-9.5% 🟢) 0.296s 57 1.70x
💻 Local Express 0.847s (+18.5% 🔺) 1.010s (~) 0.010s (+6.7% 🔺) 1.115s (+8.9% 🔺) 0.268s 54 1.76x

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 4.092s (+1.5%) 5.761s (~) 0.242s (-2.7%) 6.436s (-0.6%) 2.344s 10 1.00x
▲ Vercel Next.js (Turbopack) 4.186s (-6.5% 🟢) 6.079s (-0.6%) 0.220s (+9.9% 🔺) 6.720s (~) 2.534s 9 1.02x
▲ Vercel Nitro 4.499s (+6.2% 🔺) 5.768s (+2.8%) 0.457s (+71.8% 🔺) 6.725s (+5.7% 🔺) 2.226s 9 1.10x

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

10 parallel streams (1MB each)

💻 Local Development

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
🌐 Redis 🥇 Next.js (Turbopack) 0.900s (+1.8%) 1.035s (+3.5%) 0.000s (+106.9% 🔺) 1.039s (+3.4%) 0.139s 58 1.00x
🐘 Postgres Express 0.931s (-3.9%) 1.146s (-5.9% 🟢) 0.000s (+172.2% 🔺) 1.159s (-6.0% 🟢) 0.228s 54 1.03x
🐘 Postgres Nitro 0.946s (~) 1.220s (+12.0% 🔺) 0.000s (-17.3% 🟢) 1.232s (+10.3% 🔺) 0.286s 49 1.05x
🐘 Postgres Next.js (Turbopack) 0.956s (+3.1%) 1.177s (+5.6% 🔺) 0.000s (+Infinity% 🔺) 1.185s (+4.2%) 0.229s 51 1.06x
💻 Local Nitro 1.218s (+2.4%) 2.021s (~) 0.000s (-7.1% 🟢) 2.023s (~) 0.805s 30 1.35x
💻 Local Next.js (Turbopack) 1.256s (~) 2.019s (~) 0.000s (-11.1% 🟢) 2.022s (~) 0.767s 30 1.40x
💻 Local Express 1.444s (+17.6% 🔺) 2.022s (~) 0.001s (+14.8% 🔺) 2.203s (+8.8% 🔺) 0.758s 28 1.60x

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 2.826s (~) 4.141s (+4.9%) 0.000s (+150.0% 🔺) 4.585s (+4.0%) 1.759s 14 1.00x
▲ Vercel Nitro 2.860s (+7.4% 🔺) 4.133s (+4.8%) 0.000s (-50.0% 🟢) 4.541s (+3.8%) 1.681s 14 1.01x
▲ Vercel Next.js (Turbopack) 3.188s (-28.9% 🟢) 4.619s (-22.8% 🟢) 0.000s (-100.0% 🟢) 5.035s (-21.8% 🟢) 1.847s 12 1.13x

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

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

💻 Local Development

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
🌐 Redis 🥇 Next.js (Turbopack) 1.728s (-1.9%) 2.035s (+1.7%) 0.000s (-50.0% 🟢) 2.039s (+1.6%) 0.311s 30 1.00x
🐘 Postgres Express 1.739s (-1.1%) 2.143s (+3.8%) 0.000s (-65.5% 🟢) 2.152s (+3.4%) 0.413s 28 1.01x
🐘 Postgres Nitro 1.789s (+2.3%) 2.102s (~) 0.000s (+Infinity% 🔺) 2.113s (-1.0%) 0.324s 29 1.04x
🐘 Postgres Next.js (Turbopack) 1.958s (+6.4% 🔺) 2.147s (+0.6%) 0.000s (+Infinity% 🔺) 2.155s (~) 0.197s 28 1.13x
💻 Local Nitro 3.410s (+6.4% 🔺) 4.034s (+1.7%) 0.001s (+140.0% 🔺) 4.037s (+1.7%) 0.627s 15 1.97x
💻 Local Next.js (Turbopack) 3.725s (+3.2%) 4.233s (+1.6%) 0.000s (-12.5% 🟢) 4.236s (+1.6%) 0.511s 15 2.16x
💻 Local Express 3.802s (+9.2% 🔺) 4.368s (+8.3% 🔺) 0.001s (+233.3% 🔺) 4.372s (+8.3% 🔺) 0.570s 15 2.20x

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 3.969s (-25.3% 🟢) 5.072s (-23.7% 🟢) 0.000s (-62.5% 🟢) 5.489s (-22.2% 🟢) 1.521s 12 1.00x
▲ Vercel Next.js (Turbopack) 3.988s 5.498s 0.000s 5.941s 1.953s 11 1.01x
▲ Vercel Express 5.598s (+32.4% 🔺) 7.223s (+32.7% 🔺) 0.000s (-31.3% 🟢) 7.670s (+30.2% 🔺) 2.072s 8 1.41x

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

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World 🥇 Fastest Framework Wins
💻 Local Next.js (Turbopack) 10/21
🐘 Postgres Express 19/21
▲ Vercel Express 12/21
Fastest World by Framework

Winner determined by most benchmark wins

Framework 🥇 Fastest World Wins
Express 🐘 Postgres 17/21
Next.js (Turbopack) 🌐 Redis 10/21
Nitro 🐘 Postgres 15/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

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

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
File Description
packages/swc-plugin-workflow/transform/src/lib.rs Adds private-member reference collection + DCE pass after "use step" stripping.
packages/swc-plugin-workflow/transform/tests/fixture/private-member-dce/input.ts New fixture input covering cascading TS private elimination.
packages/swc-plugin-workflow/transform/tests/fixture/private-member-dce/output-workflow.js Expected workflow output with TS-private members removed and imports dropped.
packages/swc-plugin-workflow/transform/tests/fixture/private-member-dce/output-step.js Expected step-mode output (private members retained).
packages/swc-plugin-workflow/transform/tests/fixture/private-member-dce/output-client.js Expected client-mode output (private members retained).
packages/swc-plugin-workflow/spec.md Documents the private-member DCE behavior and example.
.changeset/private-member-dce.md Declares 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());
Copy link

Copilot AI Apr 9, 2026

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) => {
if matches!(&*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();
if matches!(&*expr.obj, Expr::This(_)) || self.private_names.contains(&name) {
self.referenced.insert(name);

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member Author

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);
Copy link

Copilot AI Apr 9, 2026

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
Member Author

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());
}
Copy link

Copilot AI Apr 9, 2026

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
Member Author

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;
}
Copy link

Copilot AI Apr 9, 2026

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
Member Author

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()),
Copy link

Copilot AI Apr 9, 2026

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
Member Author

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
Copy link
Copy Markdown
Member

@VaguelySerious VaguelySerious left a comment

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
Member Author

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
Member Author

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 main Apr 9, 2026
102 of 103 checks passed
@TooTallNate TooTallNate deleted the fix/swc-private-member-dce branch April 9, 2026 19:27
@ghost ghost mentioned this pull request Apr 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants