# Editor engine benchmark

Generated 2026-08-27T18:11:52.264Z on Apple M4.

## Scope

- Plain React editor: a hand-written state and history reference with no schema, drag-and-drop, or plugin system.
- Plain React viewer: the mixed document rendered directly with React and no document framework.
- Lexical: one rich-text editor with React rich-text, history, and change plugins.
- Widget-first: a mixed page of rich text, callouts, metrics, and galleries, with Lexical loaded only after the first widget enters edit mode.
- Read-only widgets: a mixed widget document rendered without editor state, history, subscriptions, or update callbacks.
- Puck: its standard React visual editor with fields, component selection, history, and drag-and-drop.
- Puck Render: the same mixed page through Puck's published-page renderer.
- BlockNote: its standard React text-block editor and default user interface.
- Craft.js: a React page-editor tree with selection, serialized state, and history.
- GrapesJS: its canvas, component model, selection, rich-text editing, and undo manager.
- Sizes: 25, 100, 500 paragraphs or blocks. 5 cold and 5 warm samples at CPU rates 1x, 4x.

These cases are product architectures, not feature-identical editor cores. Raw data records the cost of those choices.

## Production payload

| Engine | Initial KiB | Initial gzip KiB | Initial brotli KiB | Async JS gzip KiB |
| --- | ---: | ---: | ---: | ---: |
| plain | 193.4 | 61.2 | 52.6 | 0.0 |
| plain-viewer | 192.5 | 60.9 | 52.2 | 0.0 |
| lexical | 416.8 | 133.5 | 113.8 | 0.0 |
| widgets | 215.2 | 68.1 | 58.8 | 100.9 |
| viewer | 200.6 | 63.5 | 54.7 | 0.0 |
| puck | 863.3 | 242.4 | 199.4 | 119.7 |
| puck-viewer | 579.9 | 188.3 | 158.9 | 120.4 |
| blocknote | 1116.9 | 338.0 | 282.9 | 108.0 |
| craft | 285.0 | 90.4 | 77.8 | 0.0 |
| grapesjs | 1189.2 | 312.4 | 255.8 | 0.0 |

## 100 block cold runtime

All values are p50 at native CPU speed. Lower is better. Input and structural-edit cells are `n/a` for read-only cases.

| Engine | Ready ms | Idle heap MiB | DOM elements | Type 20 chars ms | Change payload bytes | Structure CPU ms | Serialize CPU ms |
| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
| plain | 41.1 | 1.76 | 565 | 11.7 | 17619 | 0.0 | 0.0 |
| plain-viewer | 45.9 | 1.76 | 565 | n/a | n/a | n/a | 0.0 |
| lexical | 46.0 | 2.36 | 215 | 10.0 | 32209 | 0.3 | 0.2 |
| widgets | 47.0 | 2.23 | 665 | 8.9 | 2131 | 0.8 | 0.2 |
| viewer | 46.6 | 1.95 | 665 | n/a | n/a | n/a | 0.1 |
| puck | 268.1 | 10.90 | 907 | 69.7 | 19542 | 6.4 | 0.0 |
| puck-viewer | 62.8 | 2.84 | 566 | n/a | n/a | n/a | 0.1 |
| blocknote | 78.1 | 4.10 | 421 | 42.5 | 706 | 2.2 | 0.1 |
| craft | 74.7 | 4.54 | 565 | 37.2 | 34440 | 1.7 | 0.1 |
| grapesjs | 124.5 | 13.22 | 132 | 6.6 | 43017 | 3.2 | 7.6 |

Headless Widgets records its patch batch for change payload bytes. BlockNote records its changed-block batch. Plain React, Lexical, Puck, Craft.js, and GrapesJS record their normal full-state callback payload. GrapesJS renders its canvas inside an iframe, so its host-page DOM count excludes the canvas document and must not be treated as an apples-to-apples DOM-size result.

## Findings

- Headless Widgets ships 68.1 KiB of initial gzip payload, 11% above the hand-written React editor and smaller than Lexical (133.5 KiB), Craft.js (90.4 KiB), Puck (242.4 KiB), GrapesJS (312.4 KiB), and BlockNote (338.0 KiB). Activating rich text loads another 100.9 KiB asynchronously.
- The read-only widget renderer ships 63.5 KiB initial gzip, close to direct React at 60.9 KiB and 2.97x smaller than Puck Render at 188.3 KiB.
- At 100 mixed blocks on the native CPU, Headless Widgets reached ready in 47.0 ms p50 and processed the 20-character input in 8.9 ms p50. Puck measured 268.1/69.7 ms, BlockNote 78.1/42.5 ms, Craft.js 74.7/37.2 ms, and GrapesJS 124.5/6.6 ms. GrapesJS wins this narrow direct-input timer, while Headless Widgets has the lowest startup time among the page-building systems.
- The Headless Widgets input emitted a 2,131-byte patch batch at both 100 and 500 blocks. BlockNote's text-only changed-block batch was smaller at 706 bytes. Full-state callbacks grew with the document: at 500 blocks they were 88,859 bytes for plain React, 160,609 for Lexical, 98,302 for Puck, 172,360 for Craft.js, and 214,177 for GrapesJS.
- At 500 blocks under 4x CPU slowdown, Headless Widgets measured 171.8 ms ready, 76.7 ms input, 2.6 ms structural CPU, and 59 ms of long tasks. Puck measured 1,158.8/785.0/111.1/3,211 ms; BlockNote 352.7/195.2/39.0/288 ms; Craft.js 1,287.7/250.1/28.0/5,407 ms; and GrapesJS 1,181.1/36.7/16.1/1,743 ms.
- The text-native references remain important: at 500 blocks and 4x CPU, Lexical was ready in 145.1 ms and typed in 59.8 ms, while plain React was ready in 137.6 ms and typed in 136.0 ms. They do not provide the same mixed page-building model.
- Tail latency remains visible. At native CPU, Headless Widgets' input p95 was 32.0 ms at 100 blocks and 29.4 ms at 500 blocks, versus p50 values of 8.9 and 10.1 ms. Under 4x slowdown the 500-block result tightened to 76.7 ms p50 and 78.6 ms p95.

## Artifacts

- `raw.json`: every browser sample and bundle file.
- `summary.json`: p50, p95, minimum, and maximum by engine, size, cache state, and CPU rate.

Check p95, maxima, long tasks, and failed-run fields before citing a result.
