const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true',
openAnalyzer: true,
});
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
productionBrowserSourceMaps: process.env.ANALYZE === 'true',
experimental: {
optimizePackageImports: ['lodash-es', 'date-fns', '@mui/icons-material'],
},
webpack: (config, { isServer }) => {
if (!isServer) {
config.optimization.moduleIds = 'deterministic';
}
return config;
},
};
module.exports = withBundleAnalyzer(nextConfig);
{
"name": "web",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"analyze": "ANALYZE=true next build",
"budgets": "next build && node scripts/checkBudgets.js"
},
"dependencies": {
"next": "14.2.5",
"react": "18.3.1",
"react-dom": "18.3.1"
},
"devDependencies": {
"@next/bundle-analyzer": "14.2.5"
}
}
{
"defaultBudgetKb": 180,
"routes": {
"/": 130,
"/pricing": 150,
"/dashboard": 320,
"/dashboard/reports": 380
}
}
const fs = require('fs');
const path = require('path');
const NEXT_DIR = path.join(process.cwd(), '.next');
const budgets = require('../budgets.json');
function loadManifest(name) {
const file = path.join(NEXT_DIR, name);
if (!fs.existsSync(file)) return {};
return JSON.parse(fs.readFileSync(file, 'utf8'));
}
function fileSizeKb(relativePath) {
const abs = path.join(NEXT_DIR, relativePath);
if (!fs.existsSync(abs)) return 0;
return fs.statSync(abs).size / 1024;
}
function routeSizes() {
const pages = loadManifest('build-manifest.json').pages || {};
const app = loadManifest('app-build-manifest.json').pages || {};
const shared = pages['/_app'] || [];
const merged = { ...pages, ...app };
const sizes = {};
for (const [route, files] of Object.entries(merged)) {
if (route === '/_app') continue;
const all = new Set([...shared, ...files].filter((f) => f.endsWith('.js')));
let total = 0;
for (const f of all) total += fileSizeKb(f);
sizes[route] = Math.round(total);
}
return sizes;
}
function budgetFor(route) {
return budgets.routes[route] || budgets.defaultBudgetKb;
}
const sizes = routeSizes();
const violations = [];
for (const [route, kb] of Object.entries(sizes)) {
const limit = budgetFor(route);
const status = kb > limit ? 'FAIL' : 'ok';
console.log(`${status.padEnd(4)} ${route.padEnd(28)} ${kb}kb / ${limit}kb`);
if (kb > limit) violations.push({ route, kb, limit });
}
if (violations.length) {
console.error(`\n${violations.length} route(s) over budget.`);
process.exit(1);
}
console.log('\nAll routes within budget.');
This snippet shows how to turn @next/bundle-analyzer from a one-off manual inspection into a repeatable, enforced part of a performance workflow. The idea is that measuring bundle size is only useful if it is done consistently and compared against a budget, so the setup wires analysis into the normal build and then fails CI when a route exceeds its allowed first-load JS.
In next.config.js, withBundleAnalyzer wraps the base config and is gated on process.env.ANALYZE. This matters because the analyzer plugin has overhead and produces HTML reports that should not run on every production build. The config also enables optimizePackageImports for a couple of heavy libraries, which lets Next.js rewrite barrel imports into deep imports so tree-shaking actually removes unused code — one of the most common reasons a route bundle balloons.
The analyze npm script in package.json sets ANALYZE=true and runs the build, which is what a developer runs locally to open the treemap and see which modules dominate a chunk. Reading that treemap is how the pattern starts: it reveals whether a large dependency is being pulled into a shared chunk or into a single route, which tells the engineer whether to lazy-load, swap the dependency, or split the import.
The checkBudgets.js script closes the loop. Next.js writes .next/build-manifest.json and app-build-manifest.json describing which JS files each route loads; checkBudgets sums those file sizes per route, compares them against budgets.json, and exits non-zero on any violation. The budgets map keys off route patterns so different pages can carry different limits — a marketing landing page should be far lighter than a data-heavy dashboard.
The trade-off is that byte budgets are approximate: gzip and shared-chunk deduplication mean the raw file sum overstates real transfer size, so budgets should be tuned with headroom rather than treated as exact. Still, this approach catches regressions early, keeps the treemap honest, and gives a concrete number to defend during code review. A developer reaches for it once ad-hoc profiling stops scaling across a team.
Related snips
require "csv"
class PeopleCsvStream
include Enumerable
HEADERS = %w[id full_name email signed_up_at plan].freeze
Resilient CSV Export as a Streamed Response
Rails.application.configure do
config.after_initialize do
Bullet.enable = true
Bullet.alert = false
Bullet.bullet_logger = true
Bullet.console = true
N+1 query detection with Bullet gem
# BAD: N+1 query problem
@users = User.all
@users.each do |user|
puts user.posts.count # Fires query for each user!
end
ActiveRecord query optimization and N+1 prevention
json.array! @posts do |post|
json.cache! ['v1', post], expires_in: 1.hour do
json.id post.id
json.title post.title
json.excerpt post.excerpt
json.published_at post.published_at
Fragment caching for expensive JSON serialization
import axios, { AxiosError } from 'axios'
import { v4 as uuidv4 } from 'uuid'
const api = axios.create({
baseURL: import.meta.env.VITE_API_URL || 'http://localhost:3000/api/v1',
timeout: 15000,
Axios API client with interceptors
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
interface FilterState {
search: string
category: string | null
Zustand for lightweight state management
Share this code
Here's the card — post it anywhere.