Performance Budgets for JavaScript-Heavy Applications: Frameworks, Bundles, and Hydration
Your JavaScript Bundle Is the Tax Your Users Pay on Every Page Load
Every kilobyte of JavaScript your application ships must be downloaded, parsed, compiled, and executed before the page becomes fully interactive. On a fast laptop with a fibre connection, the tax is invisible. On a mid-range phone with a 3G connection — which describes a massive portion of the global web audience — a two-megabyte JavaScript bundle means ten seconds or more before the user can interact with the page. Performance budgets put a hard limit on this tax, turning "the site feels slow" into a measurable, enforceable constraint that holds the entire team accountable.
What a Performance Budget Is
A performance budget is a set of numeric thresholds that your application must not exceed. Budgets can cover:
- Total JavaScript bundle size: The combined size (compressed) of all JavaScript sent to the browser. A common target for content-heavy sites is 200-350KB compressed.
- Largest Contentful Paint (LCP): The time until the largest visible element renders. Target: under 2.5 seconds.
- Interaction to Next Paint (INP): The responsiveness of the page to user interactions. Target: under 200 milliseconds.
- Cumulative Layout Shift (CLS): Visual stability during loading. Target: under 0.1.
- Time to Interactive (TTI): When the page becomes fully responsive to user input.
- Third-party JavaScript: The size and count of external scripts (analytics, ads, chat widgets).
The budget is not aspirational — it is enforced. When a change pushes a metric beyond the budget, the build fails or the pull request is flagged. The team addresses it before shipping, not after users complain.
Where JavaScript Weight Comes From
Framework Baseline
Every framework has a baseline cost — the minimum JavaScript that must load before your application-specific code runs. React's runtime is approximately 40KB compressed. Vue is similar. Svelte compiles away the runtime, resulting in smaller bundles for small applications. Next.js, Nuxt, and SvelteKit add their framework layer on top. Understanding your framework's baseline cost is essential for setting a realistic budget — you cannot budget 100KB total if the framework alone consumes 80KB.
Dependencies
Third-party packages are the most common source of bundle bloat. A date formatting library that adds 70KB, a charting library that adds 200KB, an animation library that adds 50KB — each one seems reasonable in isolation, but collectively they push the bundle far beyond any reasonable budget. Audit your dependencies ruthlessly. For every package, ask: does the value justify the size? Is there a smaller alternative? Can the functionality be implemented in a few lines of application code?
Hydration Cost
Server-side rendered (SSR) applications send HTML that renders immediately, then "hydrate" by attaching JavaScript event handlers to the rendered HTML. During hydration, the browser must download, parse, and execute the full component tree — even for parts of the page that are static and have no interactive behaviour. This hydration cost is the reason SSR applications can show content quickly (good LCP) but take much longer to become interactive (poor INP and TTI).
Reducing Bundle Size
Code Splitting
Instead of shipping the entire application in a single bundle, split it into chunks that load on demand. Route-based code splitting loads the JavaScript for each page only when the user navigates to it. Component-based code splitting loads heavy components (modals, charts, rich text editors) only when they are needed. The initial page load carries only the code required for the current view.
Modern frameworks support code splitting natively. Next.js automatically code-splits by route. Dynamic imports (import()) split at the component level. The key is to identify which code is needed for the initial render and defer everything else.
Tree Shaking
Tree shaking removes unused exports from your bundle. If you import one function from a library, tree shaking eliminates the rest of the library — provided the library supports ES modules and does not have side effects that prevent dead code elimination. Verify that your bundler (webpack, Vite, esbuild) is actually tree-shaking effectively by inspecting the bundle output.
Lazy Loading Below-the-Fold Content
Components below the fold — content the user will not see until they scroll — do not need to load during the initial render. Lazy-load them with intersection observers or framework-provided lazy loading utilities. This reduces the initial JavaScript and rendering cost without affecting the user's perception of page speed.
Replacing Heavy Dependencies
Audit your largest dependencies with a bundle analyser (webpack-bundle-analyzer, source-map-explorer, or Vite's built-in analysis). The usual suspects: moment.js (replace with date-fns or Intl API), lodash (import individual functions or use native methods), large icon libraries (import only the icons you use), and full-featured UI component libraries (consider whether you need the entire library or just a few components).
Hydration Strategies
Partial Hydration
Only hydrate the interactive parts of the page. Static content (headers, footers, article text, navigation) does not need JavaScript attached. Frameworks like Astro implement this natively — components are server-rendered by default and hydrated only when explicitly marked as interactive with directives like client:visible or client:idle.
Progressive Hydration
Hydrate components in priority order: above-the-fold interactive elements first, below-the-fold elements later, and non-critical components during idle time. This improves INP for the initial interaction because the critical interactive elements are ready first.
Resumability
An emerging approach (pioneered by Qwik) where the server serialises enough state that the client can resume execution without replaying the component tree. Instead of downloading and executing all component code upfront, code loads lazily only when the user interacts with a specific component. The result is near-zero JavaScript execution cost on initial load, regardless of application size.
Enforcing Budgets in CI/CD
A budget without enforcement is a suggestion. Integrate budget checks into your pipeline:
- Bundle size checks: Tools like bundlesize, size-limit, and Lighthouse CI compare the current build's bundle sizes against configured thresholds and fail the build or flag the PR if thresholds are exceeded.
- Core Web Vitals checks: Run Lighthouse in CI to measure LCP, INP, and CLS against your targets. Compare against the previous build to detect regressions.
- Dependency auditing: Flag new dependencies that exceed a size threshold (e.g., any new package over 20KB compressed requires explicit justification in the PR).
The Bottom Line
Performance budgets transform front-end performance from a reactive concern ("the site is slow, we need to fix it") into a proactive engineering constraint ("this change exceeds our budget, we need to optimise before shipping"). Set budgets for bundle size and Core Web Vitals, enforce them in CI, and use code splitting, tree shaking, lazy loading, and modern hydration strategies to stay within your limits. Your users — especially those on slower devices and connections — will thank you with engagement, retention, and conversion.