#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>
  )
}

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>
  1. The server renders HTML with markers, props embedded in bf-p.
  2. The browser loads the client JS.
  3. hydrate() finds uninitialized bf-s elements.
  4. The init function runs once per scope — signals, effects, handlers.
  5. 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.