Quality

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.

Opens a draggable, resizable panel pinned to the corner. Click again to toggle.

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.

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:

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