Skip to content

Performance ​

Runtime behavior ​

Twill is a build-time syntax layer. Ordinary trailing closures become native arrow functions and guards become native branches. Those constructs introduce no runtime library. The benchmark checks that representative callback output matches equivalent handwritten JavaScript, and React/Vue component output matches equivalent native JSX after emission.

Single-expression component children are direct JSX values. General child collection uses a local array and ordered pushes; parameterized render props remain lazy callbacks. These have the costs of their emitted JavaScript and the selected framework.

defer uses one callback and native finally for a single direct cleanup. Multiple or control-flow registrations use a lazy block-local stack with reverse-order draining. Both paths allocate cleanup closures; the dynamic path additionally allocates an array. Native try/finally remains useful for allocation-sensitive code. Explicit asynchronous cleanup adds the cost of awaiting cleanup.

Compiler measurements ​

The following synthetic run was recorded on 2026-10-05, Linux x64, Node 24.19.0, an AMD EPYC 9V74 shared execution host. Each stage has five warmups and 15 timed samples. Syntax transformation preserves types; the plugin pipeline also erases types and lowers JSX. Both include high-resolution source maps with embedded source. Values are medians in milliseconds.

Stage10 callbacks100 callbacks1,000 callbacks
Syntax → TypeScript2.19 ms15.00 ms146.44 ms
Syntax → implicit members2.03 ms15.50 ms206.07 ms
Syntax → JSX components2.05 ms10.89 ms124.55 ms
TypeScript plugin pipeline5.02 ms22.05 ms209.55 ms
Component plugin pipeline6.89 ms30.51 ms307.01 ms

Callback source sizes are 599 / 6,089 / 61,889 bytes. Implicit-member fixtures contain twice as many callbacks (20 / 200 / 2,000) and are 839 / 8,489 / 85,889 bytes. Component fixtures contain 10 / 100 / 1,000 nested views and are 679 / 6,889 / 69,889 bytes. These timings exclude process startup, filesystem discovery, project type checking, bundler optimization, rendering and editor UI. They are not whole-application build or runtime measurements. The compiler report includes p95 values, build identity and environment metadata.

Cleanup cost ​

Each cleanup workload invokes a function 10,000 times per batch. The single-direct case registers one cleanup; the dynamic loop registers 1 / 10 / 100 cleanups per invocation. Cleanup adds to observable state. The reference performs the same additions in native finally, using a direct statement or reverse loop without registration closures or a stack. Each implementation has a separate batch call site, result assertions, 50 warmup batches and 15 timed samples.

WorkloadRegistrations per invocationGenerated defer, median batchNative finally, median batchRatio
Single direct statement10.010 ms0.010 ms1.0×
Dynamic loop10.245 ms0.014 ms18.0×
Dynamic loop101.055 ms0.085 ms12.5×
Dynamic loop10019.745 ms0.828 ms23.8×

The single-direct output matches a minimal handwritten callback/finally implementation after ordinary host minification. V8 can optimize this small, warmed-up example; its near-equal timing does not mean every cleanup closure is free. Dynamic registration exposes allocation and dispatch overhead in a tight synchronous loop. These measurements do not establish an application slowdown ratio, real I/O cleanup latency, or results on other engines.

Guards and switch expressions ​

Destructured guards add a temporary and a nullish branch, followed by native destructuring. They add no wrapper object, callback or library. Direct-return switch expressions add a scoped native switch without a function. Other expression positions use one synchronous lexical arrow IIFE; this, arguments and scheduling retain native semantics. Selected object cases destructure the discriminator too, so a discriminator getter is read twice. Rest/default costs remain native destructuring costs.

The branching report measures 1,000,000 calls per sample, 15 samples after warmup, with each native/dialect case in a separate process on Node 24.19 / AMD EPYC 9V74. Isolated processes avoid shared call-site feedback favoring whichever function runs first.

WorkloadTwill medianNative medianMinified function bytes Twill / native
Destructured guard2.49 ms2.50 ms75 / 75
Direct-return value switch2.76 ms2.89 ms77 / 77
Switch in a local initializer2.75 ms2.72 ms95 / 90

The initializer comparison uses a natural native switch assigning a local. V8 can inline the IIFE and optimize these small hot functions differently; timing differences are not a general speedup claim or evidence that closures never allocate. Other engines, cold execution, larger branches and captured values need application measurements. Await/yield inside a switch requires a direct return, avoiding hidden promise conversion or additional async scheduling.

The application bundle report covers five supported paths. Guards, callbacks and a single React child match the native minified byte size. The direct object-pattern switch is 205 bytes versus 203 bytes: its explicit lexical block adds two braces. Tests enforce those exact output budgets and runtime parity. There is no Twill runtime imported into these application fixtures. Dynamic cleanup, general child collection and arbitrary expression switches have separate costs and are outside this parity claim.

Mixed projects and editing ​

The project benchmark uses isolated processes, one-third native TypeScript and two-thirds Twill, plus a module importing every file and a member-callback document. Cold checks include TypeScript standard libraries; warm checks, edited-hover requests and partial-member completions use 15 samples. Values below come from the project report on the same host.

Source filesCold checkWarm check p50 / p95Edited hover p50 / p95Member completion p50 / p95Peak process RSS
102501 ms1.2 / 1.8 ms17.9 / 30.1 ms0.29 / 21.3 ms412 MiB
502726 ms6.0 / 12.5 ms39.4 / 69.7 ms0.27 / 23.8 ms485 MiB
10021020 ms10.3 / 21.2 ms71.0 / 100.6 ms0.38 / 29.3 ms598 MiB

The project stays alive while a 1,000-closure file is formatted. That cold, single-sample operation took 410–434 ms. Peak RSS therefore includes both the formatter and TypeScript; it is not incremental editor overhead. Measurements exclude VS Code rendering, the native TS-server bridge, dependency-heavy applications, HMR and application runtime.

Unchanged editor snapshots, transforms and mapping decoders are cached. The native TS-server bridge applies only changed overlays, retaining unrelated snapshots and script versions. The linter uses line indexes for source mapping rather than parsing additional ASTs. Edits invalidate affected files, while configuration changes rebuild affected projects. Typed ESLint includes program construction and freshness checks; syntactic lint offers lighter feedback. No editor responsiveness SLA follows from these synthetic results.

Application output and distribution budgets ​

Equivalent native and dialect application fixtures are built with real esbuild bundling and minification. An implicit-member filter/map pipeline emits 76 bytes in both forms; a guarded numeric pipeline emits 88 bytes in both forms; a React single-child component emits 121 bytes in both forms. The checks compare byte counts and executed behavior, and reject compiler/runtime dependencies in the application graph. Identifier mangling can choose different short names. React's normal JSX runtime is external in this comparison. These small fixtures do not cover every application, dynamic cleanup or general child collection.

The VSIX ships one pinned TypeScript engine shared on disk by its two editor hosts, standard-library declarations, the checker and the lightweight formatter. The engine still runs in each host process; sharing its distribution does not imply shared process memory. The standalone formatter loads TS/ESTree support rather than Node's automatic parser discovery.

Distributed builds enforce these compressed artifact size limits:

ArtifactMaximum compressed size
Compiler tarball650 KiB
Formatter tarball24 KiB
Linter tarball24 KiB
Highlight tarball24 KiB
Export tarball28 KiB
VSIX, including its engine3 MiB

Tarball budgets cover the package's own files, not installed npm dependencies. Development tools remain outside application bundles. These are distribution limits, not application bundle budgets or build-time guarantees.

React framework source study ​

The React source study compares the real React 19.3.0 client-core entry graph after identical Flow erasure. It adds an independently reproducible framework workload to the small synthetic fixtures above. The report includes production bytes, gzip size, interleaved build samples and isolated core-runtime samples. ReactDOM/reconciler are not rewritten; this is not a full rendering-throughput or typed framework-port benchmark.

Evaluate your application ​

Measure build time, edited-file feedback and representative runtime paths before and after adopting a module. Use the same dependencies, hardware and production build settings. Inspect emitted code and bundle size, and exercise cleanup and error paths; warmed microbenchmarks can hide allocation costs that matter elsewhere.

The reports above include their inputs, sampling methods, environment and build identity. Instructions for reproducing repository benchmarks are in the contributor development guide. Correctness and output budgets are enforced; wall-clock thresholds are not enforced across different runners.