Skip to content
Vincle

Loading…

    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/html without safe does 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/jsx inside 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>&lt;img src=x onerror="alert(1)"&gt;</div>
    react-dom/server <div>&lt;img src=x onerror=&quot;…&quot;&gt;</div>
    preact-render-to-string <div>&lt;img src=x onerror=&quot;…&quot;></div>
    hono/jsx <div>&lt;img src=x onerror=&quot;…&quot;&gt;</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 error
    hono/jsx throws "No default value"
    react-dom throws React error #426

    Preact'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#

    Terminal window
    bun run --filter=@vincle/bench bench:stats -- --runs 8

    It 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.