Skip to content

Commit bd54ef4

Browse files
Show Redact feature ranges and lead with TanStack Start (#1423)
* Measure optional Redact feature ranges for Start and Vite * Lead with Start and show measured Redact feature ranges
1 parent d319d82 commit bd54ef4

10 files changed

Lines changed: 275 additions & 114 deletions

File tree

‎launch/benchmarks/redact-frameworks/README.md‎

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,42 @@
22

33
Measured October 25, 2026 with the same counter component, controlled input, and CSS in four single-route apps. Each pair changes only the renderer integration. The landing page uses these measurements, with one shared scale for all eight bars.
44

5-
| Framework | React | Redact | Saved |
6-
| -------------- | -------: | -------: | -------------: |
7-
| TanStack Start | 109.3 KB | 62.3 KB | 47.0 KB, 43.0% |
8-
| Vite | 68.3 KB | 20.1 KB | 48.1 KB, 70.5% |
9-
| Vinext | 138.1 KB | 98.8 KB | 39.3 KB, 28.5% |
10-
| Next.js | 175.6 KB | 139.9 KB | 35.7 KB, 20.4% |
5+
| Framework | React | Redact default | Unused features disabled | Saved by default |
6+
| -------------- | -------: | -------------: | -----------------------: | ---------------: |
7+
| TanStack Start | 109.3 KB | 62.3 KB | 59.8 KB | 47.0 KB, 43.0% |
8+
| Vite | 68.3 KB | 20.1 KB | 12.6 KB | 48.1 KB, 70.5% |
9+
| Vinext | 138.1 KB | 98.8 KB | Not available | 39.3 KB, 28.5% |
10+
| Next.js | 175.6 KB | 139.9 KB | Not available | 35.7 KB, 20.4% |
1111

1212
These are starter app measurements. A larger app has its own routes, dependencies, and chunk layout, so its percentage will differ. The separate [Vinext benchmark](https://github.com/TanStack/redact/blob/main/benchmarks/VINEXT_BENCHMARKS.md) measures a larger workload over six rounds and saves 39.4 KB, or 25.4%.
1313

1414
## What the numbers include
1515

1616
Each file is compressed separately with Node's default `gzipSync`, then the sizes are added. We count all emitted client JavaScript and CSS, including lazy chunks and framework polyfills. KB means 1,000 bytes. These totals are not the download for a single navigation. HTML, inline scripts and Flight payloads, images, source maps, and server output are excluded.
1717

18-
Start and Vite use Redact's default Vite integration. Start sets `ssr.noExternal: true` in both builds. This lets Vite transform React imports inside server dependencies as well as application code. Without it, Node can load an external dependency with native React alongside application code using Redact. Next.js uses `withRedact` with Turbopack. Vinext uses the maintained integration in both modes, with `renderer: 'react'` or `renderer: 'redact'`, so both builds receive the same platform corrections. Next.js and Vinext keep native React on the server. No presets, feature flags, minifier settings, or application code are changed between the two builds of a framework.
18+
Start and Vite use Redact's Vite integration. Start sets `ssr.noExternal: true` in all three builds. This lets Vite transform React imports inside server dependencies as well as application code. Without it, Node can load an external dependency with native React alongside application code using Redact. Vinext uses the maintained integration in both modes, with `renderer: 'react'` or `renderer: 'redact'`, so both builds receive the same platform corrections. Next.js uses `withRedact` with Turbopack. Vinext and Next.js keep native React on the server and use the full Redact runtime, without feature presets. The React and default Redact pairs use default settings. Application code, dependencies, and minifier settings stay the same in every variant.
1919

20-
This is one clean production build per variant, measuring bytes rather than speed. All eight builds were opened in the browser and checked for client startup, a counter update, a controlled input update, and the expected renderer. Start, Vinext, and Next.js were also checked for server-rendered content before JavaScript ran. See [browser checks](./browser-checks.json) and [server checks](./server-checks.json).
20+
This is one clean production build per variant, measuring bytes rather than speed. All ten builds were opened in the browser and checked for client startup, a counter update, a controlled input update, and the expected renderer. All seven Start, Vinext, and Next.js variants were also checked for server-rendered content before JavaScript ran. See [browser checks](./browser-checks.json) and [server checks](./server-checks.json).
21+
22+
## Optional features
23+
24+
The lighter green segment shows the bytes removed by disabling features this demo does not use. The lower endpoints are specific to this app. The headline savings compare the default Redact build with React.
25+
26+
Start disables Activity, Fragment refs, and portals. Hydration, context, Suspense, memoization, forwarded refs, lazy components, and class components, including error boundaries, stay enabled:
27+
28+
```ts
29+
redact({
30+
features: { activity: false, fragmentRefs: false, portal: false },
31+
})
32+
```
33+
34+
The client-rendered Vite counter needs none of the optional features, so its smaller build uses:
35+
36+
```ts
37+
redact({ preset: 'nano' })
38+
```
39+
40+
Keep the features your application and dependencies use. Both configurations are recorded in `results.json` and reproduced by `measure.mjs`. Bar labels round up to whole KB; bar lengths use exact bytes.
2141

2242
## Versions and source
2343

@@ -40,4 +60,4 @@ node /path/to/measure.mjs "$PWD"
4060
node /path/to/serve.mjs "$PWD"
4161
```
4262

43-
`measure.mjs` generates every fixture from source, builds React then Redact for each framework, records `results.json`, and preserves all eight builds. `serve.mjs` opens local production previews on ports 4380 through 4387, in the order Start, Vite, Vinext, and Next.js, React first in each pair. The small Node HTTP bridge for Start and Vite is only a preview harness and is not part of the measured output.
63+
`measure.mjs` generates every fixture from source, builds React and default Redact for each framework, adds the smaller Start and Vite builds, records `results.json`, and preserves all ten builds. `serve.mjs` opens local production previews on ports 4380 through 4389, in the order Start, Vite, Vinext, and Next.js. Each framework serves React, default Redact, then its smaller build when available. The small Node HTTP bridge for Start and Vite is only a preview harness and is not part of the measured output.

‎launch/benchmarks/redact-frameworks/browser-checks.json‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,17 @@
2323
"renderer identity"
2424
]
2525
},
26+
{
27+
"framework": "Start",
28+
"renderer": "redact-lean",
29+
"passed": true,
30+
"checks": [
31+
"hydration",
32+
"counter update",
33+
"controlled input",
34+
"renderer identity"
35+
]
36+
},
2637
{
2738
"framework": "Vite",
2839
"renderer": "react",
@@ -45,6 +56,17 @@
4556
"renderer identity"
4657
]
4758
},
59+
{
60+
"framework": "Vite",
61+
"renderer": "redact-lean",
62+
"passed": true,
63+
"checks": [
64+
"client mount",
65+
"counter update",
66+
"controlled input",
67+
"renderer identity"
68+
]
69+
},
4870
{
4971
"framework": "Vinext",
5072
"renderer": "react",

‎launch/benchmarks/redact-frameworks/measure.mjs‎

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@ import { gzipSync } from 'node:zlib'
99
// Run inside an isolated directory with the pinned dependencies installed.
1010
const root = resolve(process.argv[2])
1111
const hash = (bytes) => createHash('sha256').update(bytes).digest('hex')
12+
const leanOptions = {
13+
Start: {
14+
features: { activity: false, fragmentRefs: false, portal: false },
15+
},
16+
Vite: { preset: 'nano' },
17+
}
1218
const counter = `'use client'
1319
import { useEffect, useState } from 'react'
1420
@@ -50,8 +56,9 @@ const report = {
5056
redactTarballSha256: hash(await readFile(join(root, 'redact.tgz'))),
5157
counterSha256: hash(counter),
5258
cssSha256: hash(css),
59+
leanOptions,
5360
method:
54-
'Same single-route counter and controlled input in all four frameworks. Production client builds with default optimization settings. Start bundles its server dependencies through Vite in both variants so React imports inside dependencies are transformed too. Sum of gzipSync bytes for every emitted client .js and .css file, compressed separately. Includes lazy chunks and framework polyfills. Excludes server output, HTML, inline scripts, maps, and images. Each chart compares the same framework and dependencies, changing only its Redact integration. KB means 1,000 bytes. These are starter apps, not a promise for every app or a browser speed measurement.',
61+
'Same single-route counter and controlled input in all four frameworks. Production client builds with default optimization settings. Start bundles its server dependencies through Vite in every variant so React imports inside dependencies are transformed too. Sum of gzipSync bytes for every emitted client .js and .css file, compressed separately. Includes lazy chunks and framework polyfills. Excludes server output, HTML, inline scripts, maps, and images. Each React/default pair changes only its Redact integration. The extra Start and Vite builds disable unused features with the recorded leanOptions. KB means 1,000 bytes. These are starter apps, not a promise for every app or a browser speed measurement.',
5562
results: [],
5663
}
5764

@@ -129,7 +136,7 @@ import react from '@vitejs/plugin-react'
129136
import { redact } from '@tanstack/redact/vite'
130137
export default defineConfig({
131138
ssr: { noExternal: true },
132-
plugins: [tanstackStart(), react(), ...(process.env.RENDERER === 'redact' ? [redact()] : [])],
139+
plugins: [tanstackStart(), react(), ...(process.env.RENDERER !== 'react' ? [redact(process.env.RENDERER === 'redact-lean' ? ${JSON.stringify(leanOptions.Start)} : {})] : [])],
133140
})
134141
`,
135142
)
@@ -167,7 +174,7 @@ export const Route = createFileRoute('/')({ component: Counter })
167174
`import { defineConfig } from 'vite'
168175
import react from '@vitejs/plugin-react'
169176
import { redact } from '@tanstack/redact/vite'
170-
export default defineConfig({ plugins: [react(), ...(process.env.RENDERER === 'redact' ? [redact()] : [])] })
177+
export default defineConfig({ plugins: [react(), ...(process.env.RENDERER !== 'react' ? [redact(process.env.RENDERER === 'redact-lean' ? ${JSON.stringify(leanOptions.Vite)} : {})] : [])] })
171178
`,
172179
)
173180
await write(
@@ -222,7 +229,11 @@ export default process.env.RENDERER === 'redact' ? withRedact(config) : config
222229
}
223230
}
224231

225-
for (const renderer of ['react', 'redact']) {
232+
for (const renderer of [
233+
'react',
234+
'redact',
235+
...(framework in leanOptions ? ['redact-lean'] : []),
236+
]) {
226237
console.log(`Building ${framework} + ${renderer}`)
227238
await run(
228239
app,
@@ -252,13 +263,21 @@ export default process.env.RENDERER === 'redact' ? withRedact(config) : config
252263
console.log(
253264
`${framework} + ${renderer}: ${result.gzipBytes} gzip bytes across ${emitted.length} files`,
254265
)
255-
// Preserve both complete builds for browser verification after measurement.
266+
// Preserve every complete build for browser verification after measurement.
256267
if (framework !== 'Next.js') {
257268
const { rename } = await import('node:fs/promises')
258269
await rename(join(app, 'dist'), join(app, `dist-${renderer}`))
259270
}
260271
}
261272
}
273+
for (const framework of Object.keys(leanOptions)) {
274+
const variants = report.results.filter((item) => item.framework === framework)
275+
assert(
276+
variants.find((item) => item.renderer === 'redact-lean').gzipBytes <
277+
variants.find((item) => item.renderer === 'redact').gzipBytes,
278+
`${framework}: feature selection did not reduce output`,
279+
)
280+
}
262281
await writeFile(
263282
join(root, 'results.json'),
264283
JSON.stringify(report, null, 2) + '\n',

‎launch/benchmarks/redact-frameworks/results.json‎

Lines changed: 78 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"recordedAt": "2026-10-25T17:15:08.787Z",
2+
"recordedAt": "2026-10-25T19:55:07.317Z",
33
"node": "v24.22.0",
44
"zlib": "1.3.2.1-motley-285e94b",
55
"packageVersions": {
@@ -19,7 +19,19 @@
1919
"redactTarballSha256": "6f8592d92061ab3845b11a5635a2853ec5ba61b600f7ca213cfa42b510ec1099",
2020
"counterSha256": "642b7bf7693281301e2d3b42826b679b40f03cd723be41b0aaea9fa190727ca1",
2121
"cssSha256": "bed31695998ef8641c353a5f356dbadc243b4669edf6fc631fe7b647c1674643",
22-
"method": "Same single-route counter and controlled input in all four frameworks. Production client builds with default optimization settings. Start bundles its server dependencies through Vite in both variants so React imports inside dependencies are transformed too. Sum of gzipSync bytes for every emitted client .js and .css file, compressed separately. Includes lazy chunks and framework polyfills. Excludes server output, HTML, inline scripts, maps, and images. Each chart compares the same framework and dependencies, changing only its Redact integration. KB means 1,000 bytes. These are starter apps, not a promise for every app or a browser speed measurement.",
22+
"leanOptions": {
23+
"Start": {
24+
"features": {
25+
"activity": false,
26+
"fragmentRefs": false,
27+
"portal": false
28+
}
29+
},
30+
"Vite": {
31+
"preset": "nano"
32+
}
33+
},
34+
"method": "Same single-route counter and controlled input in all four frameworks. Production client builds with default optimization settings. Start bundles its server dependencies through Vite in every variant so React imports inside dependencies are transformed too. Sum of gzipSync bytes for every emitted client .js and .css file, compressed separately. Includes lazy chunks and framework polyfills. Excludes server output, HTML, inline scripts, maps, and images. Each React/default pair changes only its Redact integration. The extra Start and Vite builds disable unused features with the recorded leanOptions. KB means 1,000 bytes. These are starter apps, not a promise for every app or a browser speed measurement.",
2335
"results": [
2436
{
2537
"framework": "Start",
@@ -71,6 +83,31 @@
7183
}
7284
]
7385
},
86+
{
87+
"framework": "Start",
88+
"renderer": "redact-lean",
89+
"gzipBytes": 59778,
90+
"files": [
91+
{
92+
"path": "assets/index-8PKgnleu.js",
93+
"bytes": 167389,
94+
"gzipBytes": 59218,
95+
"sha256": "ee09d42966041a2063e128b854b3485e87389f01db7c510bd62bc547e98eeb01"
96+
},
97+
{
98+
"path": "assets/index-BrYQuRTz.css",
99+
"bytes": 241,
100+
"gzipBytes": 189,
101+
"sha256": "62ea65329a5844dfb94c95ad3d0bb666da2e998f50a88229d86a72d5b72390d4"
102+
},
103+
{
104+
"path": "assets/routes-ieaXzjDW.js",
105+
"bytes": 557,
106+
"gzipBytes": 371,
107+
"sha256": "a055170457c5832e0862d760e476f31e23643c8a644f3fe3c5e64019923f4778"
108+
}
109+
]
110+
},
74111
{
75112
"framework": "Vite",
76113
"renderer": "react",
@@ -109,19 +146,38 @@
109146
}
110147
]
111148
},
149+
{
150+
"framework": "Vite",
151+
"renderer": "redact-lean",
152+
"gzipBytes": 12590,
153+
"files": [
154+
{
155+
"path": "assets/index-BrYQuRTz.css",
156+
"bytes": 241,
157+
"gzipBytes": 189,
158+
"sha256": "62ea65329a5844dfb94c95ad3d0bb666da2e998f50a88229d86a72d5b72390d4"
159+
},
160+
{
161+
"path": "assets/index-CmDjlZH7.js",
162+
"bytes": 30414,
163+
"gzipBytes": 12401,
164+
"sha256": "c06a9aab10584753ef3dd73ec969b9cda75ceda320ad4aa7610e0bbec32ae3cd"
165+
}
166+
]
167+
},
112168
{
113169
"framework": "Vinext",
114170
"renderer": "react",
115171
"gzipBytes": 138094,
116172
"files": [
117173
{
118-
"path": "_next/static/95aa7be8-9659-4ff0-9197-de663bea07f7/_buildManifest.js",
174+
"path": "_next/static/bb64c4d8-f7ce-4124-a895-fda8315b989e/_buildManifest.js",
119175
"bytes": 176,
120176
"gzipBytes": 139,
121177
"sha256": "4cce301fe69e28d6463904ecf618427e95091bf3584ea347d8ca55d1077e1fb6"
122178
},
123179
{
124-
"path": "_next/static/95aa7be8-9659-4ff0-9197-de663bea07f7/_ssgManifest.js",
180+
"path": "_next/static/bb64c4d8-f7ce-4124-a895-fda8315b989e/_ssgManifest.js",
125181
"bytes": 76,
126182
"gzipBytes": 60,
127183
"sha256": "653f3e53e89b4f8548ff86c19e92bb3c6b84b6be7485a320b1e00893ed877479"
@@ -181,6 +237,18 @@
181237
"renderer": "redact",
182238
"gzipBytes": 98779,
183239
"files": [
240+
{
241+
"path": "_next/static/11ede8c5-db42-4363-bbd5-eee230bdcf64/_buildManifest.js",
242+
"bytes": 176,
243+
"gzipBytes": 139,
244+
"sha256": "4cce301fe69e28d6463904ecf618427e95091bf3584ea347d8ca55d1077e1fb6"
245+
},
246+
{
247+
"path": "_next/static/11ede8c5-db42-4363-bbd5-eee230bdcf64/_ssgManifest.js",
248+
"bytes": 76,
249+
"gzipBytes": 60,
250+
"sha256": "653f3e53e89b4f8548ff86c19e92bb3c6b84b6be7485a320b1e00893ed877479"
251+
},
184252
{
185253
"path": "_next/static/chunks/BWaJ3C7D.js",
186254
"bytes": 911,
@@ -222,18 +290,6 @@
222290
"bytes": 241,
223291
"gzipBytes": 189,
224292
"sha256": "62ea65329a5844dfb94c95ad3d0bb666da2e998f50a88229d86a72d5b72390d4"
225-
},
226-
{
227-
"path": "_next/static/d4219925-835c-4b0a-8dbd-ce21fce7e7e1/_buildManifest.js",
228-
"bytes": 176,
229-
"gzipBytes": 139,
230-
"sha256": "4cce301fe69e28d6463904ecf618427e95091bf3584ea347d8ca55d1077e1fb6"
231-
},
232-
{
233-
"path": "_next/static/d4219925-835c-4b0a-8dbd-ce21fce7e7e1/_ssgManifest.js",
234-
"bytes": 76,
235-
"gzipBytes": 60,
236-
"sha256": "653f3e53e89b4f8548ff86c19e92bb3c6b84b6be7485a320b1e00893ed877479"
237293
}
238294
]
239295
},
@@ -291,19 +347,19 @@
291347
"sha256": "8bbb58cbfd4633c1d828c6bc37bc32abc768a149c01b1dfc4f5131fd81274551"
292348
},
293349
{
294-
"path": "NdnK0xH5LMQwOKWJEoBdU/_buildManifest.js",
350+
"path": "EZN1vWdZRBGVR-raIzoF5/_buildManifest.js",
295351
"bytes": 219,
296352
"gzipBytes": 154,
297353
"sha256": "9476e0afd3f9da68a64e6ec58631191fd3eaca62c9f5a377e7517fe9a89e59ea"
298354
},
299355
{
300-
"path": "NdnK0xH5LMQwOKWJEoBdU/_clientMiddlewareManifest.js",
356+
"path": "EZN1vWdZRBGVR-raIzoF5/_clientMiddlewareManifest.js",
301357
"bytes": 96,
302358
"gzipBytes": 66,
303359
"sha256": "5483e3295dc35dfe29ea932efc85341c916f10ff78ff5b0e965e003710893a4a"
304360
},
305361
{
306-
"path": "NdnK0xH5LMQwOKWJEoBdU/_ssgManifest.js",
362+
"path": "EZN1vWdZRBGVR-raIzoF5/_ssgManifest.js",
307363
"bytes": 80,
308364
"gzipBytes": 64,
309365
"sha256": "678f6ce2cb80b1fe72fc67e7412be6e2ab6ada083111b64f7c40d35e3cba5e00"
@@ -364,19 +420,19 @@
364420
"sha256": "8bbb58cbfd4633c1d828c6bc37bc32abc768a149c01b1dfc4f5131fd81274551"
365421
},
366422
{
367-
"path": "I5aaZFtg_xGKQHYGMZiI5/_buildManifest.js",
423+
"path": "ihKICl1cD6PpEayME0Kbi/_buildManifest.js",
368424
"bytes": 219,
369425
"gzipBytes": 154,
370426
"sha256": "9476e0afd3f9da68a64e6ec58631191fd3eaca62c9f5a377e7517fe9a89e59ea"
371427
},
372428
{
373-
"path": "I5aaZFtg_xGKQHYGMZiI5/_clientMiddlewareManifest.js",
429+
"path": "ihKICl1cD6PpEayME0Kbi/_clientMiddlewareManifest.js",
374430
"bytes": 96,
375431
"gzipBytes": 66,
376432
"sha256": "5483e3295dc35dfe29ea932efc85341c916f10ff78ff5b0e965e003710893a4a"
377433
},
378434
{
379-
"path": "I5aaZFtg_xGKQHYGMZiI5/_ssgManifest.js",
435+
"path": "ihKICl1cD6PpEayME0Kbi/_ssgManifest.js",
380436
"bytes": 80,
381437
"gzipBytes": 64,
382438
"sha256": "678f6ce2cb80b1fe72fc67e7412be6e2ab6ada083111b64f7c40d35e3cba5e00"

‎launch/benchmarks/redact-frameworks/serve.mjs‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,11 @@ if (!framework) {
1414
const children = []
1515
let nextPort = 4380
1616
for (const name of ['start', 'vite', 'vinext', 'next']) {
17-
for (const runtime of ['react', 'redact']) {
17+
for (const runtime of [
18+
'react',
19+
'redact',
20+
...(['start', 'vite'].includes(name) ? ['redact-lean'] : []),
21+
]) {
1822
const serverPort = nextPort++
1923
children.push(
2024
spawn(

‎launch/benchmarks/redact-frameworks/server-checks.json‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,13 @@
1414
"passed": true,
1515
"checks": ["server-rendered greeting", "counter", "controlled input"]
1616
},
17+
{
18+
"framework": "Start",
19+
"renderer": "redact-lean",
20+
"status": 200,
21+
"passed": true,
22+
"checks": ["server-rendered greeting", "counter", "controlled input"]
23+
},
1724
{
1825
"framework": "Vinext",
1926
"renderer": "react",

0 commit comments

Comments
 (0)