Devtools
Devtools
A dock on the page that shows the island graph, with each region outlined in place, its real JS cost, what crossed the wire, and when each island woke.
On this page
Browser devtools show you elements, scripts and network requests, but they have no idea which parts of an ogygia page are islands, which are static, what each island cost to download, or why one of them woke late. The ogygia devtools fill that gap. They are a dock that mounts on each csr = false page and shows the island graph: every region outlined on the page, the JS it really costs over the wire, the values that crossed the boundary, and a timeline of when each island woke. The dock reads the same event bus the runtime emits, so what it shows is what actually happened rather than an estimate.
Enabling
The devtools ship as their own package:
pnpm add @ogygia/devtoolsThen list its plugin beside ogygia(). The devtools are compiled behind a gate, so wherever the plugin is not listed, the whole devtools graph (the dock, the instruments and their CSS) tree-shakes to nothing. Because the plugin list is code, you decide where the devtools are on:
import { ogygia } from 'ogygia/vite';
import { devtools } from '@ogygia/devtools/vite';
export default defineConfig(({ mode }) => ({
plugins: [
ogygia(),
...(mode !== 'production' ? [devtools()] : []), // dev server and preview builds, never production
sveltekit(),
],
}));devtools() takes two switches: dock (the dock, on by default) and profiler (the SSR profiler, off by default). Both are listed under Plugins.
On the dev server, a launcher appears at the bottom of each csr = false page. In a build it stays hidden until you turn it on in your own browser, as described below.
What measuring costs the page
The devtools measure the page they show, so they must not change what they measure. Loaded one after the other with measuring on and off, a heavy page (17,000 elements, 30 islands) takes about 15 ms more main-thread time and 1.6 KB more HTML (the server’s events), with no measurable change in server time. With the dock open, each tab does at most a few ms of its own work every 3 seconds and never causes a long task. internal/bench/devtools-build-check.ts checks the first two numbers on every run.
In a build (a preview deploy)
When the plugin is listed in a build (a QA, UAT or preview deploy), a page carries only a small switch, about 10 KB of JS including the event recorder. Visitors see nothing: no launcher, no measuring and no server event data. The build prints a prominent message about it once, so that a flag left on by mistake gets noticed before it ships.
To turn the devtools on in your own browser, either:
- add
?og-devtools=onto any page’s URL (the address bar drops it once it is applied), or - run
ogygia.devtools.on()in the browser console.
The setting persists across navigations and reloads, because it is stored in an og_devtools cookie for 30 days. From then on the launcher shows, your pages are measured from the start (for the Page tab), and the server adds its event data to them (for the Wire and Timeline tabs). The dock itself, with its code, tabs and CSS, loads only when you open it.
Turn it off with ?og-devtools=off, ogygia.devtools.off(), or hide in the dock’s title bar, and check the current state with ogygia.devtools.status(). Other visitors never see any of it.
Two things remain dev-only. The Main thread section’s CPU sampling needs a Document-Policy: js-profiling response header, which the dev server sends and which you can set on your preview host to get the section there too. And a Kit-hydrated (csr = true) page gets no dock in a build.
The dock
The launcher peeks up from the bottom edge and shows itself when you hover over it. Click it (or press Alt+O) to open the panel, which you can drag by its title bar and resize from any edge. Its position, size and open state persist across reloads. The dock lives in a shadow root, so your page’s CSS cannot reach it and its own styles cannot leak out. trace copies an event trace to the clipboard, and where the browser refuses clipboard access (plain http away from localhost, such as a phone on your network), it saves the trace as a file instead.
Instruments
- Lens is the boundary overlay. When it is on, each
<ogygia-region>on the page is outlined and labelled by name, tinted by kind (island, lake or hole), and dashed while cold. A roster lists the regions with their kind, wake schedule, JS cost and hydration state. Hovering over a row highlights its box on the page, and the reverse works too. “Pick on page” lets you click a region to inspect it. - Page shows what the browser saw of this visit. See The Page tab.
- Record lets you press record, use the page and press stop. See Recording a session.
- Hydration shows each island’s hydration status and what differed. See The Hydration tab.
- Bytes is the byte ledger. In a build it reads every file each island needs from the page’s island graph and weighs each file as the browser loaded it. For each island it shows the files and their size, only it (the bytes no other island needs, which is what removing that island would save), and shared. The page total counts each file once, and a list names the shared files and the islands that use each one. The runtime’s files get their own row, and an island that has not woken yet shows its files as cold. On the dev server, where there is no island graph and modules are unbundled, it shows each island’s entry chunk and an estimate of its whole bundle, in which shared code counts once per island.
- Wire shows everything the server shipped across a boundary: the devalue props per island, the page and query seeds, and capabilities.
- Hub shows the identity spine. For each
Refid it gives the kind, resolve count, reunion (memo-hit) count and disposal. - Nav shows the SPA navigation decision for each region (keep, patch, mount or remove) with timing, so you can watch the reconciler at work.
- Timeline shows client-realm events on a time axis, one lane per region, with hydrate bars.
- Profiler runs the SSR profiler on this page inside the dock (it needs
devtools({ profiler: true })). It shows the score, the server render time and what it would be after the fixes, the findings, the heaviest components and the outbound calls. Each island’s server row is linked to the island on the page: hover to highlight it and click to open it, and the island’s detail card then shows the same numbers under “on the server”. The full report is one click away.
The Page tab
The Page tab reads the same browser data that the profiler’s beacon sends, but live and on your own page, and nothing leaves the page. It shows:
- Vitals: TTFB, FCP, LCP, CLS and INP, rated good, needs work or poor by the web-vitals thresholds, along with long tasks.
- Findings: what the browser measured going wrong. Each finding names its islands; hover over it to highlight them on the page, or click an island chip to open its detail card.
- Island wakes: a waterfall with one row per island. Each row splits into the module load, the wait for its turn (islands hydrate one per task, viewport first) and the hydrate step itself. FCP, LCP, DCL and load are marked on the axis.
- Hole answers: a waterfall with one row per hole, on the same time axis. Each bar is cut at the points where the browser and the server timed it: before the request left, the wait for a render slot on the server, the server render, and then the network and the swap. The time on the right is how long the fallback was shown. A hole whose answer never came gets a red row with the reason. Hover over a row to highlight the hole on the page, or click to open it.
- Main thread: the browser’s own sampler (the JS Self-Profiling API, in Chromium) records the first 8 seconds of the page. It shows busy time by kind of code (your app, ogygia, Svelte, packages, a page script, the browser, the devtools themselves) and the functions that ran, with file and line. Each island row shows the CPU used inside its hydrate window. “Record 5 s” samples again while you use the page (a click, a navigation). The dev server sends the
Document-Policy: js-profilingheader this needs, and the trace stays in the page. - Blocked the first paint: the files that held back the first paint.
- Styles: every stylesheet on the page, linked or inline, with its rules, its size, whether it held the first paint, and how many of its rules currently match nothing on the page. The devtools’ own styles are left out. Content that appears later (a menu, a dialog) counts as unmatched until it is open, so open it and press check again.
- Files by type: count and bytes for scripts, styles, images, fonts and fetches. Every file counts, not only the first ones, because the browser’s resource list stops at 250 entries unless a page asks for more, and this page does.
- Since your last load: when you change the code, the page reloads and the tab lists what changed on this page since the previous load: findings fixed and new (by island), islands whose hydrate step or file load got faster or slower, and vitals that moved. A change in first byte, first paint, largest paint or slowest interaction names the part that moved most, using the same parts as its explanation: “LCP 2.9 s → 500 ms, mostly its download 2.7 s → 300 ms”. Small fluctuations are left out by the same rule the profiler’s “since your last profile” uses, so the two never disagree. Each page’s last load is kept in this browser tab only.
- What this browser cannot measure: Safari reports no layout shifts, long tasks or CPU samples. A finding that needs one of those cannot appear in Safari, so the tab lists which ones are missing rather than letting their absence look like a clean result.
The findings it can name:
| Finding | What the browser saw |
|---|---|
| failed to hydrate | The island threw while it loaded or mounted. It shows its server HTML and does nothing. |
| threw away the server HTML | Svelte discarded the server DOM and rendered again (a flash, and a double render). |
| markup changed | The island rendered different markup in the browser than on the server. The finding shows the first difference. |
| clicked before it woke | A visitor pressed the island before it was awake. The first click did nothing. (An island with wake: 'interaction' is exempt: its first click is its wake, and is replayed.) |
| hydrating moved the layout | A layout shift came from inside the island, right after it woke. |
| long hydrate | The hydrate step was one task of 50 ms or more. The page could not respond meanwhile. With a CPU trace, it names the function that took the time. |
| waited for its turn | The island had its code, but waited behind other islands. The finding names them. |
| slow module | The island waited a long time for its code. |
| eager below the fold | A wake: 'load' or 'idle' island starts below the first screen. wake: 'visible' would load its code later. |
| woke late | An island on the first screen woke a second or more after the page looked done (LCP). |
| largest paint repainted | The LCP element sits in an island that changed its markup as it woke. |
| never woke | A wake: 'load' island is still asleep three seconds after load. |
| long tasks | Long tasks ran outside any hydration: page scripts held the main thread. With a CPU trace, it names the script and line; without one, the browser’s long-animation-frame records name the script. |
| other origins | Scripts from other origins: their size, the main-thread time they took, the files they held the first paint with, and the scripts they loaded that the page never names (how a tag manager adds code). A well-known service gets its kind (tag manager, analytics, session replay, chat, A/B testing…) and advice for that kind. |
| another origin held the first paint | A script or style from another origin blocked the first paint. |
| another origin may have edited an island | An island did not wake from the server’s markup (it changed, was healed, recovered or failed), and scripts from another origin ran before it woke. |
| Svelte warned while hydrating | On the dev server: Svelte’s own hydration warning (the server and the browser disagreed, and Svelte kept the server’s value, so the screen does not show it). With the island that was hydrating, when that is known. |
| empty children slot | An awake island shows its children in a slot with nothing in it. An island’s children are the server’s HTML: children behind a condition that was false on the server cannot appear in the browser. |
| styles shipped without their scope | ogygia builds the CSS for lakes, holes and router pages itself. When it cannot compile a component’s scoped styles, it ships the raw style body, whose rules then apply to the whole page: a .row in it restyles every .row. The build marks such CSS, and the finding names the component and the reason. The build log has the full message. |
| held with nothing ahead | An island had its code but waited for its turn while no other island was hydrating and nothing on the first screen was still loading, past the start gate. Nothing on the page explains it: ogygia’s scheduler held the island. It is ogygia’s cost, not your page’s; the finding asks you to report it. |
| a barrel imported whole | On the dev server: an island’s module graph still holds a re-export barrel (little code of its own, many modules behind it), so everything it re-exports rides into the island. The finding names the barrel and how many modules come with it. With ogygia({ barrels }) on, the build log says why that one was left. |
| one module is most of an island | On the dev server: one module is 40% or more of an island’s code, 30 KB or more of it (dev code, unminified: shares, not shipped bytes). The island card lists its heaviest modules too. |
| a hole answered late | A hole on the first screen showed its fallback for a second or more before its answer came. The time runs from the first paint to the swap. A hole woken by a scroll is left out. The finding splits the wait: time before its request left, time waiting for a render slot on the server, and the server render. The fix follows the biggest part. A server process renders 4 holes at a time, and a hole keeps its slot while it awaits its data, so slow holes make the next ones wait. The split comes from the answer’s Server-Timing (og-queue, og-render), which the server sends only to a browser that measures: the dev server, the devtools cookie, or the profiler’s. |
| an island’s file gone | An island could not load its own content-hashed file, so it loaded its stable name fresh. The page came from a cache that outlived the build that made it. Its HTML and that island’s code now come from different builds. The finding names each island and says whether it woke. Keep the previous build’s _app/immutable/ files as long as your HTML stays cached, or purge the cached HTML on deploy. See Island files after a deploy. |
| island files not cached | In a build: the host serves the islands’ files or the runtime without a long cache (no-cache, no header, or a max-age under a day). Their names change with their content, so a returning visitor should never fetch them again. The Page tab asks the server once per file (a HEAD request) and names the heaviest files first, with the header it saw and the bytes a return visit pays. Serve _app/immutable/ with cache-control: public, max-age=31536000, immutable. SvelteKit’s adapters do, but a proxy, a CDN rule or a custom server in front can override it. |
| the first paint | When FCP is over 1.8 s, the finding splits it into four parts. Those are the wait for the HTML’s first byte, the HTML’s own download, the files that blocked the paint (the slowest named), and the rest before the paint (the long tasks in it, when a script held the thread). The fix is the costliest part’s. It replaces the bare FCP line; the render-blocking finding still lists every blocking file. |
| the document held open | The page painted early, but its HTML kept coming for 250 ms or more, and the islands woke only after its end. Islands wake once the whole document is in, so every island waited, even one that reads nothing late. When a load’s streamed promise held it, the finding names the page.data key that settled last. The fix: await it in the load, or make the part that needs it a server island (render: 'deferred'). |
| the first byte | When TTFB is over 800 ms: each step the browser timed before the page’s first byte, the ones that took time. Those are redirects, a service worker starting, the DNS lookup, connecting (and its TLS), and the request until the server answered. When the page sends a Server-Timing header, the finding quotes what the server says its time went to; the profiler adds its own render and CPU entries there for its user. The fix is the costliest step’s. It replaces the bare TTFB line. When the server’s wait is the cost, the Page tab adds what the Profiler tab’s last run of this page found: its render time, its outbound calls, and its own word on where the time went. With no run yet, it says to run one. A slow navigation’s fetch does the same for the page it went to. When the document’s Server-Timing carries the render (the profiler adds it for its user), the wait is split into the render and what came before it. On the dev server, most of that is the page compiling on its first request, so the finding says so and becomes a note: reload for the real first byte. |
| the worst shifts | When CLS is over 0.1: the worst burst of shifts (shifts under a second apart, five seconds at most, the way CLS scores them), what moved in it (the island, else the element), and what made it move. The cause is the thing that happened immediately before its biggest shift: a hole’s answer swapped in (not the size of its fallback), an image arrived with no size set, a web font swapped in, or an island hydrated. The fix is the cause’s, and when nothing the page timed explains it, the finding says what to look for. It replaces the bare CLS line. |
| the largest paint | When LCP is over 2.5 s: which element it was, which island it is in, and its four parts. They are the wait for the HTML’s first byte, the delay before the browser began fetching its file (found late: a CSS background, a script-added image, loading="lazy"), the download, and the delay after it arrived before it painted. A text paint has only the first byte and the render. The fix is for the part that cost most. It replaces the bare LCP line. |
| a slow in-app navigation | A router navigation took 400 ms or more before the new page showed (a warning past a second). The router times each one in three parts: fetching the page from the server, loading its stylesheets (it waits for them so the page never shows unstyled), and swapping it in. The fetch is split again by the page request’s own timing: the wait for the server’s first byte, and the download with its size. A page whose download is most of it gets the “send less HTML” fix instead of the server one. A link the router prefetched on hover says how much earlier its request began, and how long the visitor still waited after the click. It then counts the islands the new page woke, and names the heaviest. The fix follows the part that cost most. After the navigation, the Page tab shows the one that brought you here; the profiler report lists the slowest in the visit. |
| the slowest interaction | When INP is over 200 ms: which interaction it was (a click or a key press, and on what), which island it landed in, and where its time went. The browser splits it into three parts: the wait before its handlers could run, the handlers, and the paint after. The finding names what held each part. For the wait, that is an island that was hydrating, or a script in a long frame (its file, its function, and whether a timer, a promise or an animation frame ran it). For the handlers, it is the island’s own handler; Svelte sends every event through one dispatcher, so that is never named. The fix follows the part that cost most. It replaces the bare INP line. When the browser can sample the main thread, the finding names the function that ran instead: the handler’s own function, or the one that held the thread during the wait. A click during the load comes from the load’s trace. A later one comes from a second sampler, which waits up to a minute after the load for an interaction of 200 ms or more and keeps only that one. A line is quoted only when the frame went through a source map; otherwise you get the function and its file. A built chunk’s minified name is never quoted. |
| files fetched again | The browser asked the server again for content-named files it already had. Either each was revalidated (a round trip answered 304), or on a reload it came down whole. The island-files row above reads the header the host sends; this one is what the browser did about it on this visit. The profiler report raises it from the same visit. |
| a preload downloaded again | A file was fetched by a <link rel="preload"> and then downloaded again by the page: the preload did not match the request, so the browser could not use it. The finding names each file and the bytes paid twice, and the fix for its kind: a font preload needs crossorigin; a fetch preload needs crossorigin matching the request’s credentials; a module script wants modulepreload. |
| rules that match nothing | Half or more of the page’s CSS rules, 20 KB or more of them, match nothing on the page. The finding names the sheets that hold them. |
On the dev server modules load one by one, so load times run higher than in a build. The other findings hold in both.
On a Kit-hydrated (csr = true) page ogygia’s runtime never boots, so there are no islands, no wire and no byte ledger. In dev the dock still opens there (a build gives such a page no dock): it says so and still shows the Page tab, with the vitals, the findings about the page, the files, third parties and styles, all taken from the beacon that its boot starts.
After an in-app navigation (where ogygia’s router swaps the page without a reload), the tab reports on the new page, and its findings and island rows count from the navigation. An island that appears on both pages with the same props is kept by the router, still awake, with no code to load and nothing to hydrate. The tab lists such islands as kept, so a page whose islands were all reused does not read as “nothing woke”. The Styles section reads the new page’s sheets. The vitals stay those of the first page’s load, labelled as such, because a navigation paints nothing the browser reports. The load CPU trace also belongs to the first page, so record the main thread again for the new one.
Checked against planted problems
The playground’s /dt-lab page plants one problem per island next to healthy decoys. internal/bench/devtools-answer-key.ts loads it like a hurried visitor and checks that each problem is found on its island and that no decoy is named. /dt-third plants other origins (a blocking script that edits an island, and a script that loads three more). /dt-styles plants an unscoped component and a sheet of 600 rules for markup the page does not have. On /dt-many (320 islands, scrolled) the key checks the runtime itself: no island may wait with nothing ahead of it. internal/bench/devtools-click-check.ts presses every button, link, row and chip in every tab on each lab page, and each press must change something, throw nothing and stay on the page. internal/bench/profiler-click-check.ts does the same on a profiler report, the dashboard and the compare page. The same key also checks the profiler’s report of the same visits, and internal/bench/all-keys.ts runs every key, each against the server it needs.
Recording a session
The Record tab measures what you do on the page, not only the page load. Press Record, use the page (click, type, scroll, navigate) and press Stop, and the tab lists what happened during that stretch. Each finding points at its elements: hovering highlights them on the page, and “show on the page” scrolls to them.
A session survives a full page load (a plain link, a form post, a reload), because the page saves its part as it goes away and the next page’s devtools pick the session up. The report covers every page on one clock, with each page load marked on the timeline, and islands keep the names they had on their own page. The browser’s CPU samples stop at a full page load, because the sampler cannot be carried over, but everything else continues.
With the profiler on, and the server (on by default) records the server over the same stretch. “On the server, meanwhile” then lists the requests your actions made, the server’s hottest code across them, its outbound calls and its findings. A slow request also gets profile it on the server, which profiles that exact request in one click and shows where its time went. The session’s timeline puts everything on one clock, and each request bar shows its server part and, inside that, the upstream wait.
| Finding | What was measured |
|---|---|
| slow interaction | A click or key press took 100 ms or more to show a result. It splits the time into waiting, the handlers, and painting, and names the function that ran (from the CPU sampler), or the script (from the browser’s long animation frames). |
| repeated clicks | The same element was clicked 3 or more times within a second: to a visitor, nothing seemed to happen. |
| dead click | A click changed nothing: no DOM change it could cause, no request, no navigation. |
| late answer | A click’s island changed only after half a second: show a reaction at once. |
| long tasks | Main-thread tasks of 50 ms or more, and the function behind them. |
| layout moved by itself | A layout shift no input caused, and the island it came from. |
| requests | Failed ones (status 400 and up), slow ones (a second or more), and the same request sent again and again. |
| islands | Islands that woke during the session, and any that failed, threw away their server HTML, or were healed. |
| errors | Uncaught errors, unhandled rejections, and console.error calls. |
| DOM churn | An island (or code outside islands) that changed the DOM hundreds of times. |
| heap growth | The JS heap grew by 5 MB or more. |
| slow navigation | An in-app navigation took half a second or more. |
Checked against planted problems
The playground’s /dt-session page plants one problem per island (a slow handler, a late answer, a late banner, repeated and failed requests, an error, DOM churn), along with a dead button and a healthy decoy. internal/bench/devtools-session-key.ts records a session there the way a visitor would and checks that each problem is found and named and that the decoy is never named.
The Hydration tab
This tab lists each island on the page with its status:
- failed: its module threw while it loaded or mounted, shown with the error.
- recovered: Svelte threw the server DOM away and rendered again, shown with the runtime’s reason.
- healed: another script edited the island before it woke, and the runtime put the server markup back.
- markup changed: the browser’s first render differs from the server’s. Click the row to see the difference, which compares the server’s markup with the browser’s and shows only the changed pieces with a little context around them.
- clean, or asleep (not woken yet).
Hovering over a row highlights the island on the page. Clicking a row that has a difference shows it, and clicking any other row opens the island’s card.
A script that edits an island before ogygia’s runtime has started shows up as markup changed rather than healed, because the runtime’s copy of the server markup already contains the edit. The difference then shows the edit.
An island nested inside another awake island does not wake on its own, because it hydrates as part of the island around it. Once that island is awake it shows as clean (with “rides …, the island around it”), and never as asleep.
The profile on the page
After a run, the Profiler tab lists On this page, the measured costs that have a location on the page: the heaviest islands, the components (by the elements they rendered, on the dev server), a span split by element tag, the declarative shadow roots the server wrote (and how many the browser refused), and the largest paint. Hover over one to highlight its elements, or click it to pin the highlight and scroll there.
Inspecting one island
Click a roster row, or use “pick on page” and click a region, to open its detail card. The card shows the island’s identity, the full cost breakdown, its decoded props (as a collapsible tree with hub-ref chips), and a client-lifecycle mini-timeline (connected → scheduled → woke → hydrated). Under “in the browser” it shows the island’s row from the Page tab (module load, wait, hydrate step, layout shift) and every finding that names it. In a build, “cost” shows the island’s code: the files it needs and their size, only it (the bytes no other island needs, which is what removing it would save), and what it shares and with which islands.
Keyboard and screen readers
The dock is a heading with a tab list. Screen readers announce the tabs as tabs with the open one selected, and the panel below them is a region you can focus and scroll with the keyboard. Its text meets 4.5:1 contrast. Tab reaches everything you can click, and a row’s name is a button, so Enter opens the row the same way a click does. internal/bench/devtools-a11y-check.ts checks every tab with axe, measures the contrast of each piece of text against the background under it, and fails on any control that only a mouse can press.
Writing a sink
The dock is one reader of the runtime’s event stream, and you can add your own, for example to forward island wakes to your logging during a QA run or to attach a trace to a bug report. The stream exists where the devtools are compiled in, which means the dev server, and a build with the plugin listed in a browser that has turned the devtools on. Each event is plain JSON with a domain (compile, server, wire, runtime, hub or nav), a dotted name such as wake.fired or region.hydrate.done, and the fields the bus stamps on it: seq, t (ms on the page’s clock), realm (client or server) and the schema version v.
A sink is a function that receives each event. The functions below come from @ogygia/devtools, which re-exports them unchanged from ogygia/events. Register a sink from code that runs in the page, such as an island that wakes on load:
import { add_sink, snapshot, to_trace } from '@ogygia/devtools';
for (const event of snapshot()) forward(event); // what is already buffered
const stop = add_sink((event) => { // and each event from now on
if (event.name === 'region.hydrate.failed') forward(event);
});
const trace = to_trace(); // { kind: 'ogygia-devtools-trace', version, events }add_sink(fn)registers a sink and returns a function that removes it. A sink sees events from the moment it is added, so readsnapshot()first if you also want the earlier ones. A sink that throws is skipped and never breaks the page.snapshot()returns a copy of the buffered events, oldest first. The buffer keeps the newest 4,096, andclear()empties it.to_trace()wraps the buffer as a portable trace, with akindand aversionso a reader can check what it holds.ingest(events)adds events that were already stamped, such as those of a saved trace or another realm, to the buffer and the sinks without stamping them again. The runtime uses it to fold the server’s events for the page into the same stream.configure({ active, cap, sample })pauses the stream, resizes the buffer, or keeps one in N events of a noisy name.
Three sinks come ready-made. install_window_sink() publishes window.__ogygia_devtools (events(), on(fn), clear(), trace()), so a test runner or another tool can read the stream without importing anything; the runtime installs it on its own wherever the stream exists. install_postmessage_sink(target) posts each event to a worker, a MessagePort or a window as { __ogygia_devtools: event }. install_console_sink(filter?) logs each event, or each one filter accepts, with console.debug. Each returns its remover, apart from the window sink, which returns the hook it published.
Reading profiler reports in code
@ogygia/devtools/report reads a profiler report outside the profiler, on Node. It is what ogygia mcp loads from your app to open a downloaded report, and you can use it for your own tooling, for example a CI step that checks an exported report:
import { readFile } from 'node:fs/promises';
import { is_ogp, ogp_decode, is_dump, report_json } from '@ogygia/devtools/report';
const bytes = await readFile('slow-page.ogp');
const dump = is_ogp(bytes) ? await ogp_decode(bytes, process.env.EXPORT_KEY) : JSON.parse(bytes.toString());
if (is_dump(dump)) {
const report = report_json(dump.analysis, dump.meta, '/__profiler', dump.extras);
console.log(report.findings);
}is_ogp(bytes)says whether a file is an exported.ogprather than a plain JSON dump.ogp_decode(bytes, key)decrypts and unpacks an.ogpwith the key it was exported with, and throws on a wrong key or a changed file.is_dump(value)checks that a value is a report dump (the JSON behind/report/<id>/dump), with itsmeta,analysisandextras.report_json(analysis, meta, base, extras)builds the same JSON that/__profiler/report/<id>.jsonanswers, described under For agents and scripts.baseis the profiler’s path, used in the report’s links.fix_impact(patterns, renderMs, renders?, waitMs?)adds up what fixing a report’s patterns would take off the render, capped at 90 percent. It is the sum behind “about 326 ms off the 741 ms render”.
TIP
The devtools never mount on profiler pages, including the embedded frame in the Profiler tab, so the tools never inspect themselves.
`csr = true` pages
On a Kit-hydrated (csr = true) page the ogygia runtime never boots, so there are no islands to inspect. In dev the dock shows a notice that points you to a csr = false page; in a build such a page has no dock.