Skip to content
Vincle

Loading…

    Precompile transform

    @vincle/precompile transforms JSX at build time. Instead of emitting jsx() calls that create intermediate objects, it rewrites JSX into tagged template literals: fewer allocations, less GC pressure, faster rendering.

    // Before: runtime creates a VNode tree
    jsx("div", { class: cls }, jsx("span", {}, text));
    // After (precompile): direct string assembly
    jsxTemplate`<div ${jsxAttr("class", cls)}><span>${jsxEscape(text)}</span></div>`;

    It is a port of Deno's precompile, available on any JS runtime (Node, Bun, Deno, Edge, browser) through a Vite plugin, a Bun plugin, or the transform on its own. The generated code calls three helpers, jsxTemplate, jsxAttr and jsxEscape, and nothing else: any Deno-precompile-compatible runtime provides them.

    Framework jsxImportSource Compatible
    Vincle @vincle/core ✅
    Preact preact ✅
    Hono hono/jsx ✅
    React react ❌

    React does not export jsxTemplate: if your project targets React, the plugin throws a build error.

    What it outputs#

    There is nothing to configure: the runtime decides the output.

    • Targeting @vincle/core, precompiled or not, the page is the same bytes as the runtime renders, and static attributes are sanitized at build time. Components in template holes render in the same order too: left to right, each finishing before the next starts (see renderToString).
    • Targeting any other runtime, the output reproduces Deno's jsx: "precompile", defects included: static attributes are inlined unfiltered.

    What it buys#

    On a 100-row list mixing literal markup (<li class="item">) and holes, the precompiled path renders about 1.5× faster than jsx(). It saves what it can inline: a page of mostly literal markup gains more than a table where every value comes from data.

    It pays per render repeated. A statically generated page is rendered once: precompile is for a server answering requests, not for a build that writes files.

    Usage#

    Vite#

    import precompile from "@vincle/precompile/vite";
    import { defineConfig } from "vite";
    export default defineConfig({
    plugins: [precompile()],
    });

    No adapter file, no extra config: the plugin detects the runtime from your jsxImportSource and wires the helpers through a virtual module (virtual:vincle-precompile-runtime).

    Bun#

    A page rendered by an SSG, or by a server that imports its own modules, never goes through Vite, whatever the config says. On Bun, load the transform from a preload script instead:

    preload.ts
    import { plugin } from "bun";
    import precompile from "@vincle/precompile/bun";
    plugin(precompile());
    Terminal window
    bun --preload ./preload.ts server.ts

    There is no virtual module here: the helpers are imported from runtimeSource (@vincle/core/jsx-runtime unless you set another).

    Any other pipeline#

    Call the transform itself, which is what both plugins do:

    import precompileTransform from "@vincle/precompile";
    const result = precompileTransform(code, "/src/App.tsx", {
    runtimeSource: "@vincle/core/jsx-runtime",
    });
    // → { code, map } | null: null when there was nothing to rewrite

    Knowing it runs#

    A precompiled page and a runtime-rendered one are the same document, so a plugin that never sees your JSX is silent: only the speed differs. The Vite plugin warns at the end of a build it did nothing in:

    [vincle/precompile] nothing was precompiled in this build: no
    .jsx/.tsx module passed through it at all (11 module(s) seen). …

    If that build does not render your JSX, the plugin does not belong in it. Otherwise your modules are not reaching Vite. The Bun plugin has no end-of-build hook, so it does not warn. The check that works everywhere: a transformed module imports jsxTemplate.

    Custom runtime#

    To use your own implementation of the three helpers, point runtimeSource to your module:

    precompile({
    runtimeSource: "custom/jsx-runtime",
    });

    A module wrapping @vincle/core keeps its output only by re-exporting it whole; naming only the three helpers makes it another runtime:

    vincle-runtime.ts
    export * from "@vincle/core/jsx-precompile-runtime";

    A runtime declaring the "vincle" precompile dialect must export both jsxAttr and jsxEscape, or the build fails.

    Build-time sanitization#

    Enabled by default with @vincle/core. Each static attribute value goes through the runtime's own jsxAttr at build time, and the result is inlined into the template, at zero runtime cost. A static value gets the filtering a dynamic one gets (Security): URL schemes, style declarations, attribute names.

    // Static string: looks harmless, linter passes
    <a href="javascript:alert(1)">click</a>
    // Targeting @vincle/core → blocked at build time
    // <a href="#blocked">click</a>
    // Targeting another runtime → inlined verbatim in the bundle
    // <a href="javascript:alert(1)">click</a>