Component rendering

A <bext-include> can point to a component file — bext compiles and SSRs it in its V8 isolate pool, with optional client-side hydration via the island system.

<bext-include src="./components/PricingTable.tsx"
  render="v8"
  props='{"plan":"pro","locale":"fr"}'
></bext-include>

bext compiles the TSX, executes the exported component in a V8 context with the props, and emits the SSR'd HTML inline. No Bun process, no framework runtime, no separate build step.

Attributes

Attribute Values Purpose
render v8, jsc, quickjs Which engine to use
props JSON string Props passed to the component
hydrate boolean Wrap output for client hydration
module esm, cjs Module format (auto-detected)

How it works

<bext-include src="./components/PricingTable.tsx" render="v8" props='{"plan":"pro"}'>
  ↓
1. Resolve file path relative to site root
   ↓
2. Check bundle cache
     HIT  → load cached compiled bundle
     MISS → compile with Turbopack/SWC
            · strip TypeScript types
            · resolve imports
            · bundle into IIFE
          → store in L1 mmap cache
   ↓
3. Checkout V8 isolate from pool
   ↓
4. Execute in isolate context with:
     · component module
     · props (parsed JSON)
     · request context (headers, cookies, locale)
   ↓
5. Call renderToString() → HTML
   ↓
6. Return isolate to pool
   ↓
7. Optionally wrap in <bext-island> for hydration
   ↓
8. Return HTML

The V8 pool is pre-warmed at startup. Typical render times:

- Simple component (nav, button): ~1-5ms

- Medium component (list, form): ~5-20ms

- Complex component (chart, table): ~20-100ms

Combine with ttl caching to amortize the cost across many requests.

Engine selection

Engine Feature flag Use when
v8 v8 Default; best perf on Linux/x86
jsc jsc macOS/iOS builds, or when V8 isn't available
quickjs (always) Small binary size; slower but always enabled

Enable the engine you need:

# bext.config.toml
[include.v8]
enabled = true
max_execution_ms = 500
max_memory_mb = 64
pool_size = 4

Props

Pass props as a JSON string in the props attribute:

<bext-include src="./Hero.tsx"
  render="v8"
  props='{"title":"Welcome","subtitle":"Build fast","showCta":true}'
></bext-include>

Props are parsed as JSON and passed to the component:

// components/Hero.tsx
interface HeroProps {
  title: string;
  subtitle: string;
  showCta: boolean;
}

export default function Hero({ title, subtitle, showCta }: HeroProps) {
  return (
    <section>
      <h1>{title}</h1>
      <p>{subtitle}</p>
      {showCta && <a href="/signup">Sign up</a>}
    </section>
  );
}

Props are included in the cache key — different props produce different cached entries.

Hydration

By default, V8 renders produce static HTML — no client-side JavaScript. For interactive components, add hydrate:

<bext-include src="./Counter.tsx"
  render="v8"
  props='{"initial":0}'
  hydrate
></bext-include>

The output is wrapped in a <bext-island> element that the PRISM islands system hydrates client-side:

<div data-bext-island="Counter" data-props='{"initial":0}'>
  <button>Count: 0</button>
</div>
<script type="module">
  import { hydrateIsland } from '/__bext/islands/Counter.js';
  hydrateIsland(document.querySelector('[data-bext-island="Counter"]'));
</script>

The component's client-side JS is bundled separately and only loaded for island targets. No page-wide hydration cost.

Host functions

V8 renders have access to bext host functions — the same ones available in full page SSR:

// components/UserDashboard.tsx


export default async function UserDashboard(props) {
  const user = await bext.fetch('/api/user/me', {
    headers: { Cookie: bext.headers.cookie }
  });
  const cached = await bext.cache.get(`dashboard:${user.id}`);

  return <div>{/* ... */}</div>;
}

Available APIs:

- bext.cache — get/set/invalidate

- bext.realtime — publish to channels

- bext.fetch — HTTP fetches

- bext.headers / bext.cookies — request context

- bext.logger — structured logging

Bundle caching

Compiled bundles are cached in the L1 mmap store, keyed by the source file's mtime. Editing a component triggers recompilation; otherwise the bundle is loaded from cache on every request.

First request to /Counter.tsx
  → source mtime = 1712000000
  → compile with Turbopack
  → store bundle at .bext/cache/bundles/{hash}.js
  → execute

Subsequent requests (file unchanged)
  → mtime check passes
  → load cached bundle from mmap (microseconds)
  → execute

You edit Counter.tsx
  → mtime changes → re-compile → update cache

Dev mode: file watcher triggers immediate invalidation

Fuel limits

V8 renders run under bext's sandbox with fuel-based execution limits — the watchdog thread interrupts execution if it takes too long:

[include.v8]
max_execution_ms = 500   # hard kill at 500ms
max_memory_mb = 64       # hard kill at 64MB

A runaway component can't hang the server. If execution exceeds the limit, the include returns None and the fallback chain kicks in.

Combining with caching

V8 renders are expensive. Always pair with ttl:

<bext-include src="./PricingTable.tsx"
  render="v8"
  props='{"plan":"pro"}'
  ttl="1h"
  vary="locale"
></bext-include>

- First request: V8 render (~20ms).

- Subsequent requests: cache hit (~50µs).

- Locale change: new cache entry; V8 render happens again for that locale.

Combining with streaming

V8 renders can stream lazily:

<bext-include src="./HeavyChart.tsx"
  render="v8"
  props='{"data":"..."}'
  loading="lazy"
  timeout="500ms"
>
  <div class="chart-skeleton"></div>
</bext-include>

Shell arrives with the skeleton. V8 render happens in the background. Chunk arrives when ready.

Security

V8 renders run in an isolated context:

- No access to the host filesystem (beyond reading configured partials).

- No access to environment variables except those explicitly passed.

- Fuel-limited — no infinite loops.

- Memory-limited — no heap exhaustion attacks.

- Network-gated — host fetch respects an allowlist configured per site.

The V8 sandbox is the same one used for bext's plugin system, which has been audited for server-side execution of untrusted code.

When to use V8 renders

Use V8-rendered includes when:

- You have React/JSX components that would otherwise need a separate Bun SSR process.

- You want per-fragment SSR without a full-page SSR pipeline.

- You need interactive components with hydration for specific parts of a page.

- You're migrating from a framework but want to keep some SSR'd components.

Don't use V8 renders when:

- Static HTML is sufficient (use file partials).

- The content is purely data-driven (use an endpoint that returns HTML).

- You're rendering on every request without caching (V8 is expensive).

See also

- Islands Architecture — the hydration system V8 renders use

- Caching includes — essential for V8 render performance

- React, Preact, Solid — bext's JSX runtime

- Plugin System — V8's sandbox architecture