Performance Budgets & Optimization
HardPerformance budgets set measurable thresholds for metrics like bundle size, Time to Interactive, and Largest Contentful Paint. In frontend system design, optimization is not an afterthought but an architectural constraint that influences component splitting, asset loading strategy, and rendering patterns from the start.
Interactive Visualization
Key Points
- Core Web Vitals: LCP < 2.5s, INP < 200ms, CLS < 0.1 are Google ranking signals
- JavaScript bundle budget: < 200KB compressed for initial load on mobile
- Code splitting by route ensures users only download JavaScript for the page they visit
- Tree shaking eliminates unused exports, but only works with ES module static imports
- Image optimization (next/image, srcset, lazy loading) often has the biggest LCP impact
- Font loading strategy (display: swap, preload) prevents invisible text flashes (FOIT)
- Third-party scripts (analytics, ads) are the most common performance budget busters
Code Examples
Performance Budget Configuration
// Performance budget in a CI pipeline // webpack.config.js — bundle size limits module.exports = { performance: { maxAssetSize: 200_000, // 200KB per asset maxEntrypointSize: 300_000, // 300KB per entry hints: 'error', // Fail build if exceeded }, } // Lighthouse CI thresholds // lighthouserc.json { "ci": { "assert": { "assertions": { "categories:performance": ["error", { "minScore": 0.9 }], "first-contentful-paint": ["error", { "maxNumericValue": 1500 }], "largest-contentful-paint": ["error", { "maxNumericValue": 2500 }], "interactive": ["error", { "maxNumericValue": 3500 }], "cumulative-layout-shift": ["error", { "maxNumericValue": 0.1 }], "total-byte-weight": ["error", { "maxNumericValue": 500000 }] } } } } // Bundle analysis — track per-route sizes // next.config.js const withBundleAnalyzer = require('@next/bundle-analyzer')({ enabled: process.env.ANALYZE === 'true', }) module.exports = withBundleAnalyzer(nextConfig)
Performance budgets should be enforced in CI to prevent regressions. Setting limits on bundle size, Lighthouse scores, and Core Web Vitals catches issues before they reach production.
Code Splitting and Lazy Loading
// Route-based code splitting (automatic in Next.js) // Each page only loads its own JavaScript // Component-level code splitting for heavy modules import dynamic from 'next/dynamic' const MarkdownEditor = dynamic( () => import('./MarkdownEditor').then(m => ({ default: m.MarkdownEditor })), { ssr: false, loading: () => <EditorSkeleton />, } ) // Conditional code splitting: load only when needed function Dashboard() { const [showChart, setShowChart] = useState(false) return ( <div> <button onClick={() => setShowChart(true)}>Show Analytics</button> {showChart && ( <Suspense fallback={<ChartSkeleton />}> <AnalyticsChart /> {/* Lazy loaded on demand */} </Suspense> )} </div> ) } // Prefetch on hover: load before click function NavLink({ href, children }: NavLinkProps) { const prefetch = useCallback(() => { // Prefetch the route chunk on hover import(`./pages/${href}`) }, [href]) return ( <Link href={href} onMouseEnter={prefetch}> {children} </Link> ) }
Code splitting ensures users only download the JavaScript they need. Route-based splitting is automatic in Next.js; component-level splitting is for heavy widgets like editors, charts, and maps.
Image and Font Optimization
// Image optimization: srcset + lazy loading import Image from 'next/image' function ProductImage({ src, alt }: ImageProps) { return ( <Image src={src} alt={alt} width={800} height={600} sizes="(max-width: 768px) 100vw, 50vw" placeholder="blur" blurDataURL={generateBlurPlaceholder(src)} priority={false} // true only for above-the-fold LCP images /> ) } // Font loading: prevent FOIT/FOUT // next/font eliminates layout shift from font loading import { Inter } from 'next/font/google' const inter = Inter({ subsets: ['latin'], display: 'swap', // Show fallback font immediately preload: true, variable: '--font-inter', }) // Critical CSS: inline above-the-fold styles // next.config.js module.exports = { experimental: { optimizeCss: true, // Extract and inline critical CSS }, } // Third-party script loading import Script from 'next/script' function Analytics() { return ( <Script src="https://analytics.example.com/script.js" strategy="afterInteractive" // Load after hydration /> ) }
Images and fonts are often the biggest performance bottlenecks. Proper srcset, lazy loading, font display strategies, and deferred third-party scripts can cut LCP by 40-60%.
Common Mistakes
- Optimizing JavaScript bundle size while ignoring image optimization, which often has more impact
- Loading all third-party scripts eagerly instead of deferring non-critical scripts
- Not setting performance budgets in CI, allowing gradual regressions over time
- Using dynamic imports for everything, adding unnecessary loading states
- Forgetting about font loading: custom fonts without display:swap cause invisible text flashes
Interview Tips
- Quantify performance targets: name specific Core Web Vital thresholds
- Discuss the performance budget as an architectural constraint, not an afterthought
- Mention real-world tools: Lighthouse CI, webpack-bundle-analyzer, Web Vitals API
- Explain the waterfall: DNS > TCP > TLS > TTFB > FCP > LCP > TTI
- Show awareness of the mobile performance gap: 3G connections, low-end devices