skills/performance-first/SKILL.md
--- name: performance-first description: Make apps feel instant. The core rule — navigation must never block on data: change route immediately, render a layout-matched skeleton, stream/fetch the data in. Covers Next.js App Router (loading.tsx, Suspense streaming, Link prefetch, useOptimistic) and TanStack Start/Router (deferred loaders + pendingComponent, TanStack Query cache + prefetch-on-intent, optimistic mutations), plus shared rules (skeletons over spinners, prefetch on hover, stale-while-r
npx skillsauth add RonanCodes/ronan-skills skills/performance-firstInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
The rule: a route transition must never wait for data. Click a link → the new page appears now with a skeleton that matches its final layout → data streams in and fills it. Spinners-on-blank and "freeze until loaded" are banned. This is a baseline for every app, not a late optimisation.
Why: perceived speed is navigation latency. A page that paints instantly (even as a skeleton) feels fast; one that hangs for 400ms on a query feels broken, no matter the real numbers.
For ANY app with client-fetched data, use TanStack Query by default — its cache makes revisiting a page instant (served from cache, revalidated in the background), which is exactly the "snappy between pages" feel we want. Set it up at scaffold time, don't retrofit:
QueryClientProvider at the root; sensible defaults (staleTime ~30s, gcTime minutes, refetchOnWindowFocus off unless needed).queryClient.prefetchQuery).onMutate → cancel, snapshot, patch, rollback on error) so writes feel instant.The one caveat — Next.js App Router (RSC) apps: if pages are server components reading data on the server (e.g. RLS-bound Supabase reads in a page.tsx), the instant-nav job is already done by loading.tsx skeletons + <Link> prefetch + streaming — and bolting React Query over server-rendered pages is a downgrade (you'd lose RSC streaming + server auth). There, reach for TanStack Query only for the genuinely client-side, frequently-refetched islands (live lists, polled data, optimistic widgets), not the whole page. So: TanStack Query is the default for the client data layer; on RSC the route-level instant-nav comes from loading.tsx + prefetch. Pick per where the data is fetched, never rip out working server reads to force React Query in.
await the whole page before painting.dynamic()-import heavy/below-the-fold client code; keep the client JS small.loading.tsx per route segment — Next shows it instantly on navigation while the async server component resolves. This alone fixes "page hangs on data".<Suspense> boundaries around the slow parts with skeleton fallbacks → the static shell paints first, slow data streams in. Don't await everything at the top of the page component; isolate slow fetches in child components wrapped in Suspense.<Link> prefetches in-viewport routes in prod by default; keep links real <Link>s.useOptimistic (or useTransition + optimistic state) for server-action mutations so the UI updates before the round-trip.// app/agenda/loading.tsx — instant on navigation, no data wait
export default function Loading() {
return <div className="space-y-3">{[0,1,2].map(i => <div key={i} className="h-20 animate-pulse rounded-lg border bg-muted/40" />)}</div>;
}
defer() for slow data, with a pendingComponent (skeleton) on the route → the router renders pending UI instantly and resolves data after.staleTime so revisiting is instant from cache; queryClient.prefetchQuery on hover/intent; background refetch keeps it fresh.<Await> to render deferred loader data with a Suspense-style fallback.useMutation onMutate + rollback in onError.export const Route = createFileRoute('/bids')({
loader: ({ context }) => ({ bids: defer(context.queryClient.ensureQueryData(bidsQuery)) }),
pendingComponent: BidsSkeleton,
});
loading.tsx (Next) or pendingComponent (TanStack).<Suspense> / <Await>, not blocking the shell.housapp-build-session).testing
--- name: linear-pipeline description: The Fable orchestrator for a single dispatched Linear ticket. Holds almost no context itself; it receives `--issue <ID> --detached`, decides the stage sequence, and fans out a sub-agent per stage, passing forward only each stage's artifact (never re-derived, never inlined into its own context). Step zero, before any planning or stage routing, is a boundary triage against `canon/security-boundary.md` (#199): a match tags Ronan Connolly and stops the run, no
development
--- name: in-your-face description: Capture a chat-only answer into a durable artifact (markdown + HTML, PDF when cheap) and launch it automatically so the user cannot miss it. Use when user says "in your face", "don't let me lose this", "save that answer", "make that durable", or right after answering a substantive side question (a recipe, comparison, how-to, or generated prompt) that would otherwise die with the context. category: workflow argument-hint: [--no-open] [--vault <short>] [hint of
tools
One-shot headless OpenAI Codex CLI calls for background/admin AI tasks — summaries, classification, extraction, admin glue. The default engine for anything that runs AI constantly in the background (daemon-driven, per-event), because it bills the flat ChatGPT subscription instead of Claude usage or per-token API spend, and it keeps working while Claude is rate-limited. NEVER for coding — coding stays Claude. Use when a skill or daemon needs a cheap always-on AI call, when the user says "use codex", "ask codex", "codex as backup", or when building a background summarizer/classifier into a listener or loop. Reads auth from ~/.codex/auth.json (ChatGPT account, no API key).
research
Turn a warranty rejection, repair quote, or RMA email into a cited decision brief — legal read (NL/EU consumer law), is the part user-serviceable, live part and new-unit prices, repair-vs-DIY-vs-new economics, before-you-send-it checklist, deadlines. Use when the user pastes or screenshots a repair quote, warranty rejection, "not covered" email, onderzoekskosten fee, or asks "should I repair or replace this".