| name | bundle-optimization |
| description | Guide for optimizing bundle size and images. Server Components by default, use image quality caps. |
Bundle Optimization Skill
Purpose
Guide for optimizing bundle size and image performance. Ensure fast page loads and good Core Web Vitals.
Bundle Analysis Commands
yarn analyze
yarn analyze:browser
yarn analyze:server
yarn analyze:experimental
Image Optimization
Quality Caps
External images use quality caps to reduce bandwidth:
| Context | Quality | Use Case |
|---|
| Default | 50 | Standard images |
| Priority/LCP | 50 | Above-the-fold images |
Note: LCP quality was reduced from 60 to 50 based on Lighthouse analysis
showing 159 KiB potential savings. See utils/image-quality.ts for details.
Helper Functions
import {
getOptimalImageQuality,
getOptimalImageSizes,
} from "@utils/image-quality";
const heroQuality = getOptimalImageQuality({ isPriority: true });
const cardQuality = getOptimalImageQuality({ isPriority: false });
const sizes = getOptimalImageSizes("card");
Image Component Usage
import Image from "next/image";
import { getOptimalImageQuality, getOptimalImageSizes } from "@utils/image-quality";
<Image
src={event.imageUrl}
alt={event.title}
width={400}
height={300}
quality={getOptimalImageQuality({ isPriority: false })}
sizes={getOptimalImageSizes("card")}
loading="lazy"
/>
<Image
src={event.imageUrl}
alt={event.title}
fill
priority
quality={getOptimalImageQuality({ isPriority: true })}
sizes={getOptimalImageSizes("hero")}
/>
Image Retry Logic
For unreliable image sources:
import { useImageRetry } from "@hooks/useImageRetry";
function EventImage({ src, alt }) {
const {
retryCount,
hasError,
imageLoaded,
showSkeleton,
handleError,
getImageKey,
} = useImageRetry();
return (
<Image
key={getImageKey(src)} // Forces re-render on retry
src={src}
alt={alt}
onError={handleError}
// Exponential backoff on retry (1s, 2s, 4s...)
/>
);
}
Preloading Critical Images
import { preloadImage } from "@utils/image-preload";
preloadImage(heroImageUrl);
Caution: preloadImage has coupling to internal Next.js _next/image URL format. Use sparingly.
Cache Busting
Versioned URLs
For static assets that need cache busting, add a version query parameter:
const buildVersion = process.env.BUILD_VERSION || Date.now().toString();
function getVersionedUrl(path: string): string {
return `${path}?v=${buildVersion}`;
}
const url = getVersionedUrl("/static/data.json");
BUILD_VERSION resolves to:
- Development: timestamp
- Production: git SHA (set in CI workflows)
Code Splitting Best Practices
Dynamic Imports
import dynamic from "next/dynamic";
const HeavyChart = dynamic(() => import("@components/ui/HeavyChart"), {
loading: () => <ChartSkeleton />,
});
Server Component Default
Most components should be Server Components (no JS shipped):
export function EventList({ events }) {
return (
<ul>
{events.map((event) => (
<li key={event.id}>{event.title}</li>
))}
</ul>
);
}
Client Components - Only When Needed
"use client";
Bundle Size Targets
Monitor these in bundle analysis:
| Metric | Target | Action if Exceeded |
|---|
| First Load JS | < 100kB | Split or lazy load |
| Shared chunks | < 50kB | Check for large deps |
| Page-specific | < 30kB | Review page imports |
Common Bundle Bloaters
- Large date libraries → Use native Intl or date-fns (tree-shakeable)
- Full icon libraries → Import specific icons only
- Unused dependencies → Audit with
yarn analyze
- Client-side only code in shared → Move to client components
- ⚠️ Local barrel files (
index.ts re-exports) → NEVER create barrel files that re-export "use client" components from different route contexts. In Next.js RSC, every "use client" module re-exported from a barrel gets registered in the client-reference-manifest of every route that imports from that barrel — even if the component is never used. optimizePackageImports only applies to npm packages, not local barrels. Always use direct file imports.
⚠️ Barrel File Incident (Feb 2026)
components/ui/sponsor/index.ts re-exported 7 components. Routes like /[place] only needed SponsorBannerSlot, but importing from the barrel also pulled CheckoutButton, PlaceSelector, and PricingSectionClient (all "use client") into the manifest — adding 24 KB of bloat.
import { SponsorBannerSlot } from "@components/ui/sponsor";
import SponsorBannerSlot from "@components/ui/sponsor/SponsorBannerSlot";
Fix: Always use direct file imports for local components. Reserve barrels only for npm-published packages.
Optimization Checklist
Images
Bundles
Caching
Files to Reference