TanStack RSC, React2DoS CVE, MUI v9, WebStreams 10x Faster, and the Case for Vertical Codebases

Published on 15.04.2026

bash — 80×24$pnpm dev▶ ready on localhost:3000$git commit -m "feat: og images"$npx tsc --noEmit✓ 0 errorsCODING

React Server Components Your Way: TanStack's Client-First Approach

TLDR: TanStack Start has shipped experimental React Server Component support that treats RSCs as ordinary fetchable data streams rather than the center of your app architecture. The API is intentionally small, and it pairs naturally with TanStack Query and Router for caching without any special RSC-aware mode.

Summary: The TanStack team has spent a long time thinking carefully about how RSCs should fit into their ecosystem, and the answer they landed on is refreshingly different from Next.js. In TanStack Start, an RSC is just a React Flight stream you can fetch from a server function, cache with Query, and decode wherever you want on the client. There is no requirement to structure your entire app around a server-owned tree. The client owns the component tree, and RSCs are a data primitive you reach for when it makes sense.

In practice, this means you call a server function that returns a readable stream, pass that stream into TanStack Query as a query function, and render the decoded result like any other async data. The caching story falls out automatically. Static content gets staleTime: Infinity, route-level content gets the Router's existing loader cache, and truly static content can even be cached at the CDN layer by setting appropriate response headers on a GET server function.

The team measured the impact on tanstack.com itself and the results were honest: heavily content-driven pages like blog posts dropped around 153KB of gzipped JavaScript and saw Lighthouse scores jump meaningfully. Total Blocking Time on the blog page dropped from 1,200ms to 260ms. But some landing pages were flat or slightly worse because they were already dominated by interactive UI that doesn't benefit from server-side rendering.

The genuinely novel part of the release is Composite Components, a composition model where the server renders a UI structure with open slots and the client fills those slots with regular client components. The server does not need to know what goes in the slots. It declares a shape, and the client owns the tree assembly. This inverts the typical RSC mental model in an interesting way.

Key takeaways:

  • RSCs in TanStack Start are ordinary Flight streams, not a framework-owned tree, making them composable with existing caching tools
  • The team's measured results show 153KB JS savings on content-heavy pages but flat results on interactive ones
  • Composite Components offer a new server-client composition pattern where the client assembles the final tree

Why do I care: This is the RSC design I actually wanted to see. The Next.js model requires you to restructure your entire mental model around server-first components. TanStack's approach lets you reach for RSCs as an optimization, not a paradigm shift. If you are already using TanStack Query, you get all the caching semantics you know without learning a new cache invalidation system. The Composite Components idea is genuinely worth watching.

React Server Components Your Way


React2DoS (CVE-2026-23869): A Quadratic Complexity DoS in the Flight Protocol

TLDR: Researchers at Imperva discovered a denial-of-service vulnerability in React Server Components' Flight protocol deserialization that achieves quadratic complexity, meaning a tiny payload of tens of kilobytes can trigger minutes of CPU work. The fix landed in React 19.2.5, with backports to 19.0.5 and 19.1.6.

Summary: The React Flight protocol allows the client to send structured data to Server Functions using a chunked reference system where chunks can reference each other. The vulnerability exploits the Map and Set constructors in the deserializer. Normally, the deserializer marks references as "consumed" to avoid recomputing them, but that consumed flag is only set after successful resolution. When a Map is constructed with an invalid argument like new Map([null]), the constructor throws, and the consumed flag never gets set.

That means you can reference the same failing expression over and over in a single payload, and it will be recomputed each time. The researchers found they could get quadratic complexity by putting a series of valid map entries at the start of the root entry followed by a series of self-referential $Q0 references. The valid entries force the Map constructor to iterate over them before hitting the exception, and you get n-squared work for n entries.

The comparison to the earlier CVE-2026-23864 is telling. The BigInt deserialization vulnerability required a nearly 1MB payload to cause significant delays. React2DoS achieves comparable or worse CPU exhaustion with payloads in the tens of kilobytes. The fix was straightforward: set the consumed flag before calling the Map or Set constructor so that failed constructions are not retried.

This is the second significant Flight protocol vulnerability disclosed this year, following React2Shell earlier. The pattern is becoming familiar: custom serialization formats with complex parsing logic are an attractive target, and the RSC ecosystem is still working through the security implications of its design.

Key takeaways:

  • The vulnerability allows unauthenticated denial-of-service via quadratic CPU complexity in Map/Set deserialization
  • Tens of kilobytes of payload can cause minutes of server-side CPU work
  • React 19.2.5 and the backport versions contain the fix; update immediately if you use RSCs with user-controlled input

Why do I care: If you run any public-facing Next.js App Router or similar RSC-based app, this is a patch-immediately situation. The attack surface is any endpoint that accepts Flight-encoded form data or Server Function calls. The fix is in stable releases now, so there is no reason to wait. The broader pattern of Flight protocol vulnerabilities in rapid succession is worth watching as a category, not just individual CVEs.

React2DoS (CVE-2026-23869): When the Flight Protocol Crashes at Takeoff


Vercel Made WebStreams 10x Faster and Now It's Going Into Node.js

TLDR: Vercel built fast-webstreams, a drop-in replacement for the WHATWG Streams API that routes operations through Node.js streams internals, achieving up to 14.6x faster throughput for the exact byte stream pattern React Server Components use. The optimizations are already being contributed upstream to Node.js itself.

Summary: When the Vercel team started profiling Next.js server rendering, one unexpected culprit kept appearing in the flamegraphs: the WebStreams API itself. Not the application code running inside it, but the streams. Every reader.read() call on a native WebStream allocates a ReadableStreamDefaultReadRequest object, creates a new Promise, enqueues it, and routes resolution through the microtask queue. That is four allocations and a microtask hop just to return data that was already sitting in the buffer.

For the React Flight pattern specifically, where React renders to a byte stream with the controller captured in start() and chunks enqueued externally, native WebStreams hit around 110 MB/s. The fast-webstreams library achieves 1,600 MB/s on the same pattern by replacing the internal buffer with a minimal array-based structure that uses direct callback dispatch instead of EventEmitter.

The most impressive optimization applies when you chain pipeThrough calls between fast streams. Rather than starting the pipe immediately, the library records upstream links. When pipeTo is finally called at the end of the chain, it walks the chain, collects the underlying Node.js stream objects, and issues a single pipeline() call. Zero Promises per chunk. For three chained transforms, that is 9.7x faster than native.

The work is not just a userland library. After the Vercel team published their findings, Matteo Collina submitted a PR to Node.js itself applying two of the key ideas: resolving read() synchronously when data is buffered, and batching multiple reads from the controller queue without allocating per-chunk request objects. The long-term goal is for WebStreams to be fast enough that fast-webstreams does not need to exist.

Key takeaways:

  • Native WebStreams have 4 allocations and a microtask hop per chunk; fast-webstreams eliminates most of these for common patterns
  • The React Flight byte stream pattern goes from 110 MB/s to 1,600 MB/s, a 14.6x improvement
  • The optimizations are being contributed upstream to Node.js, benefiting every Node.js user without any library installation

Why do I care: This is the kind of infrastructure work that does not get headlines but makes everything faster. If you run a Next.js app at any scale, you are paying this WebStreams tax on every server-rendered request. The Node.js upstream PR is the right long-term outcome, and I appreciate that Vercel published the work transparently rather than keeping a proprietary performance advantage.

We Ralph Wiggumed WebStreams to make them 10x faster


Introducing Material UI and MUI X v9: A Synced Ecosystem

TLDR: MUI has simultaneously released Material UI v9 and MUI X v9, re-aligning their major version numbers for the first time since the v6 split. The release adds NumberField, Menubar, a Scheduler alpha, a Chat alpha, and a Data Grid AI Assistant backed by a new MUI Console for license and API key management.

Summary: The version number situation with MUI has been confusing for a while. Material UI was on v7 while MUI X was on v9, and keeping peer dependencies straight across projects required more mental overhead than it should have. The team decided to jump Material UI directly from v7 to v9 (skipping v8 entirely, because apparently there is no Material UI v8 just as there is no v2) to realign with MUI X. Going forward, a single shared major version number means upgrade guides, peer dependency ranges, and compatibility communication all happen in one consistent frame.

The component additions are the kind of gaps that have been annoying for a long time. NumberField and Menubar are new in Material UI, and both will eventually ship as npm packages rather than documentation recipes you copy-paste. On the MUI X side, the Scheduler debuts in alpha with resource-aware calendars and timelines, and Chat also debuts in alpha as an embeddable conversational UI component with streaming support.

The Data Grid AI Assistant is in production now, and the new MUI Console gives teams a single place to manage license keys, service API keys, billing, and usage without going through support tickets. The pricing model has also shifted to application-based licensing starting in April 2026.

One naming change worth noting: MUI Chat (the generative UI tool that builds components from natural language descriptions) is being renamed to MUI Recipes to avoid colliding with the new MUI X Chat component. They are completely different things, and the old name was genuinely confusing.

Key takeaways:

  • Material UI jumps from v7 to v9 to align with MUI X v9 under a single shared major version
  • New components include NumberField, Menubar, Scheduler alpha, and Chat alpha
  • Data Grid AI Assistant plus MUI Console create a production path for AI-assisted UI workflows

Why do I care: The version realignment is the right call even if the skip from v7 to v9 looks odd. Synchronized major versions mean I can look at a peer dependency range and immediately know what pairs with what. The Scheduler component is one I have been waiting for, since building resource-aware calendars from scratch is a recurring source of pain. The Emotion removal work planned for the next major is also good news for teams trying to adopt Tailwind.

Introducing Material UI and MUI X v9


Base UI v1.4.0: Accessibility and Stability Fixes Across the Board

TLDR: Base UI v1.4.0 ships a significant round of bug fixes across nearly every component in the library, with a notable new OTPField component for one-time password entry and substantial fixes to Combobox, Drawer, and Select behavior on mobile.

Summary: Base UI is maturing quickly, and v1.4.0 is primarily a stability release that addresses the rough edges that show up when you actually ship these components in production. The Combobox component alone got seven fixes, covering iOS viewport settling, browser autofill with object values, scroll lock during controlled re-renders, and touch event handling to prevent item taps from blurring the input. These are exactly the kinds of edge cases that documentation rarely covers but real users hit constantly.

The Drawer component got three fixes related to touch interactions: touch scroll in portaled popups, nested swipe cancel state, and interrupted swipe dismiss cleanup. Anyone who has built a mobile-first drawer in React knows these interactions are fragile, and getting them right across different iOS and Android versions takes serious work.

The new OTPField component is a preview addition for one-time password and verification code entry patterns. This is a component that most teams end up writing themselves or pulling from a separate library, so having it as part of a headless primitive library is useful.

The Toast component gained upsert support in its add method and fixed timer resumption after the window regains focus, which is a subtle bug that makes toast timers feel unreliable after the user switches away and back.

Key takeaways:

  • Combobox, Drawer, and Select received substantial mobile-specific fixes covering iOS viewport, touch events, and autofill
  • New OTPField component provides a headless primitive for one-time password entry
  • Toast gains upsert support and fixes timer behavior after window focus returns

Why do I care: Base UI is the headless component library I recommend when teams want to own their own styling without fighting Material UI's theme system. These kinds of meticulous mobile fixes are what separate production-quality component libraries from the ones that look great in demos but fall apart on real devices. The OTPField addition means one fewer dependency for auth flows.

v1.4.0 · Base UI


The Vertical Codebase: Stop Organizing by Type, Start Organizing by Domain

TLDR: TkDodo argues that the standard horizontal codebase structure (components/, hooks/, utils/, types/) creates a maintenance nightmare at scale and that organizing code vertically by domain (dashboard/, widgets/, profiling/) produces codebases that are easier to navigate, own, and evolve.

Summary: The post opens with a tweet Dominik posted two years ago that still resonates: grouping by type puts useTheme next to useTodo but not next to ThemeProvider. The type-based split is convenient early on, but it creates a codebase where code that changes together does not live together. At the Sentry scale he references as an example, horizontal structure produces over 200 files in a top-level components directory with nothing in common except that they are components.

The core argument is about coupling and cohesion. Horizontal structures create high coupling (any component can import any util) and low cohesion (code that lives together is only loosely related). A vertical structure groups everything related to a domain or feature under one directory, regardless of whether it is a component, hook, utility, or constant. src/widgets/ contains everything related to widgets. Period.

The practical challenge is finding the right vertical, which requires actual judgment about domain boundaries. Some code is used across multiple features and becomes its own vertical. Dominorepo structures with proper package exports give you enforcement: private code in a vertical is private to that package, and other verticals must go through the public interface. The eslint-plugin-boundaries approach gives you similar discipline without a full monorepo migration.

There is also a note about AI agents here that I found insightful. Agents navigate codebases the same way humans do, and a well-structured vertical codebase gives them the same advantages: clear boundaries, fast feedback, predictable conventions. The argument that "AI will rewrite everything anyway" misses that agents are also more effective when the codebase is organized well.

Key takeaways:

  • Horizontal type-based structure (components/, hooks/, utils/) creates low cohesion and becomes unmaintainable at scale
  • Vertical domain-based structure groups related code regardless of type, matching how teams actually own and change code
  • Monorepo package exports or ESLint boundary rules can enforce public interfaces between verticals

Why do I care: I have seen this pattern play out many times on large codebases. The horizontal split feels clean at 50 files and nightmarish at 500. The vertical approach does require more upfront thought about domain boundaries, but that is a feature, not a bug. The teams that debate domain boundaries early make better architectural decisions than the teams that defer the question until the codebase is already a labyrinth.

The Vertical Codebase


Custom ESLint Rules: Now More Valuable Than Ever

TLDR: A detailed walkthrough on writing custom ESLint rules by exploring Abstract Syntax Trees, building a rule that catches derived state in useEffect, and making the case that custom rules are more effective than AI coding prompts for enforcing project-specific conventions.

Summary: The post starts with a relatable frustration: the author kept getting the same PR review comment about unnecessary useEffect calls, and the conversation kept happening manually. The solution was to write a custom ESLint rule. Along the way, the post does an excellent job explaining what an Abstract Syntax Tree actually is and how ESLint's visitor pattern works.

The AST explanation is concrete and practical. Every piece of JavaScript code becomes a tree of typed nodes. A variable declaration has a VariableDeclaration node with a kind property. A function call is a CallExpression whose callee is the thing being called. A property access like console.log is a MemberExpression. AST Explorer becomes your best friend for understanding the shape of any code pattern.

The derived state rule they build tracks useState declarations to collect setter names, then finds useEffect calls where every statement in the callback body is a call to one of those setters. The resulting warning is narrow by design: it catches the obvious cases with zero false positives rather than trying to handle every edge case.

The section on AI coding assistants is the part worth remembering. A rule in your ESLint config runs after the AI writes code, catches violations before they merge, and does not forget the constraint after a long context window. Custom rules plus AI prompts work together: the prompt prevents the AI from writing bad patterns in the first place, the rule catches what slips through.

Key takeaways:

  • ESLint rules work by visiting typed AST nodes in a depth-first traversal; AST Explorer lets you prototype rules in the browser
  • Custom rules can be registered as local plugins in ESLint's flat config without any npm publishing
  • ESLint rules are more reliable than AI prompts alone for enforcing conventions because they run deterministically after code is written

Why do I care: Writing custom ESLint rules is one of the highest-leverage investments a frontend team can make, and most teams do not do it. The barrier is lower than people think, especially with ESLint's flat config and the local plugin approach. If you have a convention you keep writing in PR comments, that is a candidate for a rule. This post is the clearest introduction I have seen.

Now more than ever, you need to master custom ESLint rules


Contributing Callsite Revalidation Opt-out to React Router

TLDR: A developer's first-person account of contributing a new feature to React Router that lets you opt individual fetcher submissions and navigation calls out of the default aggressive revalidation behavior, now available as unstable_defaultShouldRevalidate in v7.11.0.

Summary: React Router's default behavior revalidates all active loaders after any successful mutation. This is a sensible default for consistency, but it becomes painful when you have a small, isolated mutation (like tagging a project with a label) that you know does not affect the data in other active loaders. The old workaround was to export a shouldRevalidate function from every affected route, which spreads knowledge of your mutation's semantics across multiple files.

The new unstable_defaultShouldRevalidate option lets you set the default at the callsite: pass false to fetcher.submit() or <Form> and no loaders revalidate unless their own shouldRevalidate function explicitly returns true. The feature works across fetcher.submit, <Form>, <Link>, and useNavigate, covering all navigation types.

The behind-the-scenes story is also worth reading. The author found the React Router contributing process through Discord, struggled with the build toolchain, eventually figured out the module watching setup, put together a PR, and then worked iteratively with the React Router steering committee to get the design right before implementing the changes. Brooks Lybrand caught a bug that led to merging a prerequisite PR first. It is a good example of how open-source collaboration actually works.

Key takeaways:

  • unstable_defaultShouldRevalidate: false on any navigation primitive prevents global revalidation after that mutation
  • The feature ships in React Router v7.11.0 with the unstable_ prefix while it soaks for feedback
  • The per-callsite approach keeps mutation-specific revalidation knowledge colocated with the mutation itself

Why do I care: React Router's nuclear revalidation has bitten me before. The shouldRevalidate export workaround works but is fragile and requires knowing which routes are active. Callsite control is the right abstraction: the component making the mutation knows what it affects, and that knowledge belongs in the same place as the fetcher.submit() call. I will be using this immediately.

Contributing Callsite Revalidation Opt-out to React Router


Agent React DevTools: Giving AI Agents Access to React Internals

TLDR: Callstack released a CLI tool called Agent React DevTools that connects AI agents directly to React DevTools, giving them access to the live component tree, profiling data, render commits, and performance hotspots without requiring any changes to application code.

Summary: The gap between what AI agents can see and what developers see during debugging has been the main limitation of AI-driven QA. Tools like agent-device gave agents visual UI tree access, which works for basic interaction testing. But if a button does not render because a TanStack Query has not resolved, the agent looking at the accessibility tree only knows the app is stuck. It has no context to determine why.

Agent React DevTools bridges that gap by exposing the same data React DevTools surfaces to human developers, but in a machine-readable format that AI agents can consume directly. Agents can inspect the live component tree snapshot, read profiling data, analyze renders and commits, and identify performance hotspots. The tool integrates as a skill that agents can install directly rather than requiring code changes in the app.

The connection between this tool and the broader Rozenite ecosystem is also notable. Rozenite gives React Native DevTools access to third-party plugin data (TanStack Query state, MMKV storage, React Navigation routing state), and Rozenite for Agents extends that to AI agents. The team plans to integrate Rozenite plugins directly into Agent DevTools so the same plugins are discoverable from a single unified CLI.

Key takeaways:

  • Agent React DevTools exposes the live React component tree, profiling data, and render analysis to AI agents via MCP
  • No app code changes required for core functionality; it connects via React DevTools protocol
  • Rozenite integration will add third-party plugin data (Query, Navigation, MMKV) to the same interface

Why do I care: AI-driven QA is coming whether teams plan for it or not. The question is whether the agents doing it have access to enough context to do it well. A UI tree that shows "button not visible" is less useful than a component tree that shows "TanStack Query in loading state because network request pending." This tool closes that gap in a reasonable way.

Agent React DevTools: Debug React Apps with AI Agents


TLDR: metro-mcp is a plugin-based MCP server that connects to Metro bundler via Chrome DevTools Protocol, giving AI agents and IDE extensions access to console logs, network requests, the React component tree, Redux state, navigation state, accessibility audits, CPU profiling, and more, with no app code changes required for most features.

Summary: The tool discovery and connection story is clean: metro-mcp scans for Metro on the standard ports (8081, 8082, 19000-19002), connects via CDP the same way Chrome DevTools does, and starts streaming console logs, network events, and errors into buffers. From that point, everything is exposed as MCP tools and resources.

The breadth of the plugin surface is impressive. Beyond the expected console and network access, there are plugins for Redux state inspection and action dispatch, React Navigation and Expo Router state, iOS Simulator and Android Emulator control, permission management, accessibility auditing, CPU profiling via a React DevTools hook, heap sampling, and render tracking. The test recording feature is particularly interesting: you can record real user interactions and have it generate Appium, Maestro, or Detox test scripts from the recorded actions.

One practical detail: Hermes only allows a single CDP debugger connection at a time, which means the MCP connection and Chrome DevTools would normally conflict. metro-mcp solves this with a built-in CDP proxy that multiplexes the single Hermes connection so both can coexist. The open_devtools MCP tool opens Chrome DevTools through the proxy instead of directly.

The optional app integration lets you expose custom commands and state snapshots to the MCP server. In development mode, you register commands on a global bridge object, and the MCP client can call them. This is useful for seeding test data, resetting app state, or triggering specific flows without tapping through the UI.

Key takeaways:

  • metro-mcp connects to Metro via CDP with no app code changes required for core features
  • Covers console, network, component tree, Redux, Navigation, profiling, accessibility, and test recording via 20+ plugin categories
  • Built-in CDP proxy allows Chrome DevTools and the MCP to coexist without conflicting over the single Hermes connection

Why do I care: React Native debugging tooling has always lagged behind web tooling. metro-mcp closes that gap in a practical way by surfacing everything through MCP, which means any AI agent or IDE extension that speaks MCP gets full debugging access for free. The test recording feature alone makes this worth exploring for any team that does end-to-end testing.

metro-mcp on GitHub


React Native 0.85: Shadow Tree Commit Branching and Animation Backend by Default

TLDR: React Native 0.85 introduces dual-branch Shadow Tree commits to eliminate contention between React updates and native animation/styling libraries, and promotes the C++ Animation Backend to enabled by default, with a clean mechanism for animation libraries to update props without going through the JavaScript rendering pipeline.

Summary: The Shadow Tree commit branching change is the most architecturally significant addition in this release. Before 0.85, all commits from React and from native sources like animations shared the same _currentRevision_, which created race conditions and resource exhaustion when multiple libraries tried to update the Shadow Tree simultaneously. The new model gives React its own dedicated _currentReactRevision_ branch while other sources continue using the main branch. At the end of each event loop pass, the JS revision merges into the main revision.

The merge behavior has an important implication for library authors: the merge overwrites anything that was committed to the main branch between fork and merge. Libraries that commit to the Shadow Tree from C++ must use a commit hook to reapply their changes after the JS revision merges, not just after regular React commits. The change is currently behind the enableFabricCommitBranching feature flag.

The Animation Backend becoming the default means animation frameworks now have a dedicated, well-defined path for updating props without touching the JavaScript rendering pipeline. The backend uses a choreographer that wraps platform-native frame callbacks, a callback-based API where animation frameworks register update functions that fire on each frame, and a commit hook that preserves animated values across React re-renders.

There is also a breaking change worth noting: the cloneMultiple API now requires std::shared_ptr<const ShadowNodeFamily> instead of raw pointers. Library authors using this API to batch style updates across components need to update their C++ code.

Key takeaways:

  • Shadow Tree commit branching separates React's JS updates from native-source updates, eliminating contention between React and styling/animation libraries
  • The C++ Animation Backend is now enabled by default, giving animation frameworks a dedicated non-React path for prop updates
  • Library authors using cloneMultiple must update to shared_ptr rather than raw pointer arguments

Why do I care: This is a release that matters most to library authors who touch the Shadow Tree from native code, but the knock-on effects reach every React Native app that uses Reanimated, Unistyles, or any animation framework. Deterministic Shadow Tree updates across multiple libraries means fewer mysterious rendering bugs. The direction toward a shared mechanism for native updates is the right long-term architecture.

React Native 0.85 changelog dive


You Can't Cancel a JavaScript Promise (Except Sometimes You Can)

TLDR: The Inngest SDK interrupts async workflow functions by returning a promise that never resolves, letting the garbage collector clean up the suspended function. This achieves generator-style interruption while letting users write plain async/await code, and it actually works correctly in production.

Summary: Promise cancellation has been discussed in the TC39 committee and rejected. The reason is sensible: cancelling arbitrary async code mid-execution can leave resources in inconsistent states, so true cancellation requires cooperative cleanup that undermines the simplicity people want from .cancel(). But there is a middle path that Inngest discovered while building their workflow SDK.

The technique relies on two JavaScript behaviors. First, a promise that never resolves does not keep the Node.js event loop alive. Promises are just objects in memory. If nothing else is waiting, the process exits cleanly. Second, JavaScript's garbage collector collects suspended async functions when nothing references them, including the promises they are awaiting. This means an async function awaiting a never-resolving promise will eventually be garbage collected along with all its closure state.

The Inngest SDK uses this to implement step-by-step workflow execution on serverless infrastructure. Each invocation re-runs the workflow function from the top. Steps that already completed return their memoized results immediately as resolved promises. The first new step registers itself, then returns a never-resolving promise. A setTimeout(0) macrotask lets all the microtask-resolved steps drain before the runtime checks whether a new step was found. If it was, the runtime executes it, memoizes the result, and ends the invocation. On the next invocation, one more step has been memoized.

The garbage collection proof is thorough: using FinalizationRegistry to observe when promises are collected, the post confirms that all promises including the hanging ones get collected after the workflow completes, even those from earlier invocations that created new promise instances for already-memoized steps.

Key takeaways:

  • A promise that never resolves does not prevent garbage collection if nothing references it, making it a safe interruption mechanism
  • The technique enables generator-style execution control (run one step, interrupt, resume) with plain async/await syntax
  • Memoized step results plus setTimeout(0) macrotask boundary create a clean re-entrant workflow execution model

Why do I care: This is a clever use of JavaScript semantics that I would not have thought to reach for. The Inngest SDK is interesting on its own, but the technique is general enough to be worth understanding for any case where you need to interrupt someone else's async function at a controlled point. The post is also a well-structured primer on the event loop, microtasks, and macrotasks.

You can't cancel a JavaScript promise (except sometimes you can)


What Encore Learned Building a 67,000-Line Rust Runtime for TypeScript

TLDR: Encore describes building a complete HTTP, database, pub/sub, tracing, and API gateway infrastructure layer in Rust with Node.js NAPI bindings, achieving 9x the throughput of Express.js and processing billions of requests daily. The post covers the non-obvious technical challenges of bridging JavaScript's async model to Rust's.

Summary: The decision to write the TypeScript runtime in Rust rather than TypeScript was driven by two concrete requirements: a shared Rust core that could eventually support multiple language targets (similar to how Prisma uses a Rust core with language bindings), and the ability to run the HTTP request lifecycle, database connection management, and pub/sub fully multi-threaded outside Node.js's single-threaded event loop.

The hardest technical problem was calling from Rust back into JavaScript and capturing return values. NAPI's standard threadsafe functions support calling JavaScript but not capturing what JavaScript returns. Encore forked the napi-rs ThreadSafeFunction to allow manual invocation and return value capture, then added promise detection to bridge JavaScript's async model to tokio channels on the Rust side.

The in-process Pingora API gateway is one of the more interesting architectural decisions. Rather than running the gateway as a separate nginx or Envoy process, Encore embeds Pingora directly into the runtime. This means the auth system, service registry, and trace collector share memory with the gateway without any serialization boundary. User-defined auth handlers written in TypeScript execute directly inside the gateway by calling back into Node.js, with the auth result passed directly to the endpoint handler as an Arc'd struct.

The benchmark numbers are real: 121,005 requests per second versus Express.js at 15,707, with P99 latency of 2.3ms versus 11.9ms. With request validation enabled, the gap widens further because Encore validates at the Rust layer using type information extracted at compile time, while Express runs Zod validation in JavaScript.

Key takeaways:

  • Encore's Rust runtime handles HTTP, database, pub/sub, tracing, and the API gateway in a single in-process Rust layer, leaving TypeScript for business logic only
  • Forking napi-rs to capture JavaScript return values and bridge async models was necessary since NAPI's standard API does not support it
  • In-process Pingora gateway allows user TypeScript auth handlers to execute inside the proxy with zero serialization overhead

Why do I care: This is the most detailed public account I have read of building a serious Rust-Node.js hybrid runtime. The lessons about NAPI, about the cost of sidecar IPC, about why you cannot replace WHATWG pipeTo with pipeline are all practically useful for anyone building native modules. The performance numbers are also a useful reality check on what is theoretically achievable when you move infrastructure concerns out of the JavaScript event loop.

What We Learned Building a Rust Runtime for TypeScript


Bun v1.3.12: Native Headless Browser, In-Process Cron, and JavaScriptCore Upgrades

TLDR: Bun v1.3.12 ships Bun.WebView, a native headless browser automation API with WebKit and Chrome backends; an in-process Bun.cron() scheduler for long-running servers; JavaScriptCore upgrades including explicit resource management (using/await using); and dozens of Node.js compatibility fixes.

Summary: The Bun.WebView addition is the headline feature. It supports two backends: WebKit via the system WKWebView on macOS with zero external dependencies, and Chrome via DevTools Protocol with auto-detection of installed browsers. All input events are dispatched as OS-level events, which means view.click() is indistinguishable from a real mouse click (isTrusted: true). Selector-based methods auto-wait for actionability, Playwright-style. Screenshots, JavaScript evaluation, navigation, scrolling, and raw CDP calls are all supported. One browser subprocess is shared per Bun process, with additional WebView instances opening tabs.

The in-process Bun.cron() variant takes a callback and schedules it on a cron expression within the current process. This is separate from the OS-level Bun.cron(path, schedule, title) which creates persistent crontab/launchd/Task Scheduler entries. The in-process variant does not allow overlapping runs, is safe with hot reload, and supports using for auto-stop at scope exit.

The JavaScriptCore upgrade brings over 1,650 upstream commits, including native support for explicit resource management via using and await using. This is the TC39 proposal that lets you declare a resource with using and have its Symbol.dispose method called automatically at the end of the block, similar to C#'s using or Python's with. Other JIT improvements include faster Array.isArray, faster promise resolution, and improved register allocation.

Key takeaways:

  • Bun.WebView provides native headless browser automation with WebKit and Chrome backends, no Playwright or Puppeteer required
  • In-process Bun.cron() adds scheduled callbacks to long-running servers with proper no-overlap semantics and hot-reload safety
  • JavaScriptCore update adds native using/await using for explicit resource management and numerous JIT improvements

Why do I care: Native headless browser automation in the runtime itself is a compelling capability for testing and scraping use cases. The fact that it uses OS-level events means detection is genuinely harder, which matters for testing scenarios where you want to verify real user flows. The explicit resource management support is also finally arriving in a runtime, which should accelerate adoption of the pattern in real code.

Bun v1.3.12


HTML-in-Canvas: Rendering Real DOM Elements Inside canvas

TLDR: WICG has published a proposal and Chromium implementation for rendering arbitrary HTML elements into 2D and 3D canvas contexts, with privacy-preserving restrictions on cross-origin content, currently available behind a Chrome flag.

Summary: The core problem this proposal solves has existed since canvas was introduced: there is no web API to render complex HTML layouts, styled text, or interactive elements into a canvas. Chart libraries draw their own text primitives. Creative tools rebuild text layout from scratch. Games that need rich in-game menus use either a separate DOM overlay or a canvas-native text engine. None of these approaches are great.

The proposal introduces a layoutsubtree attribute on canvas elements that opts child elements into layout participation and hit testing without making them visible to the user. The elements render normally from the layout engine's perspective but their output is invisible until explicitly drawn into the canvas via drawElementImage(). The method returns a CSS transform that can be applied back to the element to keep its DOM position synchronized with its drawn position, which matters for hit testing and accessibility.

The privacy restrictions are significant. Cross-origin content, system colors, visited link information, and pending autofill data are all excluded from the snapshot to prevent timing and readback attacks. Same-origin iframes still paint. Scrollbar and form element appearance (which are already detectable through foreignObject in SVG) are not considered sensitive.

The WebGL and WebGPU equivalents (texElementImage2D and copyElementImageToTexture) enable the HTML rendering in 3D scenes use case: putting styled text and interactive UI onto surfaces in a 3D scene without a separate rendering path.

Key takeaways:

  • The layoutsubtree canvas attribute opts child elements into layout, and drawElementImage() renders them into the canvas context
  • drawElementImage() returns a CSS transform to synchronize the element's DOM position with its drawn position for hit testing and accessibility
  • Privacy restrictions exclude cross-origin content, system colors, and visit history from canvas snapshots

Why do I care: This proposal would eliminate an entire category of workarounds that chart libraries, design tools, and game engines currently maintain. The current best alternative (using html2canvas or similar libraries) is slow, approximate, and breaks on many CSS features. Native browser rendering of HTML into canvas changes the math for a lot of use cases, and the privacy design is thoughtful.

WICG/html-in-canvas