Performance profiling
SkillWeb & browsingLets your agent run performance profilers on the app and interpret the results like a human would.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Performance profiling skill
About this capability
Run the headless app, catalog-load, and ZIP profilers and interpret the JSON the way a human would from window.profiler.report(), profile:load, and the ZIP console table. Use when editing sources/canvas/, load-image, renderer, zip export, zip-helpers, performance-profiler, install-item-metadata, loa
What this skill tells your AI
The instructions your AI receives, as published by liberatedpixelcup/universal-lpc-spritesheet-character-generator in .agents/skills/performance-profiling/SKILL.md and read by ahel’s review.
Do not ask the user to open DevTools. Run the matching script, then read the JSON or the diff. Phase names and console commands: PERFORMANCE_PROFILING.md.
Which command
| What changed | Command | Notes |
|---|---|---|
loadImage(), renderCharacter(), hash hydration, preview, palette recolor | npm run profile:app | Live app, ?debug=true. Default --recolor both (WebGL + ?recolor=cpu). Headless Playwright Chromium is SwiftShader; real GPU: --headed --channel chrome. |
loadAllMetadata, metadata chunks, catalog bootstrap | npm run profile:load | Production vite preview, not Vite serve. Median of 5 navigations. Port 4178 (APP_LOAD_PROFILE_PORT). |
| Generated metadata payload bytes | npm run metadata:size / metadata:size:check | Generator output, not Vite chunks. CI gates 500 KiB item / 600 KiB pair. |
| Drawing, slicing, or PNG encode | npm run profile:zip:quick | Fake JSZip. Ignore generateZip. |
Real zip packaging (generateAsync, zip-helpers) | npm run profile:zip | Real JSZip. Slower. |
These are not interchangeable. Match the command to the change. The ZIP pair
is not a substitute for profile:app, and profile:load is not a substitute
for either (Vite serve pretty-prints metadata; window.profiler starts after
catalog import). Layout shift during load is cls, not a
row on this table.
Before any of them: npx playwright install once. ZIP scripts need
dist/*-metadata.js (npm run dev or npm run build once) and use
npx serve. profile:app starts Vite serve on 127.0.0.1:5178 (override
APP_PROFILE_PORT) unless you pass --url http://127.0.0.1:5173.
profile:load runs vite build then vite preview on 127.0.0.1:4178
unless you pass --url.
A full ZIP run can take several minutes (up to 10). App profile with both
recolor modes is usually 2–4 minutes. profile:load is a production build
plus five homepage navigations. Request full permissions; Playwright
needs a real browser. If PLAYWRIGHT_BROWSERS_PATH points at a Cursor
sandbox cache, the script unsets it.
Workflow
If the change is not yet made, take a baseline first. Use the matching
pair (:quick with :quick, profile:app with profile:app).
npm run profile:app:baseline
# …make the change…
npm run profile:app
npm run diff:app-profile -- tmp/baseline-app-profile.json tmp/app-profile.json
Catalog bootstrap / metadata payload:
npm run profile:load:baseline
# …make the change…
npm run profile:load
npm run diff:app-load-profile -- tmp/baseline-app-load-profile.json tmp/app-load-profile.json
A few milliseconds on indexReadyMs / liteReadyMs is noise; 50 ms+ median
is worth a second look (first-paint gates). A 50 ms+ move on
catalogReadyMs alone can be the credits/palette tail. diff:app-load-profile
always exits 0.
diff:app-profile prints two sections when both files contain both
modes: ======== webgl ======== and ======== cpu ========. Compare
like-with-like. One mode only: --recolor webgl or --recolor cpu.
npm run profile:zip:baseline:quick
npm run profile:zip:quick
npm run diff:zip-profile -- tmp/baseline-zip-export-profile-quick.json tmp/zip-export-profile-quick.json
If the change is already made and tmp/baseline-*.json exists from this
machine, diff against it. If there is no baseline, report the absolute
numbers and flag outliers. Do not invent a baseline after the fact.
JSON lands under tmp/ (gitignored) and is also printed on stdout.
Interpreting
A positive Δ means the after run was slower. Compare on the same machine.
A few milliseconds is noise. renderCharacter can swing tens of ms between
two runs; repeat once if a single Δ is the only evidence.
Confirm activeMode and recolorStats in each section: CPU must show
activeMode: cpu and cpu > 0 if the outfit recolours. WebGL should show
webgl > 0 (or cpu if this Chromium has no GL). Confirm renderer
(unmaskedRenderer): SwiftShader / llvmpipe is software GL, not the user's
GPU. Hardware looks like ANGLE (Apple, … Metal …) or an NVIDIA/AMD name.
--headed --channel chrome fails the run if the renderer is still software.
App profile (profiler.snapshot(), same buckets as profiler.report()):
renderCharacter— compositing only (not dynamic-import wait)snapshot().renderCharacter.calls[]— per-stepphasesMs/countersfor each completed render (mithrilRedrawStart,buildDrawCalls,sizeCanvas,loadImages,recolor,draw,customLoad,customRecolor,customDraw,mithrilRedrawEnd)image-load:<path>— one span per network loadhash-loadSelectionsFromHash— URL hash hydration- Category totals:
imageLoads,draws,previews,domUpdates - Slow-operation threshold is 50ms (
slowThresholdMs) diff:app-profileprints── renderCharacter phases ──(call 0 vs call 0, call 1 vs call 1)
Catalog load profile (profile:load, production vite preview):
indexReadyMs— navigation time origin to__LPC_waitCatalogIndexReady()liteReadyMs— navigation time origin to__LPC_waitCatalogLiteReady()(hash /initCanvaswait on index+lite)catalogReadyMs— navigation time origin to__LPC_waitCatalogAllReady()(credits, palette, layers too)catalog-load/catalog-chunk:*— User Timing around each metadata import- metadata
*-metadata*resources —transferSize,decodedBodySize,duration - A few milliseconds is noise; 50 ms+ on
indexReadyMsorliteReadyMsmedian is worth a second look
ZIP profile (phasesMs / metadata):
render_imageLoadDecode_*/render_composite_*drawAndSlice,pngEncode,zipFile,staticFilesgenerateZip— only meaningful on the real-JSZip run
The default character is the full outfit in
zip-profile-default-hash.ts,
not whatever was last open in a browser. profile:app then deselects the
layered gear (body + head + expression only).
Limit one ZIP export: npm run profile:zip -- --only splitAnimations
(also splitItemSheets, splitItemAnimations, individualFrames).
Custom app hashes: npm run profile:app -- --hash '…' --hash2 '…'.
Real GPU: npm run profile:app -- --headed --channel chrome (and the same
flags on profile:app:baseline). Compare GPU baselines only to GPU runs.
Still ask the user
WebGL vs CPU visual correctness is not this skill: canvas-render. Timing both recolor paths is.
Signals
- GitHub stars
- 2k
- Forks
- 580
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
performance-profiling-liberatedpixelcup- Source
- github.com/liberatedpixelcup/universal-lpc-spritesheet-character-generator