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

angular-payload-analyzer

v0.0.1

Published

Visualises how an Angular build's initial request payload is composed — interactive standalone Sankey report, revision comparison via git worktree.

Readme

angular-payload-analyzer (apa)

npm Angular Node Jest License: MIT

What the first request of an Angular app costs, and why.

angular-payload-analyzer builds an Angular application with stats enabled and renders a standalone, interactive HTML report: a Sankey diagram from the loading phase (initial / lazy) over the emitted chunks to modules (workspace projects and npm packages) down to single files. For every file it shows the import chain that causes it to be loaded and concrete hints on what to change. With git it compares two revisions in temporary worktrees.

Install

npm install --global angular-payload-analyzer     # global `apa` and `angular-payload-analyzer`
apa info                                          # run inside any Angular / Nx workspace

Alternatively without a global install:

npx angular-payload-analyzer analyze              # one-off
npm install --save-dev angular-payload-analyzer   # per workspace → `npx apa …`

Requires Node ≥ 20.11. The CLI has no runtime dependencies; the viewer is a dependency-free SVG Sankey inlined into each report (no D3, no CDN, works offline, strict CSP without network access).

Usage

apa info                         # workspace kind + version, Angular version, package manager, git, apps
apa analyze                      # build the (only / chosen) app with --stats-json, write the HTML report
apa analyze shop -c staging      # explicit project and configuration
apa analyze --skip-build         # reuse the stats of the last build
apa analyze --stats dist/shop/stats.json
apa compare                      # main (else master/origin/HEAD) vs. current working tree incl. uncommitted changes
apa compare --base v1.4.0 --head HEAD --json cmp.json
apa --help

With several applications and no project argument, apa asks interactively (in a TTY) or lists the candidates.

Guard (apa check)

Snapshot testing for the payload, modeled on Jest snapshots:

apa check shop              # first run: records .payload-snapshots/shop-production.json — commit it
apa check shop --ci         # in CI: fails (exit 1) on changes, never writes a snapshot
apa check shop -u           # accept an intended change: rewrite the snapshot, commit it with the change
apa check shop --ci --max-initial-growth 2kB    # optionally also limit initial growth (bytes, kB, MB or %)

The snapshot lists every module (workspace project, npm package) with its initial and lazy sizes — no timestamps, git revisions or hashed chunk names, so its git diff shows exactly what changed. The check fails when

  • a module appears that is not in the snapshot (initial or lazy), or
  • a known module becomes part of the initial payload (it was lazy only), or
  • the initial total grows beyond --max-initial-growth (when given), or
  • the snapshot belongs to another project, configuration or bundler, or
  • gzip/brotli are compared while only one side measured compression (use --metric raw or record again).

The accept hint repeats every option you passed (--snapshot, --stats, --cwd, …) plus -u; -u also replaces a corrupt or outdated snapshot file. --stats inside a workspace uses the target's default configuration, so it checks against the same snapshot as a build would.

Size changes of known modules and modules that disappeared are printed as notes (the latter with a hint to refresh the snapshot). Generated chunk overhead is ignored. --metric raw|gzip|brotli selects the size shown and used for the limit (default gzip). Example from the DDD scenario (snapshot at c0, check at c1):

FAIL flights (production) against .payload-snapshots/flights-production.json
  initial 70.4 kB gzip (+672 B vs snapshot)
  ✕ @angular/forms is now part of the initial payload: 282 B (was lazy only)
  ✕ New module shared-ui-design-system (library): 118 B initial
  • @angular/core initial +139 B (31.4 kB → 31.6 kB)
If this change is intended, update the snapshot: apa check flights -u

What is detected

| Aspect | Source | |------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | Workspace kind | nx.json → NX, otherwise angular.json → Angular CLI (searched upwards from --cwd) | | Versions | installed nx / @angular/cli and @angular/core from node_modules, declared ranges as fallback | | Package manager | packageManager field, then lockfile | | Applications | angular.json projects or NX project.json files; build target, builder, configurations, default configuration, output path | | NX build targets | project.json first; when an application has no usable build target there (executor in nx.json targetDefaults, or a target inferred by a plugin), the NX project graph (nx graph --file, no daemon) supplies the resolved targets — only then, because NX takes seconds | | Applications | only projects with an Angular builder (@angular/build, @angular-devkit/build-angular, @nx/angular, @angular-builders/*, @ngx-env/builder); Node apps such as @nx/esbuild:esbuild are listed as skipped | | Stats file | stats.json (Angular ≤ 21), browser-stats.json (Angular 22); the newest wins. Only .js/.css outputs that were actually emitted count (component styles inlined into JS, the SSR server bundle and media/ assets are ignored) | | Initial set | boot entries (main, polyfills, injected global styles/scripts) plus their static imports; index.html / index.csr.html decides whether a global bundle is injected (inject: false bundles are lazy). Webpack: the chunk's initial flag | | Bundler | builder id (…:application / browser-esbuild → esbuild, …:browser / webpack-browser → webpack); the stats format itself is detected from content | | Git | top level, revision, branch, uncommitted changes |

Build

  • Angular CLI: node_modules/.bin/ng build <project> --configuration=<c> --stats-json
  • NX: node_modules/.bin/nx run <project>:build --configuration=<c> --stats-json --skip-nx-cache (NX_DAEMON=false; --nx-cache allows cache replays)

The configuration defaults to the target's defaultConfiguration, then production.

Compare

apa compare checks each revision other than the working tree out into a private temporary folder ($TMPDIR/apa-worktree-*/<repo>-<sha>, created with mkdtemp) via git worktree add --detach. Without --head, the head side is your working tree and is built in place like apa analyze (its dist/ is overwritten); with --head <rev> both sides are built in worktrees and your checkout is not touched. The base defaults to main, then master, then origin/HEAD. Repository and workspace paths are resolved to real paths, so a symlinked --cwd can never point a revision build at your checkout. Dependencies (--install):

  • auto (default): symlink the current node_modules when the lockfile at that revision equals the current one, otherwise install from the lockfile (npm ci, pnpm install --frozen-lockfile, yarn install --immutable|--frozen-lockfile, bun install --frozen-lockfile)
  • always: always install; never: always symlink

Worktrees are removed afterwards (--keep-worktrees keeps them, including the node_modules link, so they can be rebuilt; remove them with git worktree remove). Paths are normalised (everything up to the last node_modules/ is dropped), so files match across base and head even with symlinked installs.

Reading the report

  • Tiles: initial and lazy totals in the selected metric, Δ vs. base when comparing.
  • Controls: size metric (raw / gzip / brotli), load filter, level (modules ↔ files), colour by load or by change, free-text filter.
  • Sankey: click Load to filter, Chunk to focus it, Module to expand its files, File for its cause chain, Other … to show more. All nodes are keyboard reachable (Tab, Enter).
  • Details panel: sizes, why a chunk is loaded (boot entry, modulepreload or import() site), through which import a module enters the initial payload, the full import chain of a file and hints (statically imported route target → loadComponent, barrel files, dependencies entering via a component → @defer / lazy route).
  • Changes in the chart (compare): module, file and load nodes carry their delta (▲ grown, ▼ shrunk, + new, ⚠ lazy → initial) on the label, a coloured outline, the tooltip and the screen-reader label; the Modules tile counts new and gone modules (gone modules cannot appear in the head chart).
  • Tables: module table (accessible alternative to the chart); when comparing, module and file changes with status and lazy → initial shifts. File changes are decided on exact raw bytes (per-file gzip/brotli are estimated shares and would flag every file of a changed chunk).
  • Dark mode: follows the OS setting; the Theme button cycles auto → light → dark and remembers the choice.

Sizes: chunk sizes are exact (raw on disk, gzip level 9, brotli quality 11 — the brotli figure matches Angular's "estimated transfer size"). Per-file sizes are exact raw bytes for esbuild and proportional shares for gzip/brotli.

--json <file.json> additionally writes the raw report / comparison as JSON (stable, versioned format, schemaVersion: 1).

Limitations

  • Webpack stats hide most import chains (scope hoisting concatenates modules), so causes are partial there, and per-file sizes are scaled from pre-minification source sizes. Chunk totals and initial/lazy classification are exact.
  • NX: build targets from nx.json targetDefaults are verified against a real workspace; targets inferred by plugins come from the same project graph but are covered by a unit test of the graph format only.
  • Only the browser bundle is analysed; the SSR server bundle (.mjs outputs) is ignored on purpose.
  • Two webpack builds of identical code can differ by a few bytes in the webpack runtime (chunk hash map); this shows up as tiny deltas.
  • Per-file and per-module gzip/brotli values are shares of the chunk's measured compression (marked "≈" in deltas); chunk and load totals are measured.
  • Windows: builds run the workspace CLI script with Node (no shell); installs in worktrees run through cmd.exe with fixed arguments. Not tested on a Windows host.