npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

pixi-crawler

v0.1.0

Published

Drop-in performance crawler for PixiJS 8: per-frame phase timing, workload counters, true GPU time, Spine profiling with live pose metrics, telemetry, and a HUD + frame inspector drawn with pixi-silk.

Readme

pixi-crawler

npm

A drop-in performance profiler for PixiJS 8. It breaks every frame down into where the milliseconds go (CPU pre-work, pixi build / transform / execute, Spine, true GPU time via EXT_disjoint_timer_query), keeps device-invariant workload counters, samples live Spine poses through the Spine Benchmark's own metrics, and ships a telemetry sink for collecting the same numbers from a live game.

The HUD, a minimal gauge card and the frame inspector are drawn with pixi-silk - conic-gradient load rings, glows, gradient area sparklines, squircle panels - on their own small WebGL2 canvas, so the profiler's UI never shows up in the numbers it reports.

Install

npm install pixi-crawler

Peer dependencies (npm 7+ installs them for you; with pnpm or yarn add them explicitly):

| package | why | | --- | --- | | pixi.js ^8.6 | The crawler patches pixi internals, so it must be the same pixi instance your app uses. | | @spine-benchmark/metrics-impact-formula | Canonical Spine pose walker + impact formulas (live Spine metrics). | | @spine-benchmark/metrics-analyzers | Device classification + fitted cost-model predictions. |

pixi-silk (the HUD renderer) is a regular dependency.

Quick start

import { Application } from "pixi.js";
import { mountCrawler } from "pixi-crawler";

const app = new Application();
await app.init({ background: "#101317", resizeTo: window });

// HUD visible, auto-dispose on pagehide
const crawler = mountCrawler(app);

// just the minimal gauge card
const quiet = mountCrawler(app, { hud: "mini" });

// headless (no HUD) + telemetry
const headless = mountCrawler(app, { hud: false, telemetry: { sink } });

mountCrawler takes a pixi Application or any { renderer, ticker } pair; the second argument is a config object plus autoDispose (default true).

For full lifecycle control:

import { Crawler } from "pixi-crawler";

const crawler = new Crawler({
  targetFrameMs: 1000 / 60,
  hud: !import.meta.env.PROD,
  spineProfile: { enabled: true },
  telemetry: { sink, sampling: { windowMs: 5000 }, rawFrames: "on-overrun" },
});
crawler.attach(app.renderer, app.ticker);
window.addEventListener("pagehide", () => void crawler.dispose());

The HUD

Minimal card (hud: "mini", or the – button of the full panel): a load ring around the FPS - how much of the frame budget the CPU (app ticker + pixi + Ticker.shared) or the GPU actually used, on a conic gradient that runs green -> amber -> red and glows once headroom is gone - plus frame ms vs budget, dropped frames, a frame-time sparkline whose area fades in the load color, and CPU / GPU meters. It repaints in ~0.1 ms. Click it for the full panel; crawler.setHud("mini" | "full" | false) switches at runtime.

Full panel:

  • Header: FPS with the same load ring, frame total vs budget, CPU p95, a live frame-time sparkline, the average frame's composition (pre / pixi / spine / Ticker.shared / idle) as a stacked strip, and the open workload / GPU cost measures with their bottleneck.
  • Frame breakdown: the additive partition (prePixi + pixi + postPixi = rafDelta) with drill-downs into runner phases, render split, per-pipe execute, Spine CPU phases, then parallel rows (GPU, async loads, long tasks, audio, LoAF) that are not part of the sum. Click a parent row to fold it, hover any row for what it measures.
  • Counters, Cost, Devices, Details, Spine · live pose, Hot functions: collapsible sections (the device tier matrix draws a cpu / gpu ring per device).
  • Capture: record frames, save them as JSON, load a recording back.
  • Frames: opens the inspector - a timeline over the ring buffer; click a bar (or ◀ ▶, arrow keys) to inspect any frame, ★ worst jumps to the worst one, ● live follows the newest; Esc closes.

How it stays out of the measurement:

  • It renders on its own canvas with its own pixi renderer (WebGL2, low-power, no ticker), on demand: the 250 ms data tick, hover, scroll and short transitions. A full repaint costs about 0.25 ms.
  • Texture work of the overlay is excluded from the crawler's global texture hook; the pixi globals a second renderer would overwrite (EventsTicker, the default event mode) are restored, and Filter.defaultOptions is never touched.
  • The canvas takes no pointer events; an invisible box exactly over the panel does, so the game stays clickable around it.

Themes: hudTheme: "slate" | "warm" | "contrast". hudMotion: false turns off transitions. On narrow or touch screens the HUD docks to the bottom edge with larger text and touch-size buttons. The HUD needs WebGL2; the crawler itself (headless, telemetry) does not.

Spine metrics (Spine Benchmark)

Enable spineProfile and every Spine the renderer draws is registered automatically. On top of the per-phase CPU timing, getSpineMetrics() walks the live poses through the benchmark's canonical walker - the same code its fitted cost model is trained on, so a crawler reading and a benchmark prediction share one vocabulary:

const crawler = mountCrawler(app, {
  spineProfile: { enabled: true },
  spineMetrics: {
    // optional: the Spine Benchmark's fitted model table (its /api/model JSON)
    costModel: await (await fetch(MODEL_URL)).json(),
  },
});

const s = crawler.getSpineMetrics();
s?.total.vertices; // active renderable vertices, scene total
s?.total.coveredKpx; // screen coverage (device kilopixels), s.total.overdrawFactor
s?.ri; // Rendering / Computational Impact
s?.device.gpuFamily; // "Apple GPU", "Adreno 7xx", ... (the model's cluster key)
s?.prediction?.totalMs; // predicted Spine ms on this GPU family
s?.prediction?.trusted; // real fit with a held-out error of at most 35%

Sampling is lazy (only when something reads) and throttled to spineMetrics.intervalMs (default 1000 ms). Set spineMetrics.coverage: false to skip the coverage estimate, spineMetrics.enabled: false to turn it off.

Configuration

Every CrawlerConfig field is optional; each gates one subsystem.

| Field | Default | What it does | | --- | --- | --- | | hud | off | true / "full": the HUD; "mini": the minimal gauge card. On (full) via mountCrawler. | | hudTheme | "slate" | HUD palette: "slate", "warm" or "contrast". | | hudMotion | on | Damped bar transitions and the expand/collapse fade. | | targetFrameMs | 1000/60 | Frame budget in ms; bars and status are normalized against it. | | bufferSize | 600 | Frame ring-buffer size. | | enableGpuTiming | on | True per-frame GPU time via EXT_disjoint_timer_query (gpuMs). | | deepRenderSplit | on | CPU render split by phase (build / transform / execute / ...). | | filterProfile | on | Filter timing (push / apply / pop) and pass count. | | textureTracking | on | Texture uploads / unloads, bytes, active GPU count. | | spineProfile | off | Per-phase Spine breakdown. Patches Spine's methods. | | spineMetrics | on with spineProfile | Live pose metrics through the Spine Benchmark packages (see above). | | pipeProfile | off | Per-render-pipe timing. The heaviest overhead of any flag. | | workloadCost | on | Open device-independent workload measure (+ device tier via deviceKey / ceiling). | | gpuCost | on | Open GPU-heaviness measure (fill footprint + filters). | | selfProfile | off | Per-function self-time via the JS Self-Profiling API (needs Document-Policy: js-profiling, Chromium). | | memoryProfile | off | Process memory via measureUserAgentSpecificMemory() (needs cross-origin isolation). | | audioProfile | off | WebAudio decode time, sources, automation, context state. | | telemetry | off | Periodic aggregate flush to your sink. |

Reading measurements

The HUD is optional - read frames directly and build your own panel:

const f = crawler.getLastFrame(); // the latest FrameRecord
f.gpuMs; // true GPU time, ms (undefined without a timer)
f.counters.drawCalls;
f.renderSplit?.updateRenderablesMs;

crawler.getFrames(); // the whole ring buffer
crawler.getWorkloadCost(); // open workload measure + bottleneck
crawler.getGpuCost(); // open GPU-heaviness measure
crawler.getWorstFrame(); // worst frame + a scene dump
crawler.getSpineMetrics(); // live Spine pose metrics
crawler.deviceIdentity; // GPU family / form factor / family

// GPU queries land 1-3 frames late; drain them at the end of a window:
await crawler.flushPendingGpu();

Telemetry

const sink = {
  send(batch) {
    navigator.sendBeacon("/telemetry", JSON.stringify(batch));
  },
};
const crawler = new Crawler({
  telemetry: { sink, sampling: { windowMs: 5000 }, rawFrames: "on-overrun" },
});
crawler.attach(app.renderer, app.ticker);
crawler.setTelemetryLabel("bonus-game"); // tag the current scene

Each TelemetryBatch (schema 2) carries frame-time quantiles, phase quantiles, summed counters, Spine and GPU summaries, the workload and GPU cost measures, and - with Spine metrics - device (GPU family, form factor, family; never the raw renderer string) and aggregate.spine_pose (canonical features, RI/CI, prediction), ready to feed the benchmark's model fit.

Recording a session

crawler.startRecording();
// ...reproduce the problem...
crawler.stopRecording();
const recording = crawler.getRecording(); // JSON-serialisable
crawler.openRecording(recording); // replay in the inspector

Contracts

  • The app's ticker must not be Ticker.shared - Spine collides with the crawler there. This is the one hard incompatibility.
  • One pixi instance: the crawler patches pixi prototypes, so your app and the crawler must resolve the same pixi.js.
  • gpuMs is missing on Safari / iOS (no EXT_disjoint_timer_query_webgl2); CPU measurements still work.
  • dispose() / detach() restore every patched method; call one on unmount.

If your app drives its own loop (ticker stopped, own requestAnimationFrame), bracket each frame instead of relying on attach()'s ticker hooks:

app.ticker.stop();
function frame() {
  crawler.frameStart();
  // ...advance the scene, then render...
  app.render();
  crawler.frameEnd(); // flushes one FrameRecord
  requestAnimationFrame(frame);
}
requestAnimationFrame(frame);

Development

npm install
npm run dev        # demo scene with the HUD (sprites, filters, masks, Spine)
npm test           # unit tests
npm run typecheck
npm run build      # tsdown + publint + attw

The demo takes ?spines=N, ?sprites=N, ?theme= and ?model=<url>.

Credits

Extracted from @spine-benchmark/pixi-crawler in spine-benchmark. The demo's Spine symbols come from pixi-reels.

License

MIT