Performance Budgets & Optimization

Hard

Performance 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

Related Concepts