agentleFS
JoviDeCroockJoviDeCroock/pracht/skills/audit-bundles/SKILL.md

audit-bundles

Skill106 starsChanged 7 months agoMITView on GitHub

Analyze a pracht production build for client bytes per route, heavy vendor chunks, and large dependencies, and recommend code-splitting and prefetch changes. Use for "audit bundles", "why is my JS so big", "bundle size per route", "what's in my vendor chunk".

The file

We split this skill into the rules it gives an agent

9 rules. About 0 are common, 3 are distinctive, and 6 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈3

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 6

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    Do not treat exit 1 as "no output" — parse stdout anyway, or run with --no-budget-fail to keep the exit code clean.

    line 41
  2. Very specific:

    Only "full" routes pay route JS + shared entry.

    line 84
  3. Very specific:

    If the app declares budgets in the pracht plugin config, the Step 1 JSON also contains a budgets section with per-route pass/fail — lead …

    line 90
  4. Very specific:

    If every route ends up on "none", do not stop there — the prefetch listeners still ship, in a chunk the router lazily imports on every page.

    line 146
  5. Very specific:

    Verify any grouping change against the prerendered HTML, not just the numbers: a group broad enough to reshuffle entry chunks can drop the …

    line 183
  6. Distinctive:

    When a route's CSS is small and the site is entered cold — a static export, a content site arriving from search — the stylesheet link is …

    line 202
  7. Very specific:

    Always run pracht build --analyze --json first.

    line 226
  8. Distinctive:

    Use the Vite manifest as the source of truth — chunk names rotate per build.

    line 228
  9. Distinctive:

    Do not auto-edit. Bundle changes have render-blocking implications.

    line 233

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Pracht Audit Bundles

pracht performs route-level code splitting via the Vite plugin and emits a manifest. This skill reads that manifest, sizes each route's client payload, and surfaces the worst offenders.

Step 1: Build with analysis

MCP: if pracht dev-mcp is registered, prefer its inspect_*, doctor, and verify tools to the CLI. pracht inspect build needs a prior pracht build.

pracht build --analyze --json

This is the native per-route payload report: for every route (pattern + render mode) it emits the transitive client JS chunks with raw and gzip sizes (routes[].chunks), route-specific and total sums (routeGzipBytes, totalGzipBytes), and the shared entry chunks broken out (shared), sorted by total gzip descending. A stale dist/ produces misleading numbers — rebuild, don't reuse.

Exit-code footgun: when configured budgets fail, pracht build --analyze --json sets a nonzero exit code while still printing valid JSON on stdout. Do not treat exit 1 as "no output" — parse stdout anyway, or run with --no-budget-fail to keep the exit code clean.

For a human-readable table, use pracht build --analyze instead.

Step 2: Pull supporting metadata

pracht inspect build --json

Captures the resolved adapter, client entry URL, and CSS/JS manifest. Cross reference with dist/server/.vite/client-manifest.json for chunk metadata (file, imports, dynamicImports, css, isEntry) — needed for vendor fan-in analysis (Step 4) and CSS sizing, which the analyze report does not cover.

Size CSS from the cssManifest this command reports, never from dist/server/.vite/client-manifest.json. A hydration: "none" or "islands" route is absent from the client manifest by construction, so reading it directly reports no CSS for a page whose document links a stylesheet — under-reporting exactly the routes pracht recommends. inspect merges dist/server/css-manifest.json, the build's own record, over the client manifest; an older build without that file is client-only, and the CSS column should say so rather than read as zero.

Step 3: Interpret per-route payload

From the Step 1 JSON, report:

RouteHydrationRoute JS (gz)Shared (gz)Total (gz)CSS (gz)LCP-class?

gz = gzip size, taken directly from routeGzipBytes / shared.gzipBytes / totalGzipBytes. Size CSS chunks (from cssManifest) with zlib.gzipSync.

Account for the per-route hydration field the analyze JSON emits (omitted means "full") before judging sizes:

  • hydration: "none" routes ship 0 bytes of JS — the report already zeroes them out. Never flag them.
  • hydration: "islands" routes never load the shared client entry; their total is the islands bootstrap plus every island chunk in the app — an upper bound, since which islands a page actually uses is only known at render time. Treat their totals as pessimistic.
  • Only "full" routes pay route JS + shared entry.

LCP-class? is yes when total gz exceeds 200 KB — that's the order of magnitude where mid-tier mobile starts losing LCP budget. Apply the threshold after the hydration adjustment above.

If the app declares budgets in the pracht plugin config, the Step 1 JSON also contains a budgets section with per-route pass/fail — lead the report with any failures. If no budgets are configured, suggest adding them (see https://pracht.resynapse.dev/docs/reference/config), starting from the current sizes plus ~10% headroom.

Step 4: Vendor chunk health

Identify chunks under node_modules/. For each:

  • Size (raw and gz).
  • Number of route chunks that import it (fan-in).

A vendor chunk imported by every route is the framework runtime — expected. A vendor chunk imported by ONE route is a code-splitting opportunity (move the import inside that route's component or lazy-load it).

A vendor chunk over 100 KB gz that is imported by every route is worth a manual review — common offenders: date libraries, validation libraries, icon sets, charting libraries.

Step 5: Heavy dependencies in route components

For each route chunk over 50 KB gz, run pracht inspect build --json plus du-style analysis on the chunk contents:

  • Grep the chunk source for known heavy module headers (moment, lodash, chart.js, three, @stripe/stripe-js, etc.).
  • For each, recommend: (a) tree-shakeable alternative, (b) dynamic import inside an event handler, (c) lazy-load via lazy() from @pracht/core.

Step 6: Prefetch strategy

pracht inspect routes --json exposes prefetch per route on a current @pracht/cli. If your CLI version predates the field (it's absent from the JSON), fall back to grepping the route manifest source for prefetch:. Route-level strategies are "none", "hover", "intent", "viewport" ("render" exists only as a per-link override — see below). Recommend:

  • "viewport" for primary-nav links.
  • "hover" for content links inside long pages.
  • "intent" (default) is fine if you're not sure.
  • "none" for routes that are large and rarely visited (admin, settings) so hover doesn't preload them.

Cost model for "viewport": the IntersectionObserver fires once per anchor — it unobserves the anchor after the first prefetch, and prefetched route state is cached for 30 seconds — so a link in a marketing footer costs at most one prefetch per anchor per page view, not one per scroll. Viewport and render prefetches run three at a time, drop links that left the page, and are skipped on Save-Data or 2G connections. It's still wasteful for a 300 KB route that footer visitors rarely click; prefer "none" or "hover" there. Also note the per-link escape hatch: <Link prefetch="..."> overrides the route-level strategy for a single anchor (and accepts the extra "render" value — prefetch as soon as the link renders, the most eager option), so a route can stay on "intent" while its primary-nav link opts into "viewport" or "render".

If every route ends up on "none", do not stop there — the prefetch listeners still ship, in a chunk the router lazily imports on every page. Recommend pracht({ client: { prefetch: false } }) instead, which compiles the whole mechanism out (~2.6 KB gzip and one fewer request). Confirm nothing relies on prefetching first: the router silently stops honouring route({ prefetch }) and <Link prefetch>, and the imperative prefetch() export becomes a no-op. See https://pracht.resynapse.dev/docs/prefetching#shipping-less-javascript.

Step 6b: Chunk-group composition

App-level chunking and pracht's vendor chunk coexist — recommend it freely. pracht reads build.rollupOptions.output and contributes its Preact group in whichever form the app used, so adding a group (to merge the long tail of small initial chunks, say) does not cost the framework chunk:

// vite.config.ts
build: {
  rollupOptions: {
    output: {
      codeSplitting: { groups: [{ name: "app", tags: ["$initial"], minSize: 20_000 }] },
    },
  },
}

Two things to check when an app already configures chunking:

  • Mixed forms. Rolldown lets codeSplitting silence both advancedChunks and manualChunks. An app that sets more than one is running only codeSplitting; report the dead config as warn.
  • A group that swallows Preact. Precedence is higher priority first, then declaration order, with pracht's group last — so an app group matching node_modules at equal priority absorbs the framework runtime. If a separate, long-cached framework chunk was the intent, recommend a priority bump or a test that excludes Preact.

Verify any grouping change against the prerendered HTML, not just the numbers: a group broad enough to reshuffle entry chunks can drop the stylesheet links pracht injects per route. Compare <link rel="stylesheet"> tags in dist/client/**/index.html before and after.

pracht({ vendorChunk: false }) makes pracht contribute nothing, for an app that wants Preact inside its own chunks or places frameworkChunkGroups() itself.

pracht contributes one group to the server build too: each island becomes its own chunk, so a route that does not fully hydrate can resolve its CSS from that build without inheriting the server entry's merged stylesheet. An app that configures SSR chunking composes with it the same way the client side does; a group that pulls islands back into the entry shows up as extra <link rel="stylesheet"> tags on static pages, and the build warns.

build.cssCodeSplit: false is rejected outright — there is no index.html to link the single stylesheet it produces, so every page would ship without one.

When a route's CSS is small and the site is entered cold — a static export, a content site arriving from search — the stylesheet link is often the only render-blocking request left on a page that ships no JavaScript. Report pracht({ inlineCss: true }) as a suggestion there, with the cost: the same bytes in every document and no shared stylesheet cache. An app whose visitors move between pages should keep the link.

Step 7: Report

Three sections:

  1. Top 10 routes by total client payload — sorted desc.
  2. Top 10 vendor chunks by size — with fan-in.
  3. Suggestions — ordered by impact (KB saved × routes affected).

Tag every finding with a primary severity — error (budget failure), warn (LCP-class route, single-route vendor chunk), info (tuning opportunity) — and keep the size numbers as supporting detail.

Include before/after estimates for each suggestion: "Lazy-load chart.js inside Component: -180 KB gz off /dashboard/analytics."

Rules

  1. Always run pracht build --analyze --json first. A stale build is a source of bad advice.
  2. Use the Vite manifest as the source of truth — chunk names rotate per build.
  3. Report gzip size, not raw — the wire size is what users pay.
  4. Distinguish "shared" code (entry + framework) from "route-specific" code when reporting; users can only optimize the latter.
  5. Do not auto-edit. Bundle changes have render-blocking implications; surface and let the user choose.

$ARGUMENTS

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.