Comparison
Every performance number on this page comes from a benchmark in this repository. The behavior checks have executable tests alongside the benchmark.
The tree-rendering performance snapshot below was recorded on 16 September
2026 on Linux with an AMD Ryzen 7 PRO 8840HS and Bun 1.4.2,
@kitajs/html 5.0.0-next.1,
preact-render-to-string 6.7.0, react-dom 19.3.0 and hono 4.13.8.
Where Vincle fits#
Vincle is a strong fit for server-rendered HTML when you want escaped output by default, components that can directly await data, and no client-side runtime. The measurements below put its tree renderer ahead of Preact, React and Hono on three page shapes, but behind kitajs. Precompilation changes that comparison when both sides escape the same content.
Choose something else when:
- You render to the DOM, not to a string. Vincle has no client runtime, no hydration and no reconciler, and none is planned. Preact or React.
- You need every last microsecond and control your inputs.
@kitajs/htmlwithoutsafedoes less, and on Node it stays ahead of Vincle's precompile path for it. On Bun the two are level, and Vincle is escaping. With their TypeScript plugin wired into CI, either way is a defensible position. - You are already inside a framework's rendering pipeline.
hono/jsxinside a Hono app costs you nothing to keep.
There is no useful single score across speed, security defaults, API features and ecosystem fit; the right choice depends on which of these your application needs.
Escaping is not a setting#
The most consequential difference between these libraries has nothing to do with
speed. Given the string <img src=x onerror="alert(1)"> as a text child:
| Renderer | Output |
|---|---|
@vincle/core |
<div><img src=x onerror="alert(1)"></div> |
react-dom/server |
<div><img src=x onerror="…"></div> |
preact-render-to-string |
<div><img src=x onerror="…"></div> |
hono/jsx |
<div><img src=x onerror="…"></div> |
@kitajs/html |
<div><img src=x onerror="alert(1)"></div> |
@kitajs/html does not escape text children unless you ask it to, with a safe
attribute or its escape helper. This is deliberate on their part, and they
ship @kitajs/ts-html-plugin, an
editor plugin and CLI, to catch unescaped interpolations before they reach
production.
Vincle takes the other side of that trade: escaping cannot be turned off from the component API. The one way to emit unescaped markup is named after what it does:
import { raw } from "@vincle/core";
<div>{userInput}</div> {/* always escaped */}<div>{raw(trustedHtml)}</div> {/* the only escape hatch, and it is greppable */}Which is the better design is a real argument. What this page settles is that a benchmark comparing the two defaults is not comparing the same work: see below.
Attribute names are a list, not a rule#
SVG mixes two conventions in one namespace: viewBox and preserveAspectRatio
are camelCase in the specification, stroke-width and clip-path are
hyphenated. No transformation separates them: only a list does. Given
<svg viewBox="0 0 24 24" strokeWidth={2} clipPath="url(#a)" />:
| Renderer | Output |
|---|---|
@vincle/core |
<svg viewBox="0 0 24 24" stroke-width="2" clip-path="url(#a)"></svg> |
react-dom/server |
<svg viewBox="0 0 24 24" stroke-width="2" clip-path="url(#a)"></svg> |
hono/jsx |
<svg viewBox="0 0 24 24" stroke-width="2" clip-path="url(#a)"></svg> |
preact-render-to-string |
<svg viewBox="0 0 24 24" strokeWidth="2" clipPath="url(#a)"></svg> |
@kitajs/html |
<svg viewBox="0 0 24 24" strokeWidth="2" clipPath="url(#a)"></svg> |
Neither convention is a defect. @kitajs/html and Preact pass attribute names
through, so you write the name the markup carries. Vincle, React and Hono accept
the camelCase spelling and map it.
Mixing the two fails silently. The browser reads strokeWidth as strokewidth,
which is not an SVG attribute, so the stroke keeps its default width: no error,
no warning, and @kitajs/html's types accept both spellings. JSX written against
@types/react, icon sets included, uses camelCase throughout.
Performance#
Each competitor is measured in the same process as Vincle. Each ratio below is the mean of 8 within-process throughput ratios; ± is their sample standard deviation, not a confidence interval. The values describe this workload and harness, not every application.
text renders 1,000 repeated text blocks, stack renders ten trees 1,000
elements deep, and realworld renders a full page with 1,000 purchases. Each
measurement includes element creation and HTML rendering; the purchase data is
created before timing.
Higher is better for Vincle. Below 1 means Vincle is slower.
| Case | vs @kitajs/html |
vs preact |
vs react |
vs hono/jsx |
|---|---|---|---|---|
text |
0.60× ± 0.09 | 1.82× ± 0.07 | 6.44× ± 0.62 | 7.78× ± 0.41 |
stack |
0.48× ± 0.01 | 2.67× ± 0.07 | 2.45× ± 0.07 | 4.47× ± 0.09 |
realworld |
0.73× ± 0.01 | 1.83× ± 0.06 | 2.17× ± 0.06 | 3.79× ± 0.08 |
Against React, Preact and Hono the comparison covers similar work: all three escape by default and build a tree from the same page shapes. Vincle is between 1.8× and 7.8× faster on these three cases.
Throughput and time per page#
The realworld case gives the scale behind those ratios: the mean of 8
fresh-process results, ± the sample standard deviation. Time per page is
derived from throughput, not timed separately.
| Renderer | Pages/s | Time/page |
|---|---|---|
@kitajs/html |
2,503 ± 38 | 0.400 ms |
@vincle/core (tree walk) |
1,834 ± 39 | 0.545 ms |
preact-render-to-string |
1,004 ± 30 | 0.996 ms |
react-dom/server |
844 ± 18 | 1.185 ms |
hono/jsx |
484 ± 12 | 2.066 ms |
These are in-process rendering costs, not HTTP request latency. Absolute times depend on hardware; the ratios above compare better across machines.
Reading the kitajs column#
Two differences compound in that column.
It renders a different string. realworld contains no character needing an
escape, so there 0.73× is a fair number. On any input carrying & or <, Vincle
is doing work kitajs is not.
It uses a different architecture. @kitajs/html returns a string
immediately; Vincle builds a tree, then walks it. Comparing them measures the
tree, not the renderer.
Vincle has a path with no tree: the precompile transform emits templates the way kitajs emits concatenations. Measured on the same 100-row list, with byte-identical output, over 7 fresh processes on each engine:
| Renderer | Bun / JSC | Node / V8 | |
|---|---|---|---|
@vincle/core (precompile) |
9.19 µs | 12.44 µs | escapes |
@kitajs/html (safe) |
12.92 µs | 18.06 µs | escapes → Vincle 1.40× / 1.45× faster |
@kitajs/html (default) |
9.21 µs | 11.09 µs | does not escape |
So: on the tree-walking path Vincle is slower than kitajs, and on the comparable path it is faster. Both are true, and neither on its own is the whole picture.
Capabilities and runtime cost#
| Criterion | Vincle | Kita | Preact | Hono | React |
|---|---|---|---|---|---|
| Escapes a text child by default | yes | no | yes | yes | yes |
Neutralizes javascript: in href |
yes | no | no | no | yes |
Component returning a Promise in the string API |
yes | yes | no | no | no |
| Context API | provider + execution scope | none | provider | provider | provider |
| Client DOM rendering / hydration | no | no | yes | yes | yes |
| Direct dependencies of SSR package | 0 | 1 | 0¹ | 0 | 1² |
The first three rows are behavior checks on specific inputs, not a general
security or API certification. For the URL row, the input is
<a href="javascript:alert(1)">x</a>: Vincle emits href="#blocked"; React
substitutes a URL that throws; Kita, Preact and Hono emit the supplied URL. The
dependency row counts direct dependencies from the package manifests, not
package size or the transitive install tree.
For the async row, rendering <div><Async /></div> where
Async returns Promise.resolve("hello") gives:
@vincle/core <div>hello</div>@kitajs/html <div>hello</div>preact <div></div> ← content dropped, no errorhono/jsx throws "No default value"react-dom throws React error #426Preact's renderToStringAsync does not change that result: its async support is
for components that throw a promise, not ones that return one. React and Hono
both support async rendering through their streaming entry points
(renderToReadableStream, hono/jsx/streaming): the row is about the plain
string API.
¹ preact-render-to-string declares no direct dependency but requires preact
as a peer, so SSR uses two packages.
² react-dom depends on scheduler and requires react as a peer.
Vincle's createContext scopes a value to a subtree, as in Preact, Hono and
React; its separate Scope stores mutable per-execution state that later
siblings can read. Both stay isolated across concurrent renders. See
Context & Scope. @kitajs/html documents
why it has no context API.
Beyond the plain string API: streaming and deferred fragments live in
@vincle/flow, a separate install; Kita, React and
Hono stream too, each with a different component contract. The
@vincle/precompile transform works with Vincle,
Preact and Hono, not React.
Reproducing this#
bun run --filter=@vincle/bench bench:stats -- --runs 8It prints the throughput and ratio tables above. A single run is not a
measurement: the protocol, and what counts as a real delta, are in
apps/bench/README.md. The like-for-like kitajs measurement is
apps/bench/src/ab-precompile-kita.js.