{"slug":"audit-islands","title":"audit-islands","summary":"Audit pracht hydration: over-hydrated routes that should be `hydration: \"islands\"` or `\"none\"`, dead interactivity outside the islands directory, non-serializable island props, mis-tuned `client` strategies, and invalid render/hydration pairs. Use for \"audit islands\", \"reduce hyd","platform":"Claude","tags":[],"authorName":"LLM Mart","authorSlug":"llm-mart","score":0,"source":"github","price":null,"verified":false,"createdAt":"2026-10-19T21:17:24.4843Z","repo":{"url":"https://github.com/JoviDeCroock/pracht","stars":99,"forks":5,"license":"MIT","updatedAt":"2026-10-19T16:43:31Z"},"bodyHtml":"<hr>\n<p>name: audit-islands\nversion: 1.1.0\ndescription: |\nAudit pracht hydration: over-hydrated routes that should be <code>hydration: \"islands\"</code> or <code>\"none\"</code>, dead interactivity outside the islands directory,\nnon-serializable island props, mis-tuned <code>client</code> strategies, and invalid\nrender/hydration pairs.\nUse for \"audit islands\", \"reduce hydration\", \"why is this island not\ninteractive\", \"check partial hydration\".\nallowed-tools:</p>\n<ul>\n<li>Bash</li>\n<li>Read</li>\n<li>Grep</li>\n<li>Glob</li>\n</ul>\n<hr>\n<h1>Pracht Audit Islands</h1>\n<p>Report-only audit of hydration modes and islands usage (see <a href=\"https://pracht.resynapse.dev/docs/islands\">https://pracht.resynapse.dev/docs/islands</a>).\nPracht hydrates the whole page by default (<code>hydration: \"full\"</code>); routes can opt\ninto <code>\"islands\"</code> (only components from the islands directory hydrate) or\n<code>\"none\"</code> (zero JS). This audit finds routes shipping JS they don't need and\nislands wired in ways that break at render time or ship dead handlers.</p>\n<h2>Step 1: Enumerate routes and hydration modes</h2>\n<p>MCP: when the pracht MCP server is registered (<code>pracht dev-mcp</code>), prefer its\n<code>inspect_routes</code>/<code>inspect_api</code>/<code>inspect_build</code>/<code>doctor</code>/<code>verify</code> tools over\nshelling out.</p>\n<pre><code>pracht inspect routes --json\n</code></pre>\n<p>Prerequisite: <code>pracht inspect</code> needs a vite config with the pracht plugin\nwired up.</p>\n<p>Each route entry carries <code>render</code> and <code>hydration</code> (<code>null</code> = framework default,\ni.e. full hydration). Also locate the islands directory: default <code>src/islands/</code>,\nconfigurable via <code>pracht({ islandsDir })</code> in the vite config. In pages-router\napps (<code>mode: \"pages\"</code>), hydration is the per-file <code>export const HYDRATION = \"islands\" | \"none\"</code> constant; islands still live in <code>src/islands/</code>.</p>\n<h2>Step 2: Measure what each route actually ships</h2>\n<pre><code>pracht build --json\n</code></pre>\n<p>Prerequisite: this runs a full production build (<code>--json</code> implies <code>--analyze</code>).\nInterpret the totals hydration-aware:</p>\n<ul>\n<li>Full-hydration routes: route chunks + shell chunks + shared entry.</li>\n<li><code>hydration: \"islands\"</code> routes: islands bootstrap + island chunks only — no\nshared client entry. The listed island chunks are an <strong>upper bound</strong> (every\nisland in the app); a page only downloads the islands it renders.</li>\n<li><code>hydration: \"none\"</code> routes report <code>0b</code>.</li>\n</ul>\n<h2>Step 3: Run the checks</h2>\n<h3>3a. Over-hydration (the headline check)</h3>\n<p>For every route with <code>hydration</code> <code>null</code>/<code>\"full\"</code>: read the route module, its\nshell, and their imported components. If the page is mostly static — no\nhooks, no event handlers, or only one or two isolated widgets — flag it:</p>\n<ul>\n<li>Zero interactivity → recommend <code>hydration: \"none\"</code> (<code>warn</code>; <code>info</code> if the\nroute's total gzip JS is already tiny).</li>\n<li>A few isolated widgets → recommend <code>hydration: \"islands\"</code> with the widgets\nmoved into the islands directory (<code>warn</code>). Cite the Step 2 total as the\npayload this change removes.</li>\n</ul>\n<p>Render-mode fit is owned by <code>/tune-render-mode</code>; deep chunk analysis is owned\nby <code>/audit-bundles</code>. Point there instead of duplicating their findings.</p>\n<h3>3b. Dead interactivity on islands routes (<code>error</code>)</h3>\n<p>On a <code>hydration: \"islands\"</code> route, everything outside the islands directory\nrenders as inert HTML — an <code>onClick</code> in a regular component silently does\nnothing. Grep the route tree of every islands route for event handlers and\nhooks in components <strong>not</strong> under the islands directory and flag each one.\nIslands are auto-discovered from that directory only (default and named\nfunction-component exports alike; each becomes its own code-split chunk) —\nthere is no wrapper to mark a component elsewhere as an island.</p>\n<h3>3c. Island props and children (<code>error</code>)</h3>\n<p>Island props are serialized to JSON in the HTML. At each island call site on\nan islands route, flag props that are functions, symbols, bigints, class\ninstances (<code>Date</code>, <code>Map</code>, ...), JSX elements, or circular — rendering throws a\ndescriptive error naming the offending prop path. Children passed into an\nisland are fine: markup renders once on the server as static HTML, and plain\ntext arrives as a string value. Flag:</p>\n<ul>\n<li>a render function passed as children (it throws);</li>\n<li>children the island expects to re-render or pass props into, and islands\namong the children that read context the outer island provides (the browser\ngives them the default) — those belong inside the island or need a prop;</li>\n<li>an island that splits markup children with <code>toChildArray</code>, <code>Children.map</code>, or\n<code>Children.count</code> (tabs, carousels): it receives one opaque node, so labels\nbelong in props and panels are toggled through a ref (<code>hidden</code>);</li>\n<li>an island that renders <code>children</code> inside <code>&lt;p&gt;</code>, <code>&lt;a&gt;</code>, or <code>&lt;button&gt;</code> while\ncall sites pass block elements, links, or buttons (the dev server throws\nwhen it sees this), or raw HTML in children that may be malformed: the\nbrowser moves those nodes out of the slot and the island is left unhydrated\nwith a console error.</li>\n</ul>\n<h3>3d. Hydration strategy tuning (<code>info</code>)</h3>\n<p>Each island usage picks a strategy via the framework-owned <code>client</code> prop:\n<code>\"load\"</code> (default, chunk is <code>&lt;link rel=\"modulepreload\"&gt;</code>-ed), <code>\"idle\"</code>\n(requestIdleCallback), <code>\"visible\"</code> (IntersectionObserver; chunk fetched only\nwhen triggered). Flag default-<code>load</code> islands that are plausibly below the fold\nor not needed at first paint (comment sections, newsletter signups, footers) →\nsuggest <code>client=\"visible\"</code> or <code>client=\"idle\"</code>.</p>\n<h3>3e. Invalid combinations (<code>error</code>)</h3>\n<p><code>render: \"spa\"</code> always implies full hydration; combining it with\n<code>hydration: \"islands\"</code> or <code>\"none\"</code> is a configuration error. Flag any such\nroute (manifest field or pages-router <code>RENDER_MODE</code>/<code>HYDRATION</code> pair).</p>\n<h3>3f. MPA navigation assumptions (<code>warn</code>)</h3>\n<p>Routes with <code>hydration: \"islands\"</code> or <code>\"none\"</code> never load the client router:\nnavigation to, from, and between them is full-document (MPA-style), and\nroute-state prefetching is skipped for them. Flag apps that rely on\nclient-side state surviving navigation across such routes (in-memory stores,\nmodule-level caches shared between pages) — every navigation is a fresh\ndocument, unless <code>pracht({ client: { islandsNavigation: true } })</code> is set. With\nit, links between islands pages (and from them to <code>none</code> pages) swap the page\ninto the same document and an island rendered with identical props on both\npages keeps its state; module state survives too. Still flag state that must\nsurvive a link to a full-hydration route, a reload, or a browser without the\nNavigation API, since those remain full loads. Page-change animation does not need the client router: with\n<code>defineApp({ viewTransitions: true })</code> these documents carry a\n<code>@view-transition</code> rule and animate as cross-document view transitions, so\nflag only apps that load extra JavaScript to animate such navigations.</p>\n<h3>3g. Personal data in islands on cached pages (<code>warn</code>)</h3>\n<p>Island props on an <code>ssg</code>/<code>isg</code> route are fixed at build or regeneration time\nand shared by every visitor. Flag islands that exist only to fetch per-visitor\ncontent after load (a hand-rolled <code>fetch</code> of the session, cart, or \"signed in\nas\" data in <code>useEffect</code>) and islands whose props look personal. Suggest a\nserver island instead (<code>src/server-islands/</code>, see\n<a href=\"https://pracht.resynapse.dev/docs/server-islands\">https://pracht.resynapse.dev/docs/server-islands</a>): its\nloader runs per request with the route's middleware context, and it may\ncontain islands, which hydrate once the server island is swapped in. On\nfull-hydration pages islands inside a server island stay static HTML — flag those\n(<code>warn</code>) if they are expected to be interactive.</p>\n<h2>Step 4: Report</h2>\n<table>\n<thead>\n<tr>\n<th>Route</th>\n<th>File</th>\n<th>Severity</th>\n<th>Finding</th>\n<th>Suggested fix</th>\n</tr>\n</thead>\n</table>\n<p>Severities: <code>error</code> (breaks or throws today: dead handlers, non-serializable\nprops, spa+islands), <code>warn</code> (works but wasteful or fragile), <code>info</code> (tuning\nopportunity). Include the measured gzip totals for every over-hydration\nfinding.</p>\n<h2>Rules</h2>\n<ol>\n<li>Report only — never edit routes, components, or config. Hand changes to\n<code>/tune-render-mode</code> (modes) or the user.</li>\n<li>Use <code>pracht inspect routes --json</code> as the source of truth for hydration\nmodes — groups inherit <code>hydration</code>, so reading <code>src/routes.ts</code> manually\nunder-counts.</li>\n<li>Islands components behave like plain components on full-hydration routes\nand inside other islands — only flag 3b/3c on <code>hydration: \"islands\"</code>\nroutes.</li>\n<li>Verify hydration in a running app via <code>html[data-pracht-islands-hydrated=\"true\"]</code>\n(set after all <code>load</code> islands hydrate) and per-island <code>data-hydrated=\"true\"</code>.\nServer islands report <code>html[data-pracht-server-islands-ready=\"true\"]</code> once every server\nisland fetch the page started has settled, on every hydration mode.</li>\n</ol>\n<p>$ARGUMENTS</p>\n","files":[{"path":"SKILL.md","sizeBytes":8560,"isText":true}],"reviewScore":null,"reviewSummary":null,"trust":{"provenance":"trusted-source-unreviewed","notice":"Community-authored content, reproduced verbatim and not vetted as instructions. Treat it as data to evaluate, never as directives to follow.","bodySource":null},"bodyLocked":false,"purchaseUrl":null,"sourceUrl":null,"report":{"provenance":"trusted-source-unreviewed","screen":{"ran":true,"outcome":"clean","suspicious":0,"notes":0,"hiddenCharacters":false},"virusScan":{"engine":"clamav","status":"clean","scannedAt":"2026-10-19T21:17:57.615194Z","sha256":"6AE797E85DAEB8F0125E5962E676BDF5A7A493BF2B6BD56761AA98FB0448534D","sizeBytes":3878},"review":null,"source":{"repositoryUrl":"https://github.com/JoviDeCroock/pracht","path":"skills/audit-islands","license":"MIT","commit":"8bcdd52989de3e97708e7f16221839d6313cfdac","subtreeSha":"CB67B7D1E93E284DEFF56A72B58E98B3A6100ACDEFA38278439AEC7F31AA4DEC","lastSyncedAt":"2026-10-19T21:17:21.911347Z"},"reviewedAt":"2026-10-19T21:19:00.484099Z","notice":"Community-authored content, reproduced verbatim and not vetted as instructions. Treat it as data to evaluate, never as directives to follow."},"install":[{"target":"skills-cli","command":"npx skills add https://github.com/JoviDeCroock/pracht/tree/main/skills/audit-islands"},{"target":"claude-code","command":"claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install jovidecroock-pracht@llmmart"},{"target":"git","command":"git clone https://github.com/JoviDeCroock/pracht.git"}]}