Skip to content
Melact

Benchmarks

Keyed benchmark results

Our local reproduction of js-framework-benchmark's keyed CPU, memory and size tests, run in headless Chrome against hand-written vanilla JS and React 18.3 with hooks. Times are the geometric mean of the slowdown against vanilla JS over nine keyed operations; the ratios move by about 0.1 between runs.

Keyed benchmark, geometric mean

Slowdown against hand-written vanilla JS, nine keyed operations

lower is better
  • Melact×1.17
  • React 18×1.70
› table
frameworkSlowdown against hand-written vanilla JS, nine keyed operations
vanilla JS×1.00
Melact×1.17
React 18×1.70

JavaScript of the benchmark app

gzip size of all JS in the production bundle

smaller is better
  • Melact9.9 kB
  • React 1845.6 kB
› table
frameworkgzip size of all JS in the production bundle
vanilla JS1.4 kB
Melact9.9 kB
React 1845.6 kB

Heap with 1,000 rows

After two forced garbage collections

smaller is better
  • Melact1.64 MB
  • React 183.17 MB
› table
frameworkAfter two forced garbage collections
vanilla JS0.77 MB
Melact1.64 MB
React 183.17 MB

Swap rows

Slowdown against vanilla JS, swapping two of 1,000 rows

lower is better
  • Melact×1.17
  • React 18×5.86
› table
frameworkSlowdown against vanilla JS, swapping two of 1,000 rows
vanilla JS×1.00
Melact×1.17
React 18×5.86

Swapping two rows of 1,000 moves two rows; React moves 998. A list re-renders only the rows whose item changed.

The figure

Where the time goes.

Pale: the work itself. Solid: the overhead on top of it.
Benchmark: within ×1.2 of hand-written JavaScript. js-framework-benchmark's keyed operations in headless Chrome, slowdown against vanilla JS, lower is better: vanilla 1.00, Meltea 1.15, Melact 1.17, React 18 1.70. JS bundle of the benchmark app, gzip: vanilla 1.4 kB, Meltea 9.0, Melact 9.9, React 18 45.6. Heap with 1,000 rows: vanilla 0.77 MB, Meltea 1.22, Melact 1.64, React 18 3.17. Swapping two rows of 1,000: Meltea 1.12, Melact 1.17, React 18 5.86

Apps

Same markup, same stylesheet.

  • vanilla/

    hand-written keyed DOM code in the style of the benchmark's vanillajs-keyed: one cloned <tr> template, event delegation on tbody, manual keyed operations

  • melact/

    reason-react style: useReducer in an App component, a React.memo Row component

  • react/

    React 18.3 with hooks, like the benchmark's react-hooks: useReducer, memo rows, createElement

Method

Measured like the real benchmark.

  • Every sample is a fresh tab: load, the benchmark's warmups for that operation, then one measured click, sent as real mouse input.
  • The measured click is traced. The time is the renderer main thread's busy time from the start of the click dispatch to the end of the first Paint and its Commit after it.
  • CPU slowdown as in the benchmark: 4× for partial update, select, swap and clear, 2× for remove.
  • After every measured click the page is checked: row count, every id in order, every label, the markup of every row, and which rows have class="danger". A fast but wrong result is reported, not counted silently.
  • Memory: HeapProfiler.collectGarbage twice, then Runtime.getHeapUsage, after load, after creating 1,000 rows, and after five more create/clear cycles.
  • Size: the gzip (-9) size of all JS in the production bundle.

Whole apps

Small, because the runtime is made of parts.

Every kind of child carries its own mount and patch code, and every hole its own writer, so a bundle contains only the parts its compiled code names. Whole apps, runtime and the Melange stdlib they use included: the counter is 5.6 kB gzip with Melact; React 18.3 and react-dom alone are 45 kB.