audit-bundle-size — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited audit-bundle-size (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Degree of freedom: MIXED — Offender triage and swap choice [HIGH freedom]; Phase 1 production build + analyser [LOW freedom — run exactly].
Audit-and-fix exception. Measure the payload, then shrink it. Runtime slowness → audit-performance.>
Fix only what the measurement named, in the files it named; a repo-wide dependency sweep the report did not list is a different session. Edit surgically — the offending import,dynamic()boundary, or chunk config — rather than rewriting whole files. Pre-existing bugs met on the way go in the report as follow-ups. Add or change tests only where the repo already keeps them for that surface; scratch checks and generated analyser reports (client.html,bundle-report.html) stay out of the commit. AnANALYZE=true-guarded analyser config stays only if the repo already keeps one.
Every kilobyte of JavaScript the browser must download, parse, and compile before showing anything costs real users real time. A large initial bundle is the #1 avoidable cause of slow LCP and poor Core Web Vitals. Find exactly what is bloating it and fix each item.
| Skill | Owns |
|---|---|
| audit-bundle-size (this) | Analyse and shrink the JavaScript bundle: code splitting, initial load |
audit-performance | Audit and fix runtime performance: Core Web Vitals, load priority |
workflow-quality-gate | Pre-release gate across red-team, security, bundle, performance, unit tests |
workflow-launch-ready | Launch-preparation sweep for a new app or major release |
file + package)Observe: Next.js First Load JS 420 KB gzip. Treemap: defaultlodashimport inlib/utils.ts;momentwith all locales in the shared layout. Interpret: two libraries pull unused code onto every route. Classify: full-library import + oversized date lib. Severity: High — over the 250 KB review threshold. Finding: namedlodash/debounce+ replacemomentwithdayjs; remesure First Load JS before claiming a save.
package.json scripts.build → build command and framework
vite.config.* → Vite + rollupOptions
next.config.* → Next.js (webpack / Turbopack)
webpack.config.* → standalone Webpack
astro.config.* → Astro (islands, Vite underneath)Identify:
rollup-plugin-visualizer, webpack-bundle-analyzer,@next/bundle-analyzer, source-map-explorer
If rollup-plugin-visualizer is not installed:
npm install --save-dev rollup-plugin-visualizerAdd temporarily to vite.config.ts:
import { visualizer } from 'rollup-plugin-visualizer';
// in plugins array:
visualizer({ open: false, filename: 'dist/bundle-report.html', gzipSize: true })Then build:
npm run build 2>&1 | tail -40Read dist/bundle-report.html for the treemap, or parse dist/stats.json if the plugin is configured to output JSON.
ANALYZE=true npm run build 2>&1 | tail -60This requires @next/bundle-analyzer in next.config.*:
const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true',
});
module.exports = withBundleAnalyzer({ /* your config */ });Read the generated client.html report. Key number: First Load JS per route. Next.js prints this in the build output — capture it for before/after comparison.
Use source-map-explorer on the build output:
npx source-map-explorer 'dist/**/*.js' --html dist/bundle-report.htmlOr for a quick size summary:
find dist -name '*.js' | xargs ls -lh | sort -k5 -hr | head -20
find dist -name '*.css' | xargs ls -lh | sort -k5 -hr | head -5For each chunk or entry point, record:
| Chunk | Raw size | Gzip size | Largest contributors |
|---|---|---|---|
| main / page.js | ... | ... | [dep@version, ...] |
| vendor | ... | ... | [dep@version, ...] |
| Problem | Signal | Impact |
|---|---|---|
| One giant vendor chunk | Single vendor.js > 200 KB gzip | High — blocks first paint |
| Duplicate dependency | Same library listed twice (e.g. lodash + lodash-es) | Medium |
| Full library import | import _ from 'lodash' (imports everything) | High |
| Missing lazy routes | All routes in one bundle | High |
| Unused package | Large dep that appears in bundle but only 1–2 exports used | High |
| Dev-only dep in prod bundle | faker, debug, chalk in client code | Medium |
| Moment.js | 67 KB gzip with all locales | Medium — replace with date-fns or dayjs |
| Material UI / Ant Design full import | Full icon library loaded | High |
react-icons full package | 50+ MB raw, huge when not tree-shaken | High |
Also check:
experimental.optimizePackageImports / direct paths (icon libs, lodash, date libs)<Link prefetch> only on likely-next routes, and don't double-prefetch what Speculation Rules already coverBelow-fold widgets → dynamic() with a Suspense skeleton; navigation prefetch/prerender → enhance-web-instant-nav.
For the largest offenders, check current alternatives:
firecrawl:firecrawl_search
{
"query": "replace <package-name> smaller alternative bundle size [current year]",
"limit": 3,
"sources": [{ "type": "web" }]
}Common swaps (research to confirm current state):
moment → date-fns or dayjs (much smaller, tree-shakeable)lodash → native JS or lodash-es with named importsaxios → native fetch (if browser targets allow)audit-performance// Before — pulls in the whole library
import _ from 'lodash';
import { BellIcon } from '@heroicons/react/24/solid'; // only needs Bell
// After — only the export you need
import debounce from 'lodash/debounce';
import { BellIcon } from '@heroicons/react/24/solid'; // already correct for heroicons v2For icon libraries that do not tree-shake well, use per-icon deep imports or an SVG sprite sheet.
Next.js App Router — code-splits by route automatically. If you have heavy components inside a page, split them:
import dynamic from 'next/dynamic';
const HeavyChart = dynamic(() => import('../components/HeavyChart'), {
loading: () => <ChartSkeleton />,
ssr: false, // if it uses browser-only APIs
});Vite / React Router:
const LazyPage = React.lazy(() => import('./pages/LazyPage'));
// Wrap in <Suspense fallback={<PageSkeleton />}>SvelteKit — routes are code-split by default. For heavy components:
{#await import('./HeavyComponent.svelte') then { default: Component }}
<Component />
{/await}Follow the research from Phase 3. For any replacement:
npm run build, compare sizes before merging everywhere.Vite — split large deps into their own cacheable chunks:
build: {
rollupOptions: {
output: {
manualChunks: {
'react-vendor': ['react', 'react-dom'],
'router': ['react-router-dom'],
'charts': ['recharts'], // or whatever charting lib
},
},
},
},Next.js — configure splitChunks in next.config.* only if the default chunking is creating sub-optimal splits. Prefer dynamic() over manual config.
Any package that should only run server-side or in tests must not be imported from client-side code. Move to devDependencies and verify it disappears from the bundle after rebuild.
After fixes, rebuild and re-run Phase 1:
Before: First Load JS = X KB gzip
After: First Load JS = Y KB gzip
Saved: Z KB (N%)For each route (Next.js build output shows this), confirm the numbers improved. Run a quick Playwright check to confirm the app still works:
PW="npx --yes @playwright/cli@latest"
$PW -s=bundle-check open --headed "<app-url>" # then `goto` each key route
$PW -s=bundle-check snapshot # no blank screen or error
$PW -s=bundle-check console # no new errors| Asset | Target | Review if |
|---|---|---|
| Initial JS (gzip) | < 100 KB | > 250 KB |
| Route chunk (gzip) | < 50 KB | > 150 KB |
| CSS total (gzip) | < 20 KB | > 50 KB |
| Largest single image | < 200 KB | > 500 KB |
| Total page weight | < 500 KB | > 1.5 MB |
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.