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 treejsx("div", { class: cls }, jsx("span", {}, text));
// After (precompile): direct string assemblyjsxTemplate`<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 (seerenderToString). - 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:
import { plugin } from "bun";import precompile from "@vincle/precompile/bun";
plugin(precompile());bun --preload ./preload.ts server.tsThere 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 rewriteKnowing 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:
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>