BarefootJS is designed for performance by default — server-side rendering with minimal client JavaScript. This guide covers strategies to minimize bundle size, reduce hydration cost, and optimize runtime reactivity.
#How BarefootJS Achieves Performance
#Zero-JS by Default
Components without reactive state generate no client JavaScript at all. Only components with "use client" produce client-side code:
// Server-only — 0 bytes of client JS
export function Header() {
return (
<header>
<h1>My App</h1>
<nav>...</nav>
</header>
)
}#Minimal Hydration
Unlike frameworks that ship the full component tree to the client, BarefootJS sends only:
- Signal initialization
- Event handler bindings
- Effect setup for reactive updates
The HTML structure is never re-created on the client — it was already rendered by the server.
#Reducing Client JS Size
#Minimize Signal Count
Each signal adds tracking overhead. Use createMemo for derived values instead of separate signals:
// ❌ Redundant signal
const [count, setCount] = createSignal(0)
const [doubled, setDoubled] = createSignal(0)
createEffect(() => setDoubled(count() * 2)) // Extra signal + effect
// ✅ Use memo instead
const [count, setCount] = createSignal(0)
const doubled = createMemo(() => count() * 2) // Computed, no extra signal#Use Static Arrays When Possible
If a list doesn't change after initial render, the compiler detects it as a static array and skips reconcileList:
// Static — no reconcileList generated
const tabs = ['Home', 'About', 'Contact']
{tabs.map(tab => <Tab label={tab} />)}
// Dynamic — reconcileList needed
const [items, setItems] = createSignal([...])
{items().map(item => <Item key={item.id} data={item} />)}#Optimizing Hydration
#Use Keys for List Reconciliation
Always provide stable keys for dynamic lists. Without keys, the reconciler can't reuse DOM nodes:
// ✅ Stable key — DOM nodes reused when list changes
{items().map(item => <li key={item.id}>{item.name}</li>)}
// ❌ Index key — DOM nodes recreated on reorder
{items().map((item, i) => <li key={i}>{item.name}</li>)}#Preserve Focus in Lists
The reconciler automatically preserves focused elements during list updates. If a focused input is in a list item, it won't lose focus when the list re-renders. This is built-in — no action needed on your part.
#Optimizing Reactivity
#Use `untrack` for One-Time Reads
When you need a signal's current value without subscribing to changes:
createEffect(() => {
// Only re-runs when items() changes, not when count() changes
const currentCount = untrack(() => count())
console.log(`${items().length} items, count was ${currentCount}`)
})#Avoid Effects for Derived Data
createMemo is cheaper than createEffect + createSignal:
// ❌ Effect → Signal chain (two subscriptions)
const [total, setTotal] = createSignal(0)
createEffect(() => setTotal(price() * quantity()))
// ✅ Single memo (one subscription)
const total = createMemo(() => price() * quantity())#Guard Effect Side Effects
Effects with the same result can skip expensive operations:
createEffect(() => {
const cls = isActive() ? 'active' : 'inactive'
if (element.className !== cls) {
element.className = cls // Only touches DOM when value changes
}
})Note: The compiler already generates guarded updates for text content and common attributes. This tip applies to custom effects.