Performance
Flexium is designed for high performance by default. With fine-grained reactivity, no Virtual DOM overhead, and intelligent optimizations, your applications run faster with less code. This guide covers performance patterns, optimization techniques, and best practices.
Core Performance Advantages
Fine-Grained Updates
Because Flexium uses signals, updates are pinpointed. If a state changes, only the specific text node or attribute bound to that state updates. There's no component re-rendering or VDOM diffing.
import { state } from 'flexium/core';
function Counter() {
const [count, setCount] = state(0);
// When count changes, ONLY the text node updates
// The button, div, and everything else remain untouched
return (
<div class="container">
<h1>Counter</h1>
<p>Count: {count}</p> {/* Only this text updates - count works directly */}
<button onclick={() => setCount(c => c + 1)}>Increment</button>
</div>
);
}No Virtual DOM
There is no Virtual DOM overhead. No diffing phase. No reconciliation. This reduces memory usage and CPU cycles significantly.
Traditional VDOM approach:
- State changes
- Re-render entire component tree
- Create new VDOM tree
- Diff old vs new VDOM
- Compute patches
- Apply patches to real DOM
Flexium approach:
- State changes
- Update DOM directly
This means Flexium can be 2-10x faster than VDOM-based frameworks for many operations.
Automatic Dependency Tracking
Effects and computed values automatically track only the signals they read. No manual dependency arrays, no risk of stale closures or missing dependencies.
import { state, effect } from 'flexium/core';
const [a, setA] = state(1);
const [b, setB] = state(2);
effect(() => {
if (a > 5) { // Both a() and a work in effects
// Only when a > 5, this effect depends on 'b'
console.log('b is', b);
}
});
// Changing 'b' when a <= 5 won't trigger the effect
setB(10); // No effect runs
// But once a > 5, b becomes a dependency
setA(6); // Effect runs
setB(20); // Effect runs againSignal Optimization Patterns
Use Computed for Derived State
Instead of recalculating values in JSX, use computed() to memoize derived values:
```tsx
import { state } from 'flexium/core';
// Bad - recalculates on every access
function TodoList() {
const [todos] = state([...]);
return (
<div>
<p>Total: {todos().length}</p>
<p>Completed: {todos().filter(t => t.done).length}</p>
<p>Active: {todos().filter(t => !t.done).length}</p>
</div>
);
}
// Good - computed values memoize results
function TodoList() {
const [todos] = state([...]);
const [total] = state(() => todos().length);
const [completed] = state(() => todos().filter(t => t.done).length);
const [active] = state(() => todos().filter(t => !t.done).length);
return (
<div>
<p>Total: {total}</p>
<p>Completed: {completed}</p>
<p>Active: {active}</p>
</div>
);
}Computed values only recalculate when their dependencies change, and they cache the result for multiple reads.
Prefer Computed Over Effect
When you need a derived value, use computed() instead of effect() with manual state updates:
```tsx
import { effect, state } from 'flexium/core';
const [firstName, setFirstName] = state('John');
const [lastName, setLastName] = state('Doe');
// Bad - uses effect to maintain derived state
const [fullName, setFullName] = state('');
effect(() => {
setFullName(`${firstName()} ${lastName()}`);
});
// Good - computed automatically updates
const [fullName] = state(() => `${firstName()} ${lastName()}`);Computed is more efficient because it's lazy (only computes when read) and doesn't trigger unnecessary updates.
Avoid Nested Signal Reads
Reading signals inside loops or nested structures can create many dependencies:
```tsx
import { state } from 'flexium/core';
const [items, setItems] = state([...]);
// Bad - creates dependency on every item property
effect(() => {
const list = items();
list.forEach(item => {
console.log(item.name, item.value);
});
});
// Good - depend only on the array itself
effect(() => {
const list = items();
console.log('Items changed:', list.length);
});
// Or use granular signals for each item
const itemSignals = items().map(item => state(item));Computed vs Effect Usage
Understanding when to use computed() vs effect() is crucial for performance.
Use Computed When:
- Deriving values from other signals
- The result will be read by other computations or UI
- You want lazy evaluation (only computes when accessed)
- You need memoization
```tsx
import { state } from 'flexium/core';
const [price, setPrice] = state(100);
const [tax, setTax] = state(0.08);
// Computed - perfect for derived values
const [total] = state(() => price() * (1 + tax()));
// Used in UI - only recalculates when price or tax changes
<div>Total: ${total()}</div>Use Effect When:
- Performing side effects (logging, API calls, DOM manipulation)
- The computation doesn't produce a value for others to use
- You need eager execution (run immediately on changes)
- Setting up subscriptions or event listeners
```tsx
import { effect, state } from 'flexium/core';
const [userId, setUserId] = state(null);
// Effect - perfect for side effects
effect(() => {
const id = userId(); // userId() works in effects
if (id) {
// Side effect: log to analytics
analytics.track('user_viewed', { userId: id });
}
});
// Effect with cleanup
effect(() => {
const handler = () => console.log('Window resized');
window.addEventListener('resize', handler);
return () => window.removeEventListener('resize', handler);
});Performance Comparison
```tsx
import { effect, state } from 'flexium/core';
const [count, setCount] = state(0);
// Computed - lazy, memoized
const [doubled] = state(() => {
console.log('Computing doubled');
return count() * 2;
});
// Effect - eager, runs on every change
effect(() => {
console.log('Effect triggered');
const value = count() * 2;
});
setCount(1); // Both run
doubled(); // Returns cached value, no recomputation
doubled(); // Still cached
setCount(2); // Effect runs immediately, computed marks stale
// Computed only recalculates when accessed
doubled(); // Recomputes nowSync Updates
When making multiple state changes, sync them to avoid intermediate updates:
import { sync } from 'flexium/advanced';
import { state } from 'flexium/core';
const [firstName, setFirstName] = state('John');
const [lastName, setLastName] = state('Doe');
const [age, setAge] = state(30);
// Bad - triggers 3 separate updates
function updateUser(user) {
setFirstName(user.first); // Update 1
setLastName(user.last); // Update 2
setAge(user.age); // Update 3
// UI re-renders 3 times!
}
// Good - batches into 1 update
function updateUser(user) {
sync(() => {
setFirstName(user.first);
setLastName(user.last);
setAge(user.age);
});
// UI re-renders once!
}Batching/Syncing is especially important when:
- Updating multiple related signals
- Processing arrays of changes
- Responding to user input that affects multiple states
Automatic Batching
Flexium automatically batches updates in event handlers:
// Updates are automatically batched in event handlers
<button onclick={() => {
setCount(c => c + 1);
setName('Updated');
setActive(true);
// All 3 updates batched automatically
}}>
Update
</button>
// But not in async callbacks
setTimeout(() => {
// These are NOT automatically batched
setCount(c => c + 1);
setName('Updated');
setActive(true);
}, 1000);
// Wrap in sync() for async contexts
setTimeout(() => {
sync(() => {
setCount(c => c + 1);
setName('Updated');
setActive(true);
});
}, 1000);Event Delegation
Flexium uses efficient event delegation for all standard events. Listeners are attached to the document root rather than individual elements, reducing memory usage and setup time.
- Smart Traversal: Uses
event.composedPath()for fast target resolution, even through Shadow DOM. - Automatic Cleanup: Global listeners are managed automatically.
- Automatic Batching: Event handlers automatically batch state updates.
Memory Management
Root Scopes
Use root() to create disposal scopes for effects and computations:
import { effect, state } from 'flexium/core';
import { root } from 'flexium/advanced';
const [count, setCount] = state(0);
// Bad - effect never gets cleaned up
function createWatcher() {
effect(() => {
console.log('Count:', count());
});
}
createWatcher(); // Creates effect
createWatcher(); // Creates another effect - memory leak!
// Good - effect is properly cleaned up
function createWatcher() {
return root(dispose => {
effect(() => {
console.log('Count:', count());
});
return dispose; // Return cleanup function
});
}
const cleanup1 = createWatcher();
const cleanup2 = createWatcher();
// Clean up when done
cleanup1();
cleanup2();Component Cleanup
In components, effects are automatically cleaned up when the component unmounts:
function Timer() {
const [time, setTime] = state(0);
// This effect is automatically cleaned up on unmount
effect(() => {
const interval = setInterval(() => {
setTime(t => t + 1);
}, 1000);
return () => clearInterval(interval);
});
return <div>Time: {time}</div>; {/* time works directly */}
}Untrack for Read-Only Access
Use untrack() to read a signal without creating a dependency:
import { effect, state } from 'flexium/core';
import { untrack } from 'flexium/advanced';
const [count, setCount] = state(0);
const [multiplier, setMultiplier] = state(2);
// This effect only tracks 'count', not 'multiplier'
effect(() => {
const c = count();
const m = untrack(() => multiplier()); // Read without tracking
console.log('Result:', c * m);
});
setCount(5); // Effect runs
setMultiplier(3); // Effect does NOT runThis is useful for:
- Reading initial/default values
- Logging or debugging without creating dependencies
- Conditional logic where you don't want tracking
Peek Method
Signals also have a .peek() method that works like untrack():
import { state, effect } from 'flexium/core';
const [count, setCount] = state(0);
effect(() => {
// Tracked access
const current = count();
// Untracked access
const initial = count.peek();
console.log(`Changed from ${initial} to ${current}`);
});List for Large Lists
When rendering thousands of items, use List with virtual mode to only render visible items:
import { List } from 'flexium/primitives';
import { state } from 'flexium/core';
function BigList() {
const [items] = state(
Array.from({ length: 10000 }, (_, i) => ({
id: i,
name: `Item ${i}`,
value: Math.random()
}))
);
return (
<List
items={items}
virtual
height={600}
itemSize={50}
overscan={5}
getKey={item => item.id}
>
{(item, index) => (
<div style={{ padding: '10px' }}>
{index}: {item.name}
</div>
)}
</List>
);
}Performance benefits:
- Constant memory usage regardless of list size
- Smooth scrolling even with 100,000+ items
- Fast initial render - only visible items rendered
- Efficient updates - only visible items re-render
List Best Practices
- Use stable keys - Provide
getKeyfor efficient reconciliation - Fixed item heights - Best performance with consistent heights
- Optimize overscan - Balance smoothness vs memory (default: 3)
- Memoize renderers - Avoid recreating render functions
import { List } from 'flexium/primitives';
import { state } from 'flexium/core';
function OptimizedList() {
const [items] = state([...]); // 100,000 items
// List component automatically optimizes rendering
return (
<List items={items}>
{(item, index) => (
<div class="item">
<span>{index}</span>
<span>{item.name}</span>
</div>
)}
</List>
);
return (
<List
items={items}
virtual
height={600}
itemSize={40}
overscan={3}
getKey={item => item.id}
>
{renderItem}
</List>
);
}Lazy Loading Patterns
Code Splitting with Dynamic Imports
Split your application into smaller chunks that load on demand:
// Use dynamic imports with state for code splitting
function App() {
const [route, setRoute] = state('dashboard');
const [components, setComponents] = state({
dashboard: null,
settings: null,
profile: null
});
// Load components on demand
effect(() => {
if (route === 'dashboard' && !components.dashboard) {
import('./Dashboard').then(m => setComponents(prev => ({ ...prev, dashboard: m.default })));
}
if (route === 'settings' && !components.settings) {
import('./Settings').then(m => setComponents(prev => ({ ...prev, settings: m.default })));
}
if (route === 'profile' && !components.profile) {
import('./Profile').then(m => setComponents(prev => ({ ...prev, profile: m.default })));
}
});
return (
<div>
<nav>
<button onclick={() => setRoute('dashboard')}>Dashboard</button>
<button onclick={() => setRoute('settings')}>Settings</button>
<button onclick={() => setRoute('profile')}>Profile</button>
</nav>
{route === 'dashboard' && (components.dashboard ? <components.dashboard /> : <div>Loading...</div>)}
{route === 'settings' && (components.settings ? <components.settings /> : <div>Loading...</div>)}
{route === 'profile' && (components.profile ? <components.profile /> : <div>Loading...</div>)}
</div>
);
}Route-Based Code Splitting
With Flexium Router, automatically split by route:
import { Router, Route } from 'flexium/router';
// Use dynamic imports with state for code splitting
const [Home, setHome] = state(null);
const [About, setAbout] = state(null);
const [Contact, setContact] = state(null);
// Load on demand
import('./pages/Home').then(m => setHome(m.default));
import('./pages/About').then(m => setAbout(m.default));
import('./pages/Contact').then(m => setContact(m.default));
function App() {
return (
<Router>
<Route path="/" component={Home} />
<Route path="/about" component={About} />
<Route path="/contact" component={Contact} />
</Router>
);
}Lazy Data Loading
Load data only when needed:
import { state, effect } from 'flexium/core';
function UserProfile({ userId }) {
const [expanded, setExpanded] = state(false);
const [details, setDetails] = state(null);
// Only fetch when expanded
effect(() => {
if (expanded && !details) {
fetch(`/api/users/${userId}/details`)
.then(res => res.json())
.then(data => setDetails(data));
}
});
return (
<div>
<h2>User {userId}</h2>
<button onclick={() => setExpanded(e => !e)}>
{expanded ? 'Hide' : 'Show'} Details
</button>
{expanded && details && (
!details.loading ? (
<div>{details?.bio}</div>
) : (
<div>Loading...</div>
)
)}
</div>
);
}Image Lazy Loading
Use native lazy loading for images:
import { Image } from 'flexium/primitives';
function Gallery({ images }) {
return (
<div class="gallery">
{images.map((img) => (
<Image
key={img.url}
src={img.url}
alt={img.title}
loading="lazy"
width={300}
height={200}
/>
))}
</div>
);
}Bundle Size Optimization
Tree Shaking
Flexium is designed for excellent tree shaking. Import only what you use:
// Bad - imports everything
import * as Flexium from 'flexium';
// Good - imports only what you need
import { state, effect } from 'flexium/core';
import { Column, Row, Text } from 'flexium/primitives';Entry Points
Flexium provides multiple entry points for optimal bundling:
// Core reactivity (smallest)
import { state, effect } from 'flexium/core';
// DOM rendering
import { render } from 'flexium/dom';
// Primitives
import { Column, Row, Text } from 'flexium/primitives';
// Advanced APIs
import { root, untrack } from 'flexium/advanced';
// Router
import { Router, Route, Link } from 'flexium/router';
// Canvas
import { Canvas } from 'flexium/canvas';
// Interactive utilities
import { keyboard, mouse, createLoop } from 'flexium/interactive';Analyze Bundle Size
Use your bundler's analysis tools:
# Vite
npx vite-bundle-visualizer
# Webpack
npx webpack-bundle-analyzer
# Rollup
npx rollup-plugin-visualizerLook for:
- Unexpectedly large dependencies
- Duplicate packages
- Unused code that wasn't tree-shaken
Dynamic Imports for Large Libraries
For large third-party libraries, use dynamic imports:
import { state } from 'flexium/core';
function ChartComponent({ data }) {
const [Chart, setChart] = state(null);
// Load chart library only when needed
effect(() => {
if (data.length > 0 && !Chart) {
import('chart.js').then(module => {
setChart(module.default);
});
}
});
return (
<div>
{Chart ? (
<ChartRenderer Chart={Chart} data={data} />
) : (
<div>Loading chart...</div>
)}
</div>
);
}Component Optimization
Avoid Unnecessary Nesting
Flat component structures are more efficient:
// Less efficient - extra nesting
function UserCard({ user }) {
return (
<Column>
<Row>
<Column>
<Text>{user.name}</Text>
</Column>
</Row>
</Column>
);
}
// More efficient - flatter structure
function UserCard({ user }) {
return (
<Column>
<Text>{user.name}</Text>
</Column>
);
}Keep Components Small
Smaller components are easier to optimize and maintain:
// Large monolithic component
function Dashboard() {
// 500+ lines of code
// Multiple states
// Complex logic
}
// Better - split into focused components
function Dashboard() {
return (
<Column>
<DashboardHeader />
<DashboardStats />
<DashboardChart />
<DashboardTable />
</Column>
);
}Memoize Expensive Computations
For expensive calculations, memoize outside the render:
```tsx
import { state } from 'flexium/core';
function DataTable({ rawData }) {
// Expensive computation - do it once
const [processedData] = state(() => {
return rawData // rawData works directly
.filter(item => item.active)
.map(item => ({
...item,
score: calculateComplexScore(item)
}))
.sort((a, b) => b.score - a.score);
});
return (
<table>
{processedData().map((row) => <TableRow key={row.id} data={row} />)}
</table>
);
}Measuring Performance
Browser DevTools
Use browser performance tools:
// Mark performance checkpoints
performance.mark('app-init-start');
render(<App />, root);
performance.mark('app-init-end');
performance.measure('app-init', 'app-init-start', 'app-init-end');Custom Performance Monitoring
Track specific operations:
import { effect } from 'flexium/core';
function measureEffect(name, fn) {
return effect(() => {
const start = performance.now();
const result = fn();
const end = performance.now();
if (end - start > 16) { // Slower than one frame
console.warn(`Slow effect "${name}": ${end - start}ms`);
}
return result;
});
}
// Usage
measureEffect('user-data-sync', () => {
const user = userData; // userData works directly
syncToLocalStorage(user);
});React DevTools Profiler
Flexium works with React DevTools for component profiling:
# Install the browser extension
# Then profile your app to find bottlenecksBest Practices Summary
- Use computed for derived values - Lazy, memoized, efficient
- Batch multiple updates - Reduce intermediate renders
- Leverage List with virtual mode for long lists - Constant memory, smooth scrolling
- Code split with dynamic imports - Smaller initial bundles
- Import only what you need - Better tree shaking
- Use untrack() judiciously - Prevent unnecessary dependencies
- Clean up effects in root scopes - Avoid memory leaks
- Profile before optimizing - Measure, don't guess
- Keep components small - Easier to optimize and maintain
- Use conditional rendering with && or ternary - Optimized for reactivity
Benchmarks
Flexium's fine-grained reactivity and no-VDOM architecture provide significant performance advantages:
- Initial render: 2-3x faster than React
- Update performance: 3-10x faster than React (no diffing)
- Memory usage: 40-60% less than React (no VDOM)
- Bundle size: ~10KB gzipped (vs 45kb+ for React)
For detailed benchmarks and comparisons, see our benchmark repository.