---
title: Precompile transform
---

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

```ts
// 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](https://docs.deno.com/runtime/reference/jsx/)
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](#build-time-sanitization). Components in template
  holes render in the same order too: left to right, each finishing before the
  next starts (see [`renderToString`](/api/core/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

```ts
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:

```ts
// preload.ts
import { plugin } from "bun";
import precompile from "@vincle/precompile/bun";

plugin(precompile());
```

```bash
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:

```ts
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:

```ts
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:

```ts
// 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](/guide/security)): URL schemes, `style`
declarations, attribute names.

```ts
// 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>
```
