#Two-Phase Compilation
One JSX source file produces two outputs:
JSX Source
↓
[Phase 1] Analyze + Transform → IR (Intermediate Representation)
↓
[Phase 2a] IR → Marked Template (server)
[Phase 2b] IR → Client JS (browser)Phase 1 produces a backend-independent IR: structure, reactive expressions, event handlers, types. Phase 2 renders it twice — as a marked template in the chosen adapter's format, and as client JS that creates signals and binds handlers to the marked elements.
Source:
"use client"
import { createSignal } from '@barefootjs/client'
export function Counter() {
const [count, setCount] = createSignal(0)
return (
<button onClick={() => setCount(n => n + 1)}>
Count: {count()}
</button>
)
}Marked template (Phase 2a) — bf-s is the component scope, bf="s1" the button, and the s0 markers bracket the reactive text:
export function Counter({ __instanceId, ... }) {
const __scopeId = __instanceId || `Counter_${Math.random().toString(36).slice(2, 8)}`
const count = () => 0
return (
<button bf-s={__scopeId} bf="s1">
Count: {bfText("s0")}{count()}{bfTextEnd()}
</button>
)
}{{define "Counter"}}
<button bf-s="{{bfScopeAttr .}}" bf="s1">
Count: {{bfTextStart "s0"}}{{.Count}}{{bfTextEnd}}
</button>
{{end}}Client JS (Phase 2b) — the effect writes count() into slot s0, the handler attaches to s1:
import { $, createEffect, createSignal, escapeText, escapeTextOrNode, hydrate, lazySlots } from '@barefootjs/client/runtime'
export function initCounter(__scope, _p = {}) {
if (!__scope) return
const [count, setCount] = createSignal(0)
const [_s1] = $(__scope, 's1')
const __bfw_s0 = lazySlots(__scope, [{ id: 's0', kind: 'markup', path: [] }])
createEffect(() => {
const __val = count()
__bfw_s0('s0', escapeTextOrNode(__val))
})
if (_s1) _s1.addEventListener('click', () => { setCount(n => n + 1) })
}
hydrate('Counter', {
init: initCounter,
template: (_p) => `<button bf="s1"> Count: <!--bf:s0-->${escapeText((0))}<!--/--></button>`
})#Hydration
bf-* attributes in the marked template tell the client script where to attach:
| Marker | Purpose | Example |
|---|---|---|
bf-s |
Component scope boundary (~ = child) |
<div bf-s="Counter_a1b2"> |
bf |
Interactive element (slot) | <p bf="s0"> |
bf-p |
Serialized props JSON | <div bf-p='{"initial":5}'> |
bf-c |
Conditional block | <div bf-c="s2"> |
bf-po |
Portal owner scope ID | <div bf-po="Dialog_a1b2"> |
bf-pi |
Portal container ID | <div bf-pi="bf-portal-1"> |
bf-pp |
Portal placeholder | <template bf-pp="bf-portal-1"> |
bf-i |
List item marker | <li bf-i> |
- The server renders HTML with markers, props embedded in
bf-p. - The browser loads the client JS.
hydrate()finds uninitializedbf-selements.- The init function runs once per scope — signals, effects, handlers.
- The runtime tracks initialized scopes; none runs twice.
#Inspecting the output
compileJSX(source, file, { adapter, outputIR: true }) adds the IR to result.files as a .ir.json entry (type: 'ir'). bf debug graph <component> prints a component's signal graph. The full pipeline and IR schema are in spec/compiler.md.
#What this means for performance
- Components without
"use client"ship zero JavaScript. - Hydration touches only marked nodes; the server-rendered HTML is never re-rendered.
- Give list items a stable
key(item.id, not the index) so rows are reused, not recreated.