DevTools.
Aktion DevTools (aktion-runtime/devtools) is a
real, in-page debugger for any <aktion-app> on the
page — the Aktion equivalent of the React / Vue DevTools, built
on Aktion’s own runtime signals. Inspect and edit live reactive
$state, profile every render commit, and watch a
timeline of every effect as it mounts, runs, and cleans up.
Try it live
The little app below is a normal <aktion-app>. Click
Launch DevTools to open the inspector docked to the
corner of this page, then poke at the app — increment the counter,
toggle the checkbox, watch the uptime ticker — and see the three
panels react in real time.
The program driving the demo:
$count = 0
$seconds = 0
$user = { name: "Ada", role: "engineer", prefs: { notify: true, theme: "dark" } }
$tags = ["state", "renders", "effects"]
// An interval effect — runs every second, so the Effect timeline fills up live.
$effect(() => { $seconds = $seconds + 1 }, ["every(1000)"])
// A user component — appears as its own instance in the Render profiler.
function Stat(label, value, tone) {
return Card([
Column([
Text(label, { variant: "small", tone: "muted" }),
Text(value, { variant: "title" }),
Badge(tone, { tone: tone })
], { gap: "xs" })
])
}
$app(Column([
CardHeader("DevTools demo", { subtitle: "Open the inspector, then interact." }),
Row([
Stat("Count", `${$count}`, "info"),
Stat("Uptime", `${$seconds}s`, "success")
], { grow: true }),
Row([
Button("Increment", { onClick: () => $count = $count + 1, variant: "primary" }),
Button("Reset", { onClick: () => $count = 0, variant: "secondary" })
]),
Separator(),
Checkbox("notify", { label: "Email notifications", value: $user.prefs.notify }),
Text(`Signed in as ${$user.name} (${$user.role})`, { variant: "small", tone: "muted" })
]))
How it works
DevTools mirrors the architecture of the browser DevTools: a backend (the runtime) and a frontend (the panel) that only ever talk through a structured event protocol.
-
The runtime always emits to a global hook
(
window.__AKTION_DEVTOOLS_HOOK__), exactly like React’s__REACT_DEVTOOLS_GLOBAL_HOOK__. Until a frontend installs that hook and subscribes, every emit is a single property-read no-op — so production bundles that never open DevTools pay essentially nothing. -
The panel is a separate, opt-in entry
(
aktion-runtime/devtools). It installs the hook, adopts every<aktion-app>on the page, and renders inside its own shadow root — fully isolated from the app it inspects, so it can debug even a broken program without sharing its fate. - Everything is the real runtime. State edits flow back through the genuine reactive pipeline; the profiler reads the actual per-instance memoization decisions; the effect timeline is fed by the real effect runner. Nothing is mocked.
State inspector
A live, editable tree of every reactive $state atom. Expand
objects and arrays to any depth, filter by name, and
click any value to edit it — the edit is parsed
(JSON first, then bare string) and written through the same path the
runtime uses, so dependents re-render and computed atoms re-derive
exactly as a real event handler would. Dotted edits
(user.prefs.notify) reconstruct the object immutably, so
sibling fields are preserved. Atoms flash as they change, and runtime-owned
atoms (like route) are shown but marked read-only.
Each atom also carries a reactivity-heat badge — a small bar and count of how often it has changed this session — so the busiest state stands out instantly. Hit Sort by activity to rank atoms by change frequency and find what your UI actually churns on.
import { mountDevtools } from "aktion-runtime/devtools";
mountDevtools(); // attaches to every <aktion-app> on the page
Render profiler
Every commit is captured with its trigger, total duration, and a per-component breakdown. The commit strip shows each render as a bar (green = initial mount, amber = full render, blue = incremental); click one to inspect it. The flamegraph lists every component instance in that commit, indented by tree depth and coloured by phase:
- mount — the instance appeared for the first time;
- update — it re-evaluated, with the reason (an arg changed, a
$statedependency changed, or a forced full render); - memoized — its body was skipped because neither its args nor the
$statepaths it reads changed.
A ranked components table aggregates render counts,
total / average / max self-time and memo skips across
captured commits — click any column header to sort — so you
can spot hot paths at a glance. Above it, a performance
summary shows headline numbers (total render time, average and
slowest commit, memoization ratio, full-render count, commit rate), a
reactivity panel ranks the $state paths
that triggered the most commits, and an insights list
calls out likely problems — heavy component bodies, components that
re-render without ever memoizing, and bursts of forced full renders.
Flip on Flash on commit to outline the app element every
time it re-renders.
Effect timeline
A stream of every effect lifecycle transition —
mount, run, cleanup,
unmount, and error — each attributed to
the trigger that fired it: a $state change
(state:count), an interval tick
(every(1000)), or a lifecycle event. A
visual timeline plots every event for each effect along
a shared time axis — coloured by phase — so bursts,
overlapping runs, and cleanup→run pairing are obvious at a glance
(toggle to a flat Log view when you want exact
ordering). An effect summary totals runs, run time,
cleanups, and errors, while insights flag effects that
re-ran excessively (a hot trigger or over-broad dependency list), did
heavy work per run, or threw. Per-effect lanes summarise
run counts and total time, and you can filter by phase. Component-scoped
effects (declared inside a function body) are tagged
instance and correctly report their unmount when the
component leaves the tree.
Enable it
One line, from any page that already loads Aktion:
import "aktion-runtime"; // registers <aktion-app>
import { mountDevtools } from "aktion-runtime/devtools";
mountDevtools();
Or straight from a CDN, as ES modules:
<script type="module">
import "https://cdn.jsdelivr.net/npm/aktion-runtime/dist/aktion.js";
import { mountDevtools } from "https://cdn.jsdelivr.net/npm/aktion-runtime/dist/devtools.js";
mountDevtools();
</script>
mountDevtools() returns a controller
({ element, hook, open, close, toggle, selectApp, destroy }) so
you can bind it to a keyboard shortcut or a “debug” button
and ship it behind a flag. Because the hook is shared, multiple panels
cooperate on one event stream.
Zero-cost when closed. If you never import
aktion-runtime/devtools, the runtime’s instrumentation is
dormant — no profiling bookkeeping, no event allocation. Open the
panel and it lights up on the next render.
Keep exploring
Hooks
Per-instance $state / $memo — exactly what the State inspector shows you live.
Side effects
Effects, intervals, and cleanup — the events that fill the Effect timeline.
Open the guide → QualityTesting
Assert on rendered output, $state, and events with the Aktion Testing Library.