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

threeops

v0.9.1

Published

3D performance for three.js / R3F projects, as steps in your own pipeline: optimize assets on export, guard builds, verify a deployed preview against live

Readme

threeops

3D performance for three.js and React Three Fiber projects. Your scenes get light enough for phones, and stay that way. ThreeOps isn't a CI/CD system. It adds a few steps to the pipeline you already have:

| Step in your pipeline | Command | What it does | | --- | --- | --- | | Asset export / build | threeops-optimize <models dir> | KTX2 textures that stay compressed on the GPU, meshopt geometry, texture caps. Skips files it already did | | Before deploy | threeops-optimize --check <models dir> | Stops the build if an asset didn't finish optimizing | | Before deploy, or in CI | threeops verify --local | Builds and serves this project locally, measures it against the live site (or --against-ref main), exits 1 if anything got worse | | After a preview deploy | threeops verify <preview-url> | Measures the preview against the live site on a throttled phone, writes a before/after report, exits 1 if anything got worse | | Once, to set up | threeops fix | Wires the above into the project: KTX2 loading, optimize on export, the build guard |

threeops init [dir]                       detect the stack, write threeops.config.json
threeops fix --project <dir>              one-time setup, in a separate git worktree
threeops optimize <dir>                   same as threeops-optimize, from the CLI
threeops verify [url] [--against <url>]   measure url against the live site (or --against), report, exit 0/1
threeops verify --local [--against-ref <ref>]   the same for this build, served locally: no deploy (CI, before a push)
threeops files <paths...>                 analyze local .glb/.gltf files, report with optimized copies (no browser)
threeops runs                             what ran on this project, newest last
threeops doctor [--project <dir>]         is this machine (and project) ready to export and ship optimized models?
threeops login / logout                   free account on the ThreeOps service, for sharing reports (coming soon)
threeops share [report]                   share a report as a link anyone can open (coming soon)
threeops agents [--project <dir>]         install the Claude Code skill, print how to connect the MCP server
threeops mcp                              MCP server on stdio, for coding agents
threeops firebase deploy | promote        optional, for Firebase projects without a deploy pipeline

Every run is recorded in the project, in .threeops/runs/ (one threeops.run/v1 JSON file per run; the folder ignores itself in git). Reports go to .threeops/reports/. Nothing is sent anywhere unless you share a report.

Projects that threeops fix set up don't install this CLI: they get the small @threeops/optimize package (vendored as vendor/threeops-optimize-<version>.tgz), so their npm scripts can call threeops-optimize. The CLI is for setting up and for verify, which needs a browser. From the monorepo root, run it as npm run threeops -- <command>.

Coding agents

ThreeOps is built to be used by coding agents as much as by people:

  • MCP server: threeops mcp serves the same actions over stdio. The tools are scan_project, analyze_models, check_models, optimize_models, setup_project, verify_deploy (a URL, or local: true with an optional againstRef), share_report, list_runs and doctor. Each answers with a one-line summary followed by JSON. Read-only tools are marked as such, and errors say what to do next ("install KTX-Software 4.3+", "stop the dev server", "pass the preview URL"). verify_deploy sends progress notifications while it measures.

  • Claude Code skill: .claude/skills/threeops/SKILL.md says when to reach for ThreeOps (adding models, slow or crashing scenes on phones, before and after a preview deploy), the workflow, and the rules (KTX2 not WebP, load through the helper, never remove the build guard). threeops fix installs it when the project uses Claude Code (a .claude/ folder in the project or repo root). threeops agents installs it anywhere and prints the command that connects Claude Code to the MCP server:

    threeops agents --project <dir>
    claude mcp add --scope user threeops -- threeops mcp
  • THREEOPS.md, written by fix, tells any agent (and person) what ThreeOps changed in the project and what not to undo. AGENTS.md, CLAUDE.md and Copilot instructions get a pointer to it.

For scripts: --json prints results on stdout (logs go to stderr). Exit codes are stable: 0 ok, 1 the check failed (verify got worse, unoptimized assets, a high-severity finding), 2 a usage or setup problem (no URL, KTX-Software missing), 3 fix stopped before finishing (the next run completes it).

Setting up a project

  1. threeops init <dir> scans the project:

    • three, R3F and drei versions
    • how glTF is loaded, and whether KTX2 and meshopt are set up
    • eager preloads
    • where models live, and whether a script generates them (git-ignored output)
    • Firebase hosting

    Review the threeops.config.json it writes: url (the live site, verify's default reference), tier, textureCap, and steps for scenes behind clicks.

  2. threeops fix --project <dir> works in a separate git worktree next to the repo (<repo>.threeops, branch threeops/fix-<date>), so your checkout, its branch, uncommitted work and any running dev server are never touched. The worktree gets its own node_modules and a copy of the git-ignored exports in public/. It applies:

    • KTX2 loading (R3F + drei): adds src/threeops/gltf.ts, a drop-in useGLTF that sets up a shared KTX2Loader. The useGLTF imports are pointed at it; nothing else in those files changes. The Basis transcoder is self-hosted in public/basis/.
    • Build guard: threeops-optimize --check <models dir> in the prebuild script, so a build stops if an export didn't finish optimizing.
    • Asset pipeline: threeops-optimize <models dir> is appended to the script that regenerates the models (contentStep), so every export is optimized. @threeops/optimize is packed into the project's vendor/ and added as a dev dependency, so a fresh clone or CI can npm ci. Projects fixed before 0.3 (which vendored the whole CLI as threeops-audit) are migrated on the next fix.
    • Cache rule: a Firebase cache rule for the transcoder, inserted as text so firebase.json keeps its formatting.
    • Current models: optimized right away.
    • THREEOPS.md in the project: what changed and why, what not to undo, day-to-day commands, and a section for coding agents. If the project has AGENTS.md, CLAUDE.md or .github/copilot-instructions.md, a one-line pointer is added there. Both live between threeops:start/end markers, so anything people write outside them survives reruns.

    The changes are committed on the worktree's branch (--no-commit to skip). Merge with git merge threeops/fix-<date> from your checkout, then git worktree remove <repo>.threeops. Reruns reuse the worktree and are idempotent. --in-place edits the current checkout on its current branch instead (CI, solo use); then nothing is committed unless you pass --commit. If a dev server is running from that checkout, fix stops before npm install (on Windows it would fail half-way on locked files), names the server's PID, and the next run finishes the install. Fixes that need a decision in the code (lazy loading, merging draw calls) are listed as guided.

  3. Check locally (npm run build, npm run preview), deploy a preview the way you always do, then verify it.

Verify

threeops verify https://my-game--pr-42.web.app                     # against the live site in threeops.config.json
threeops verify https://staging.example.com --against https://example.com
threeops verify https://preview.example.com --json                 # for pipelines and agents

Both URLs are measured fresh, one right after the other so they don't compete for CPU, as the same mid-range phone (4× CPU slowdown, fast 4G) with the same steps. Nothing is compared with an old saved audit, so the network and machine are the same for both.

It passes when the new version still loads the models and no measured number got meaningfully worse: texture memory uploaded to the GPU (+5%), model download (+10%), draw calls or triangles (+20%), plus no load error, no lost WebGL context, and a first 3D frame. Timings such as first frame are reported but don't fail the check: they vary between runs with CDN warm-up.

It writes, in .threeops/reports/verify-<host>-<time>/ (or --out):

  • report.html, led by the measured result, then the usual findings for the new version
  • side-by-side.html: both versions running next to each other in phone or desktop frames. Serve the folder to open it; iframes don't load from file://
  • verify.json, and the reference's own capture and report in against/

A capture counts as settled when both draw calls and uploaded GPU memory have held steady for 5 s (90 s at most): large textures can take many seconds to decode on a throttled phone CPU while the frame doesn't change.

--steps <file> (or steps in the config) takes a module whose default export (page, { sleep, log }) drives the Playwright page after load, for scenes that only load after clicks (games, configurators). Set THREEOPS_CHROMIUM to use an installed Chrome instead of Playwright's Chromium.

Without deploying: --local

threeops verify --local                      # build, serve the output on 127.0.0.1, measure it against live
threeops verify --local --no-build           # measure the build that's already there
threeops verify --local --against-ref main   # this build against main, both built and served locally

--local runs the project's build (build in threeops.config.json, else npm run build), serves the output folder (output, the Firebase public dir, or dist/build/out) on a local static server that falls back to index.html for client routes, and measures it on the same throttled phone as the live site. GPU memory, draw calls, triangles and "do all models load" are the same as on your host. Compression and caching aren't (they're the local server's), so delivery findings are left out for the local side and downloads are compared uncompressed. Static builds only (Vite, CRA, static exports); a server-rendered app needs a deployed URL.

--against-ref <ref> measures one change on its own. The project folder at the ref (a branch, tag or commit) is exported to a temporary folder (only that folder: not the rest of a big game repo, and with no worktree or change to your repository), given its own npm ci, built and served next to this build. Both sides then run on the same machine and the same local server, so only the code differs. The temporary folder is deleted afterwards. If the project's generated exports are git-ignored (models exported from Blender, say), the ref has none. It then gets this checkout's copy, and the report says the comparison shows code changes only. --no-copy-ignored turns that off.

In CI

The same command gates a pull request with no deploy: the runner builds the branch, serves it, and compares it with what players have now.

# .github/workflows/threeops.yml
name: 3D performance
on: pull_request
jobs:
  threeops:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 22, cache: npm }
      - run: npm ci
      - run: npx playwright install --with-deps chromium
      - run: npx threeops verify --local            # exits 1 if the build got heavier than live
      # or measure the pull request on its own (needs the base branch: fetch-depth: 0 on checkout)
      # - run: npx threeops verify --local --against-ref origin/${{ github.base_ref }}
        # env: { THREEOPS_TOKEN: ${{ secrets.THREEOPS_TOKEN }} }   # with --share: a link for the PR
      - uses: actions/upload-artifact@v4
        if: always()
        with: { name: threeops-report, path: .threeops/reports }
  • npx threeops needs the CLI on npm. Until it's published, install it from a packed tarball or a git checkout in a step before.
  • Stable numbers on shared runners: CI machines are noisy, so the check only fails on numbers that don't depend on machine speed: GPU texture memory, model download, draw calls, triangles, models loaded. Timings are reported, never gated.
  • Models must exist in CI. If they're generated outside git (exported from Blender, git-ignored), the runner has none and the build guard stops the build. Commit the optimized exports, fetch them from storage in a step before the build, or run the export in CI.
  • Which reference: live (the default) answers "is this heavier than what players have now". --against-ref origin/<base> answers "what did this pull request change", and needs the base branch fetched (actions/checkout with fetch-depth: 0).

Sharing a report

Coming soon: the hosted service isn't live yet. Until it is, login, share and verify --share say so (and verify --share still verifies); everything else works without it. Here's how it will work:

threeops login                                   # once: a code in the terminal, confirmed in the browser (GitHub or Google)
threeops verify https://preview.example.com --share
threeops share                                   # the newest report in this project (or pass a report folder)
threeops share --list
threeops share --delete <id>

A shared report is a link anyone can open, no account needed. It is deleted after 7 days without a view; each view keeps it for another 7 days. What is sent: the report's data (numbers, findings, the URLs measured) and its images as WebP (screenshot, renders). Local file paths are removed first, and the service renders the page itself from that data: it never hosts uploaded HTML. In CI, set THREEOPS_TOKEN instead of logging in; THREEOPS_API points the CLI at another service (e.g. http://localhost:5000, the service on the Firebase emulators). Agents use the MCP tool share_report, which says it uploads, so clients can ask first.

Firebase helper (optional)

For Firebase Hosting projects that deploy from a laptop and have no pipeline:

threeops firebase deploy --project <dir>                  # build + preview channel threeops-<branch>, never live
threeops verify <preview-url> --project <dir>
threeops firebase promote --project <dir> --yes           # copy the verified channel to live

promote refuses unless the last verify of that channel passed. Without --yes it only says what it would do.

Optimize

threeops optimize / threeops-optimize is idempotent:

  • Skips finished files: a .threeops-cache.json manifest records each output's hash and settings, so reruns skip files it already produced.
  • Keeps originals when nothing improves: if the optimized file isn't lighter on the GPU or the wire, the original stays.
  • KTX2 or a loud stop: without KTX-Software 4.3+ it exits 2 instead of quietly making WebP textures, which use 4-8x more GPU memory. --allow-webp makes WebP on purpose.

Setup

Requires Node 20+.

npm install
npx playwright install chromium   # needed for verify
npm run samples                   # optional: two Khronos test models in ./samples
node bin/threeops.mjs doctor      # checks KTX-Software, Chromium, Firebase CLI

Install KTX-Software 4.3+ to optimize models (THREEOPS_KTX points at it if it isn't on PATH).

Run npm test for the unit tests (codemod, worktrees, busy detection, commit messages, Firebase output parsing).

Local files

node bin/threeops.mjs files samples/
node bin/threeops.mjs files public/models/hero.glb --no-optimize

Analyzes each model, writes a report with optimized copies and before/after renders, and exits 1 on a high-severity finding.

Output

Each report (verify, files) is a folder under .threeops/reports/ in the project (or --out <dir>). A verify report is about 1 MB: the copies of every downloaded file (assets/) are deleted once the report is written (--keep-assets keeps them). Only the newest 20 reports are kept ("reports": { "keep": 20 } in threeops.config.json; false keeps all).

| File | What it is | | --- | --- | | report.html | The visual one-page report, easy to share with the people who own the game: headline, budget meters, breakdown by asset group, the top three fixes with charts, GPU memory after each fix, every model. Light and dark themes, works on phones | | report.md | The same findings as plain Markdown | | findings.json | Every number and finding, machine-readable (threeops.audit/v1) | | compare.html | Before/after viewer with one shared camera. Serve it: npx http-server <report folder> -o compare.html | | optimized/<group>/ | Optimized models: the report's file fixes applied (KTX2 or WebP textures, outliers resized, meshopt geometry) | | renders/ | Before/after images of the model with the biggest GPU saving in each of the top 3 groups, shown in the report as proof | | capture.json, screenshot.png | verify only: what the browser downloaded (URLs, sizes, headers) and the runtime stats, a screenshot. assets/ (the files themselves) only with --keep-assets |

What it measures

Static, per model (from the file): size, triangles, estimated draw calls, materials, textures with sizes and roles, and estimated GPU memory (textures decoded as RGBA8 with mipmaps, plus vertex/index buffers).

Runtime, per site (from the browser): time to the first WebGL draw, draw calls and triangles per frame, three.js revision, WebGPU use, WebGL context loss, and GPU memory actually uploaded (textures, render targets and buffers, from hooks on the WebGL upload calls). Comparing uploaded textures with what was downloaded shows assets that were fetched but never drawn. Headless runs use a software GPU (SwiftShader), so frame times there aren't representative. Use --headed for real ones. Draw-call, triangle and upload counts are accurate either way.

Asset groups: each model is grouped by the folder it loads from (/models/characters/x.glb → characters), so the report can say which kind of asset carries the weight.

Findings (stable IDs, mobile budgets in src/budgets.mjs):

| ID | Fires when | | --- | --- | | FILE_OVER_BUDGET | A model alone exceeds the scene's download budget (8 MB mobile) | | TEX_OVERSIZED | Textures exceed 1024 px (mobile) / 2048 px (desktop) | | TEX_UNCOMPRESSED | PNG/JPEG/WebP textures where KTX2 would stay compressed on the GPU | | GPU_MEM_OVER_BUDGET | Estimated GPU memory exceeds 128 MB (mobile) | | TRIS_OVER_BUDGET | More than 300k triangles (mobile) | | DRAWCALLS_HIGH | More than 100 estimated draw calls (mobile) | | MESH_UNCOMPRESSED | No meshopt/Draco on a mesh over 50k triangles | | MATERIALS_DUPLICATE | Identical materials defined more than once | | NPOT_TEXTURE | Non-power-of-two textures |

Scene-wide findings, across files:

| ID | Fires when | | --- | --- | | ASSETS_NOT_DRAWN | Downloaded textures would need 20%+ (and 32 MB+) more GPU memory than the page actually uploaded: assets preloaded but never shown | | TEX_OUTLIER | In a group of 5+ files, some use 2048 px+ textures, more than twice the group's typical size | | ANIM_DUPLICATE_SETS | 3+ animation-only files carry the same set of clips | | IMAGES_OVERSIZED | Standalone image files (posters, decals) exceed the texture cap | | DOWNLOAD_OVER_BUDGET | All 3D models together exceed the download budget | | DRAWCALLS_RUNTIME | Measured draw calls per frame exceed the budget | | CACHE_SHORT | Models/images over 50 KB cached for less than a day, or without Cache-Control | | CACHE_UNHASHED_IMMUTABLE | immutable caching on file names without a content hash | | NOT_COMPRESSED | glTF, JSON, JS, CSS, wasm or SVG over 20 KB served without gzip/brotli | | IMAGE_OVERSIZED_FOR_DISPLAY | An image has 2.25× more pixels than its on-screen size × DPR |

Project findings (verify scans the project): KTX2_LOADER_MISSING, EAGER_PRELOADS, ASSETS_GENERATED. The report's "Fix plan" marks each finding as automatic (threeops fix / optimize apply it) or guided.

The report's "GPU memory after each fix" chart applies the fixes in order (load only what is drawn → resize textures → KTX2), scaling each step by what the previous ones left, so savings are not counted twice.

Optimization

The "after" file is built with glTF-Transform: dedup → instance → prune → weld → resample → resize textures to the tier cap (texture outliers get their group's lower cap) → textures → meshopt geometry. With KTX-Software installed, textures become KTX2: ETC1S for colour, UASTC for normal maps at half the cap (UASTC is large on disk). They stay compressed in GPU memory. Without it, textures become WebP, which shrinks downloads but not GPU memory. The report's "Already done" section shows the real totals of these files, re-analyzed, plus before/after renders (same camera, rendered with three.js in headless Chromium; needs internet for three.js from jsDelivr).

Example, Khronos "A Beautiful Game" sample on the mobile tier: 41.0 MB → 11.9 MB download; ~727 MB → ~185 MB GPU memory with WebP, ~53 MB estimated with KTX2.

Limits

  • GPU memory and draw calls in files are estimates; runtime numbers come only from verify, which loads the site.
  • Models built in code aren't captured. Models loaded after a click need a --steps script.
  • ASSETS_NOT_DRAWN compares totals; it can't yet name the individual files that were never drawn, only the largest groups.
  • No mesh simplification or LODs yet; triangle findings are advice only.
  • Always check optimized models in compare.html before quoting savings. The renders cover only three models.

Project layout

bin/threeops.mjs     CLI: init, fix, optimize, verify, files, runs, firebase deploy/promote, agents, mcp, doctor
src/actions.mjs      What the commands do, shared by the CLI and the MCP server
src/mcp.mjs          MCP server (stdio)
src/service.mjs      The ThreeOps service from the CLI: login, share
skills/threeops/     The Claude Code skill
src/project/scan.mjs Project scan: stack, loaders, preloads, generated assets, hosting
src/project/doctor.mjs Machine and project readiness checks
src/fix/             threeops fix: R3F + drei KTX2 loader, pipeline hook, vendoring, worktree, THREEOPS.md, skill, busy detection
src/deploy/          Firebase helper: preview channels, promote
templates/gltf.ts    The KTX2-aware useGLTF that threeops fix installs
test/                Unit tests (npm test)

Measuring (capture, findings, before/after comparison, reports) lives in @threeops/measure; optimizing and run records in @threeops/optimize.