Skip to content

Extract browser-safe serialization format and move hydration client-side#1015

Merged
TooTallNate merged 7 commits intomainfrom
nate/split-serde-for-browser
Feb 13, 2026
Merged

Extract browser-safe serialization format and move hydration client-side#1015
TooTallNate merged 7 commits intomainfrom
nate/split-serde-for-browser

Conversation

@TooTallNate
Copy link
Member

@TooTallNate TooTallNate commented Feb 12, 2026

Summary

Split the serialization/deserialization logic into environment-specific layers so data hydration can happen client-side in the browser. This is a prerequisite for e2e encryption where decryption keys are only available in the browser.

Architecture

Layer 1: @workflow/core/serialization-format (new, browser-safe)

  • Format prefix encoding/decoding (devl, future encr, etc.)
  • Generic hydrateData() dispatch — handles Uint8Array (v2 binary), legacy arrays (v1), and plain values
  • hydrateResourceIO(resource, revivers) resource-type dispatcher (step/hook/event/workflow field mapping)
  • ClassInstanceRef (plain data class, no node:util dependency)
  • StreamRef, type guards (isStreamRef, isStreamId, isClassInstanceRef), utility functions (extractStreamIds, truncateId)
  • Shared observabilityRevivers for stream/class/step display overrides
  • 36 unit tests covering all of the above

Layer 2: Environment-specific revivers

  • @workflow/web-shared (lib/hydration.ts) — browser-safe revivers using atob() for base64, real URLSearchParams/Headers/URL instances, ClassInstanceRef for UI rendering
  • @workflow/cli (lib/inspect/hydration.ts) — Node.js revivers using Buffer.from() for base64, CLIClassInstanceRef with util.inspect.custom for CLI output

Each module exports a pre-bound hydrateResourceIO(resource) that uses its environment's revivers.

Removed: @workflow/core/observability

  • Deleted observability.ts and observability.test.ts entirely (no remaining consumers)
  • Removed "./observability" export from @workflow/core/package.json
  • Removed the workflow package's internal/observability.ts re-export
  • All functionality has been split between serialization-format.ts (shared types/utilities) and the environment-specific hydration modules

Web package changes

  • Server passes raw world data through without hydration (CBOR preserves Uint8Array)
  • Client calls hydrateResourceIO from @workflow/web-shared after receiving CBOR-decoded data
  • No Vite node:* stubs needed since @workflow/core/serialization-format is browser-safe
  • Optimized: event hydration finds the matching event before hydrating (instead of hydrating all)
  • Reduced server log noise from handled API errors (4xx errors no longer logged)

Packages affected

  • @workflow/core — new serialization-format export (with tests), removed observability export
  • @workflow/web-shared — new lib/hydration.ts with browser-safe revivers
  • @workflow/cli — new lib/inspect/hydration.ts with Node.js revivers, updated output.ts import
  • @workflow/web — client-side hydration, removed server-side hydration
  • workflow — removed internal/observability.ts re-export

@changeset-bot
Copy link

changeset-bot bot commented Feb 12, 2026

🦋 Changeset detected

Latest commit: 7889a91

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

This PR includes changesets to release 15 packages
Name Type
@workflow/core Patch
@workflow/web-shared Patch
@workflow/web Patch
@workflow/cli Patch
@workflow/builders Patch
@workflow/next Patch
@workflow/nitro Patch
workflow Patch
@workflow/world-testing Patch
@workflow/astro Patch
@workflow/nest Patch
@workflow/rollup Patch
@workflow/sveltekit Patch
@workflow/vite Patch
@workflow/nuxt 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

@github-actions
Copy link
Contributor

github-actions bot commented Feb 12, 2026

🧪 E2E Test Results

Some tests failed

Summary

Passed Failed Skipped Total
✅ ▲ Vercel Production 490 0 38 528
✅ 💻 Local Development 418 0 62 480
✅ 📦 Local Production 418 0 62 480
✅ 🐘 Local Postgres 418 0 62 480
✅ 🪟 Windows 45 0 3 48
❌ 🌍 Community Worlds 102 42 9 153
✅ 📋 Other 123 0 21 144
Total 2014 42 257 2313

❌ Failed Tests

🌍 Community Worlds (42 failed)

mongodb (1 failed):

  • webhookWorkflow

turso (41 failed):

  • addTenWorkflow
  • addTenWorkflow
  • should work with react rendering in step
  • promiseAllWorkflow
  • promiseRaceWorkflow
  • promiseAnyWorkflow
  • hookWorkflow
  • webhookWorkflow
  • sleepingWorkflow
  • nullByteWorkflow
  • workflowAndStepMetadataWorkflow
  • fetchWorkflow
  • promiseRaceStressTestWorkflow
  • 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()
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars)
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument
  • closureVariableWorkflow - nested step functions with closure variables
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step
  • health check (queue-based) - workflow and step endpoints respond to health check messages
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly
  • Calculator.calculate - static workflow method using static step methods from another class
  • AllInOneService.processNumber - static workflow method using sibling static step methods
  • ChainableService.processWithThis - static step methods using this to reference the class
  • thisSerializationWorkflow - step function invoked with .call() and .apply()
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE
  • instanceMethodStepWorkflow - instance methods with "use step" directive
  • crossContextSerdeWorkflow - classes defined in step code are deserializable in workflow context
  • stepFunctionAsStartArgWorkflow - step function reference passed as start() argument
  • pages router addTenWorkflow via pages router
  • pages router promiseAllWorkflow via pages router
  • pages router sleepingWorkflow via pages router

Details by Category

✅ ▲ Vercel Production
App Passed Failed Skipped
✅ astro 44 0 4
✅ example 44 0 4
✅ express 44 0 4
✅ fastify 44 0 4
✅ hono 44 0 4
✅ nextjs-turbopack 47 0 1
✅ nextjs-webpack 47 0 1
✅ nitro 44 0 4
✅ nuxt 44 0 4
✅ sveltekit 44 0 4
✅ vite 44 0 4
✅ 💻 Local Development
App Passed Failed Skipped
✅ astro-stable 41 0 7
✅ express-stable 41 0 7
✅ fastify-stable 41 0 7
✅ hono-stable 41 0 7
✅ nextjs-turbopack-stable 45 0 3
✅ nextjs-webpack-stable 45 0 3
✅ nitro-stable 41 0 7
✅ nuxt-stable 41 0 7
✅ sveltekit-stable 41 0 7
✅ vite-stable 41 0 7
✅ 📦 Local Production
App Passed Failed Skipped
✅ astro-stable 41 0 7
✅ express-stable 41 0 7
✅ fastify-stable 41 0 7
✅ hono-stable 41 0 7
✅ nextjs-turbopack-stable 45 0 3
✅ nextjs-webpack-stable 45 0 3
✅ nitro-stable 41 0 7
✅ nuxt-stable 41 0 7
✅ sveltekit-stable 41 0 7
✅ vite-stable 41 0 7
✅ 🐘 Local Postgres
App Passed Failed Skipped
✅ astro-stable 41 0 7
✅ express-stable 41 0 7
✅ fastify-stable 41 0 7
✅ hono-stable 41 0 7
✅ nextjs-turbopack-stable 45 0 3
✅ nextjs-webpack-stable 45 0 3
✅ nitro-stable 41 0 7
✅ nuxt-stable 41 0 7
✅ sveltekit-stable 41 0 7
✅ vite-stable 41 0 7
✅ 🪟 Windows
App Passed Failed Skipped
✅ nextjs-turbopack 45 0 3
❌ 🌍 Community Worlds
App Passed Failed Skipped
✅ mongodb-dev 3 0 0
❌ mongodb 44 1 3
✅ redis-dev 3 0 0
✅ redis 45 0 3
✅ turso-dev 3 0 0
❌ turso 4 41 3
✅ 📋 Other
App Passed Failed Skipped
✅ e2e-local-dev-nest-stable 41 0 7
✅ e2e-local-postgres-nest-stable 41 0 7
✅ e2e-local-prod-nest-stable 41 0 7

📋 View full workflow run

@vercel
Copy link
Contributor

vercel bot commented Feb 12, 2026

Copy link
Member Author

TooTallNate commented Feb 12, 2026

The WorkflowTraceViewer defined DetailPanel as an inline function
component, causing React to remount EntityDetailPanel on every parent
re-render. The customPanelComponent stored in TraceViewerContext was
also only set during initialization and never updated.

Fix by:
- Replace inline DetailPanel component with a JSX element (no remount)
- Add setCustomPanelComponent reducer action and useEffect to update
  the context when the panel element changes
- Remove debug hydration logging

Also fix web revivers for non-structurally-cloneable types:
- URLSearchParams, Headers, URL are converted to plain equivalents
  since the trace viewer's web worker uses postMessage which requires
  structured cloneability
- Fix reviver spread order in CLI and web-shared hydration: spread
  observabilityRevivers first so environment-specific overrides for
  Instance/Class take precedence
- Remove @workflow/core/observability module entirely (no consumers)
- Delete observability.test.ts, port relevant tests to
  serialization-format.test.ts (isStreamRef, isStreamId,
  extractStreamIds, truncateId, ClassInstanceRef JSON roundtrip)
- Optimize event hydration: find matching event before hydrating
  instead of hydrating all events
- Add comprehensive unit tests for serialization-format.ts (36 tests)
Stream chunks were serialized without the 'devl' format prefix,
unlike step inputs/outputs. This meant stream data couldn't go
through the same client-side hydration pipeline.

Core changes:
- getSerializeStream: wrap each chunk with encodeWithFormatPrefix
- getDeserializeStream: handle both format-prefixed (v2) and
  legacy newline-delimited (v1) chunks

Web changes:
- Stream route: CBOR-encode and length-prefix each chunk so
  the client can distinguish chunk boundaries
- useStreamReader: decode length-prefixed CBOR frames, then
  hydrate each chunk using hydrateData with web revivers
@TooTallNate TooTallNate force-pushed the nate/split-serde-for-browser branch from 6878be5 to 515d669 Compare February 13, 2026 00:31
@graphite-app graphite-app bot changed the base branch from graphite-base/1015 to main February 13, 2026 00:32
The global parameter was never passed by any callsite — all three
usages call with no arguments. Use the global constructors directly.
@TooTallNate TooTallNate force-pushed the nate/split-serde-for-browser branch from 515d669 to 7889a91 Compare February 13, 2026 00:32
@github-actions
Copy link
Contributor

github-actions bot commented Feb 13, 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.031s (-5.2% 🟢) 1.004s (~) 0.974s 10 1.00x
💻 Local Nitro 0.034s (-20.7% 🟢) 1.005s (~) 0.971s 10 1.09x
💻 Local Next.js (Turbopack) 0.046s 1.005s 0.960s 10 1.48x
🌐 Redis Next.js (Turbopack) 0.048s (-2.8%) 1.005s (~) 0.957s 10 1.55x
🌐 MongoDB Next.js (Turbopack) 0.093s 1.008s 0.914s 10 3.02x
🐘 Postgres Nitro 0.315s (-24.8% 🟢) 1.016s (+0.6%) 0.701s 10 10.19x
🐘 Postgres Express 0.338s (-6.4% 🟢) 1.009s (-1.2%) 0.672s 10 10.92x
🐘 Postgres Next.js (Turbopack) 0.376s 1.011s 0.634s 10 12.18x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 0.757s (-5.0%) 2.547s (+30.0% 🔺) 1.790s 10 1.00x
▲ Vercel Express 0.765s (+9.6% 🔺) 2.154s (+3.1%) 1.389s 10 1.01x
▲ Vercel Nitro 0.806s (+3.6%) 2.249s (+9.2% 🔺) 1.443s 10 1.06x

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

workflow with 1 step

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Express 1.102s (~) 2.005s (~) 0.903s 10 1.00x
💻 Local Next.js (Turbopack) 1.104s 2.005s 0.901s 10 1.00x
💻 Local Nitro 1.105s (-0.8%) 2.005s (~) 0.900s 10 1.00x
🌐 Redis Next.js (Turbopack) 1.106s (~) 2.006s (~) 0.900s 10 1.00x
🌐 MongoDB Next.js (Turbopack) 1.312s 2.008s 0.696s 10 1.19x
🐘 Postgres Next.js (Turbopack) 1.790s 2.013s 0.224s 10 1.62x
🐘 Postgres Express 2.215s (-5.1% 🟢) 3.014s (~) 0.800s 10 2.01x
🐘 Postgres Nitro 2.352s (-1.7%) 3.015s (~) 0.664s 10 2.13x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 2.720s (+3.7%) 3.651s (+3.8%) 0.930s 10 1.00x
▲ Vercel Next.js (Turbopack) 2.806s (+6.4% 🔺) 4.049s (+7.1% 🔺) 1.244s 10 1.03x
▲ Vercel Express 3.091s (+14.3% 🔺) 4.176s (+14.3% 🔺) 1.084s 10 1.14x

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

workflow with 10 sequential steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🌐 Redis 🥇 Next.js (Turbopack) 10.716s (~) 11.023s (~) 0.306s 3 1.00x
💻 Local Next.js (Turbopack) 10.745s 11.023s 0.278s 3 1.00x
💻 Local Express 10.817s (~) 11.021s (~) 0.204s 3 1.01x
💻 Local Nitro 10.820s (~) 11.022s (~) 0.202s 3 1.01x
🌐 MongoDB Next.js (Turbopack) 12.339s 13.025s 0.686s 3 1.15x
🐘 Postgres Next.js (Turbopack) 14.891s 15.543s 0.652s 2 1.39x
🐘 Postgres Nitro 20.212s (~) 21.062s (~) 0.850s 2 1.89x
🐘 Postgres Express 20.331s (+10.2% 🔺) 21.054s (+10.5% 🔺) 0.722s 2 1.90x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 19.756s (-3.2%) 20.833s (-2.8%) 1.077s 2 1.00x
▲ Vercel Express 19.852s (~) 20.999s (~) 1.147s 2 1.00x
▲ Vercel Nitro 19.893s (-2.6%) 21.089s (-4.5%) 1.196s 2 1.01x

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

workflow with 25 sequential steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🌐 Redis 🥇 Next.js (Turbopack) 26.917s (~) 27.051s (~) 0.134s 3 1.00x
💻 Local Next.js (Turbopack) 27.317s 28.054s 0.737s 3 1.01x
💻 Local Express 27.460s (~) 28.048s (~) 0.588s 3 1.02x
💻 Local Nitro 27.496s (~) 28.053s (~) 0.557s 3 1.02x
🌐 MongoDB Next.js (Turbopack) 30.553s 31.039s 0.486s 2 1.14x
🐘 Postgres Next.js (Turbopack) 36.972s 37.095s 0.122s 2 1.37x
🐘 Postgres Express 50.376s (+31.6% 🔺) 51.131s (+32.5% 🔺) 0.755s 2 1.87x
🐘 Postgres Nitro 50.493s (~) 51.135s (~) 0.642s 2 1.88x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 48.619s (-2.1%) 49.909s (-1.9%) 1.290s 2 1.00x
▲ Vercel Express 50.766s (+4.1%) 53.064s (+5.6% 🔺) 2.298s 2 1.04x
▲ Vercel Next.js (Turbopack) 51.725s (+5.6% 🔺) 53.335s (+6.8% 🔺) 1.610s 2 1.06x

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

workflow with 50 sequential steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🌐 Redis 🥇 Next.js (Turbopack) 54.311s (~) 55.099s (~) 0.788s 2 1.00x
💻 Local Next.js (Turbopack) 57.025s 57.105s 0.080s 2 1.05x
💻 Local Express 57.252s (~) 58.098s (~) 0.846s 2 1.05x
💻 Local Nitro 57.378s (~) 58.100s (~) 0.722s 2 1.06x
🌐 MongoDB Next.js (Turbopack) 60.952s 61.557s 0.605s 2 1.12x
🐘 Postgres Next.js (Turbopack) 74.157s 74.672s 0.515s 2 1.37x
🐘 Postgres Nitro 100.181s (+32.9% 🔺) 100.229s (+31.6% 🔺) 0.048s 1 1.84x
🐘 Postgres Express 100.492s (+33.7% 🔺) 101.232s (+33.8% 🔺) 0.740s 1 1.85x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 98.606s (-3.1%) 99.663s (-3.1%) 1.057s 1 1.00x
▲ Vercel Next.js (Turbopack) 101.056s (-0.8%) 102.886s (~) 1.830s 1 1.02x
▲ Vercel Express 102.009s (-1.5%) 103.059s (-1.8%) 1.050s 1 1.03x

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

Promise.all with 10 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🌐 Redis 🥇 Next.js (Turbopack) 1.244s (-1.0%) 2.006s (~) 0.762s 15 1.00x
💻 Local Express 1.410s (-0.7%) 2.005s (~) 0.596s 15 1.13x
💻 Local Nitro 1.421s (+3.2%) 2.005s (~) 0.584s 15 1.14x
💻 Local Next.js (Turbopack) 1.421s 2.006s 0.585s 15 1.14x
🐘 Postgres Next.js (Turbopack) 2.063s 2.833s 0.769s 11 1.66x
🌐 MongoDB Next.js (Turbopack) 2.171s 3.008s 0.837s 10 1.75x
🐘 Postgres Nitro 2.198s (+27.3% 🔺) 3.014s (+39.8% 🔺) 0.816s 10 1.77x
🐘 Postgres Express 2.513s (+46.6% 🔺) 3.013s (+44.9% 🔺) 0.500s 10 2.02x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 2.791s (-10.2% 🟢) 3.707s (-9.9% 🟢) 0.917s 9 1.00x
▲ Vercel Nitro 2.888s (-2.0%) 3.809s (-4.8%) 0.921s 8 1.03x
▲ Vercel Next.js (Turbopack) 3.044s (+12.5% 🔺) 3.949s (+13.2% 🔺) 0.904s 8 1.09x

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

Promise.all with 25 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Next.js (Turbopack) 2.487s 3.008s 0.521s 10 1.00x
🌐 Redis Next.js (Turbopack) 2.498s (-0.9%) 3.008s (~) 0.509s 10 1.00x
💻 Local Express 2.595s (-2.0%) 3.007s (~) 0.412s 10 1.04x
💻 Local Nitro 2.660s (+3.6%) 3.007s (~) 0.347s 10 1.07x
🌐 MongoDB Next.js (Turbopack) 4.714s 5.178s 0.464s 6 1.90x
🐘 Postgres Express 8.212s (-9.9% 🟢) 8.779s (-12.5% 🟢) 0.567s 4 3.30x
🐘 Postgres Nitro 8.399s (-7.1% 🟢) 9.042s (-9.8% 🟢) 0.643s 4 3.38x
🐘 Postgres Next.js (Turbopack) 11.214s 11.702s 0.488s 3 4.51x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 4.257s (~) 5.108s (-12.6% 🟢) 0.851s 6 1.00x
▲ Vercel Express 4.910s (+68.0% 🔺) 6.327s (+51.7% 🔺) 1.417s 5 1.15x
▲ Vercel Next.js (Turbopack) 5.322s (+75.8% 🔺) 6.563s (+50.5% 🔺) 1.241s 5 1.25x

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

Promise.all with 50 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🌐 Redis 🥇 Next.js (Turbopack) 4.011s (-1.5%) 4.724s (+3.1%) 0.713s 7 1.00x
💻 Local Nitro 7.531s (+5.1% 🔺) 8.018s (~) 0.487s 4 1.88x
💻 Local Express 7.609s (-1.4%) 8.019s (~) 0.410s 4 1.90x
💻 Local Next.js (Turbopack) 8.079s 8.515s 0.436s 4 2.01x
🌐 MongoDB Next.js (Turbopack) 9.780s 10.352s 0.572s 3 2.44x
🐘 Postgres Express 43.088s (-6.7% 🟢) 44.104s (-6.4% 🟢) 1.016s 1 10.74x
🐘 Postgres Nitro 44.975s (+0.6%) 45.122s (~) 0.147s 1 11.21x
🐘 Postgres Next.js (Turbopack) 51.163s 52.123s 0.960s 1 12.76x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 8.619s (-3.4%) 9.621s (-12.4% 🟢) 1.002s 4 1.00x
▲ Vercel Express 9.351s (+193.6% 🔺) 10.671s (+136.4% 🔺) 1.320s 3 1.08x
▲ Vercel Next.js (Turbopack) 9.658s (+85.0% 🔺) 11.092s (+76.4% 🔺) 1.434s 3 1.12x

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

Promise.race with 10 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🌐 Redis 🥇 Next.js (Turbopack) 1.252s (+1.5%) 2.006s (~) 0.754s 15 1.00x
💻 Local Express 1.424s (-0.6%) 2.004s (~) 0.581s 15 1.14x
💻 Local Next.js (Turbopack) 1.430s 2.006s 0.576s 15 1.14x
💻 Local Nitro 1.438s (+2.8%) 2.005s (~) 0.567s 15 1.15x
🐘 Postgres Express 2.103s (+12.0% 🔺) 2.742s (+32.0% 🔺) 0.638s 11 1.68x
🐘 Postgres Next.js (Turbopack) 2.140s 2.741s 0.600s 11 1.71x
🌐 MongoDB Next.js (Turbopack) 2.156s 3.008s 0.852s 10 1.72x
🐘 Postgres Nitro 2.245s (+12.3% 🔺) 3.013s (+19.9% 🔺) 0.768s 10 1.79x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 2.587s (-82.3% 🟢) 3.436s (-77.9% 🟢) 0.848s 9 1.00x
▲ Vercel Express 2.842s (-73.4% 🟢) 3.895s (-66.5% 🟢) 1.054s 8 1.10x
▲ Vercel Nitro 3.972s (+42.7% 🔺) 5.051s (+34.6% 🔺) 1.078s 6 1.54x

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

Promise.race with 25 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🌐 Redis 🥇 Next.js (Turbopack) 2.517s (+1.3%) 3.008s (~) 0.491s 10 1.00x
💻 Local Next.js (Turbopack) 2.718s 3.008s 0.290s 10 1.08x
💻 Local Nitro 2.723s (+3.1%) 3.008s (~) 0.285s 10 1.08x
💻 Local Express 2.811s (+2.5%) 3.008s (~) 0.197s 10 1.12x
🌐 MongoDB Next.js (Turbopack) 4.711s 5.178s 0.467s 6 1.87x
🐘 Postgres Express 11.060s (+14.8% 🔺) 11.704s (+12.9% 🔺) 0.644s 3 4.39x
🐘 Postgres Nitro 12.453s (+47.0% 🔺) 12.702s (+40.5% 🔺) 0.249s 3 4.95x
🐘 Postgres Next.js (Turbopack) 12.905s 13.371s 0.466s 3 5.13x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 2.947s (-29.5% 🟢) 3.945s (-24.8% 🟢) 0.998s 8 1.00x
▲ Vercel Nitro 2.966s (-0.7%) 3.804s (-3.2%) 0.837s 8 1.01x
▲ Vercel Next.js (Turbopack) 2.970s (-17.2% 🟢) 4.287s (-4.4%) 1.317s 8 1.01x

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

Promise.race with 50 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🌐 Redis 🥇 Next.js (Turbopack) 4.086s (~) 4.724s (+3.1%) 0.638s 7 1.00x
💻 Local Express 7.848s (-8.0% 🟢) 8.021s (-11.1% 🟢) 0.173s 4 1.92x
💻 Local Nitro 8.323s (+7.5% 🔺) 9.020s (+12.5% 🔺) 0.697s 4 2.04x
💻 Local Next.js (Turbopack) 8.579s 9.269s 0.689s 4 2.10x
🌐 MongoDB Next.js (Turbopack) 9.798s 10.350s 0.552s 3 2.40x
🐘 Postgres Next.js (Turbopack) 50.382s 51.110s 0.728s 1 12.33x
🐘 Postgres Nitro 50.620s (+14.0% 🔺) 51.118s (+13.2% 🔺) 0.498s 1 12.39x
🐘 Postgres Express 50.712s (+5.0%) 51.121s (+4.1%) 0.409s 1 12.41x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 5.200s (-41.5% 🟢) 6.307s (-39.0% 🟢) 1.107s 6 1.00x
▲ Vercel Next.js (Turbopack) 7.283s (+108.8% 🔺) 8.638s (+88.2% 🔺) 1.355s 4 1.40x
▲ Vercel Express 7.725s (+151.9% 🔺) 9.193s (+108.0% 🔺) 1.467s 4 1.49x

🔍 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
🌐 Redis 🥇 Next.js (Turbopack) 0.146s (~) 1.000s (~) 0.001s (+9.1% 🔺) 1.007s (~) 0.861s 10 1.00x
💻 Local Next.js (Turbopack) 0.150s 1.001s 0.012s 1.018s 0.867s 10 1.03x
💻 Local Express 0.169s (-2.3%) 1.002s (~) 0.011s (-2.6%) 1.016s (~) 0.848s 10 1.16x
💻 Local Nitro 0.175s (-4.9%) 1.003s (~) 0.012s (+13.3% 🔺) 1.017s (~) 0.842s 10 1.20x
🌐 MongoDB Next.js (Turbopack) 0.506s 0.939s 0.002s 1.008s 0.502s 10 3.47x
🐘 Postgres Next.js (Turbopack) 1.040s 1.282s 0.001s 1.512s 0.472s 10 7.13x
🐘 Postgres Express 2.099s (+62.0% 🔺) 2.941s (+62.0% 🔺) 0.001s (-20.0% 🟢) 3.015s (+49.8% 🔺) 0.917s 10 14.38x
🐘 Postgres Nitro 2.298s (+105.3% 🔺) 2.745s (+42.9% 🔺) 0.001s (+7.7% 🔺) 3.015s (+49.7% 🔺) 0.717s 10 15.75x

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 2.358s (-4.1%) 2.773s (-5.8% 🟢) 0.365s (+65.2% 🔺) 3.802s (~) 1.444s 10 1.00x
▲ Vercel Express 2.458s (-3.4%) 2.912s (-4.7%) 0.214s (-16.8% 🟢) 3.775s (-6.8% 🟢) 1.317s 10 1.04x
▲ Vercel Next.js (Turbopack) 2.476s (-14.0% 🟢) 2.660s (-13.9% 🟢) 0.416s (+122.0% 🔺) 3.748s (-3.0%) 1.272s 10 1.05x

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

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World 🥇 Fastest Framework Wins
💻 Local Next.js (Turbopack) 6/12
🐘 Postgres Next.js (Turbopack) 7/12
▲ Vercel Nitro 7/12
Fastest World by Framework

Winner determined by most benchmark wins

Framework 🥇 Fastest World Wins
Express 💻 Local 11/12
Next.js (Turbopack) 🌐 Redis 9/12
Nitro 💻 Local 11/12
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

@TooTallNate TooTallNate merged commit c56dc38 into main Feb 13, 2026
151 of 153 checks passed
Copy link
Member Author

Merge activity

@TooTallNate TooTallNate deleted the nate/split-serde-for-browser branch February 13, 2026 00:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants