Framework Performance: Getting Fast Pages From React, Next.js, Vue, Angular, Astro and SvelteKit
Most modern sites are built with a framework, and the framework shapes performance more than any single optimization. It decides how much JavaScript ships by default, whether pages are rendered on the server, at build time or in the browser, how the page becomes interactive (hydration), how data is loaded, and how updates propagate through the component tree. Two sites with the same design can differ by seconds in LCP and hundreds of milliseconds in INP purely because of framework choices and how the framework is used.
The good news is that every major framework now provides strong performance primitives: server rendering and streaming, static generation, partial and lazy hydration, automatic code splitting, image and font components, and compilers that reduce runtime work. The bad news is that defaults rarely use them fully, and common patterns — fetching data in client components, rendering everything as interactive, passing large objects through context, importing whole libraries — quietly undo them.
This section is organised by framework. React rendering performance covers wasted renders, memoization, the React Compiler, context and concurrent features. Next.js performance covers first-load JavaScript, fonts, partial prerendering, third-party scripts and images. Vue and Nuxt performance covers payload size, reactivity cost, lazy hydration and hybrid rendering. Angular performance covers zoneless change detection, deferrable views and hydration with event replay. Astro and SvelteKit performance covers islands, client directives, preloading and choosing frameworks for content sites.
Diagnostic Overview: What the Framework Is Costing You
Start with three measurements. First, how much JavaScript ships for the initial route: the framework runtime plus your code, compressed and uncompressed. Next.js prints "First Load JS" per route; other frameworks have bundle analysers. On a mid-range phone, every 100KB of compressed JavaScript costs roughly 300–500ms of parse, compile and execute time. Second, how long hydration takes: record a Performance trace of page load with CPU throttling and look for the framework's hydration work (a long task after the main bundle executes). Third, what runs on interaction: record the slowest interactions from field INP data and see whether the time goes into framework rendering (component functions, virtual DOM diffing, change detection) or into your own logic.
Map what you see to the framework's levers:
- Too much JavaScript on load → server components, islands, lazy hydration, deferrable views, route-level code splitting, removing client-side dependencies.
- Long hydration → hydrate less (islands, server components, lazy/partial hydration), hydrate later (on visibility or interaction), or hydrate in chunks (Suspense, incremental hydration).
- Slow interactions → fewer re-renders (memoization, compiler, signals, fine-grained reactivity), cheaper re-renders (smaller components, virtualization), and non-urgent updates marked as such (transitions, deferred values).
- Slow LCP despite SSR → the LCP image or content depends on client-side data, the image component is misconfigured, or fonts and third-party scripts block rendering.
Framework-level issues show up in field data in characteristic ways. Single-page apps with heavy hydration have good LCP on repeat visits and poor INP during the first seconds after load. Client-rendered pages have poor LCP because content waits for JavaScript and data. Over-rendering apps have poor INP across the session, worst on large pages.
Architecture 1: Rendering Strategy
The rendering strategy determines when HTML is produced and what the browser must do. Static generation produces HTML at build time — fastest TTFB, cacheable everywhere, best for content that does not change per request. Server-side rendering produces HTML per request — necessary for personalised or real-time content, with TTFB depending on server work (and improved by streaming). Client-side rendering sends an empty shell and builds the page in JavaScript — slow LCP, especially on mobile, and rarely the right choice for public pages. Most frameworks support mixing these per route: Next.js with static and dynamic routes and partial prerendering, Nuxt with route rules, Astro with per-page prerendering, SvelteKit with per-route options, Angular with SSR and prerendering.
The guiding principle is to render as early as the content allows. Content that is the same for everyone should be static or cached; content that varies per request should be server-rendered and streamed; only truly interactive state belongs in the client.
Architecture 2: Hydration
Server rendering produces HTML, but frameworks must then "hydrate": download the component code, re-run components in the browser to rebuild their state, and attach event handlers. Hydration cost scales with the number and complexity of components on the page, not with how much of the page is actually interactive. A product page with one "add to cart" button may hydrate its entire component tree — header, footer, description, reviews — just to make that button work.
Frameworks have converged on ways to hydrate less. Islands (Astro, and similar patterns elsewhere) ship JavaScript only for interactive components; everything else stays static HTML. Server components (React Server Components in Next.js App Router) render on the server and never ship their code to the client. Lazy and incremental hydration (Nuxt lazy hydration, Angular incremental hydration with @defer, React Suspense boundaries) delays hydrating parts of the page until they are visible or interacted with. Resumability (Qwik) avoids hydration by serialising state and resuming on interaction. Each reduces the main-thread work during load, which improves TBT and early INP.
Architecture 3: Bundles and Dependencies
Frameworks split code by route automatically, but the shared bundle — framework runtime, UI library, state management, utilities — loads on every page. It grows silently as dependencies are added. Common culprits are date libraries, icon packs imported whole, charting libraries on pages that do not show charts, rich-text editors, analytics SDKs and polyfills for browsers you no longer support. Bundle analysers (@next/bundle-analyzer, rollup-plugin-visualizer for Vite-based frameworks, source-map-explorer) show what each chunk contains.
The fixes are the same across frameworks: import only what you use, move heavy components behind dynamic imports, prefer lighter alternatives, keep server-only code on the server (server components, server routes), and set per-route JavaScript budgets in CI. Framework-specific components for images, fonts and scripts (Next.js next/image, next/font, next/script; Nuxt Image and Fonts modules; Angular's NgOptimizedImage) encode best practices — when configured correctly.
Architecture 4: Reactivity and Re-rendering
After load, performance depends on how much work each update triggers. React re-renders a component and, by default, all its descendants when state changes; unnecessary re-renders ("wasted renders") are the most common React performance problem, addressed by memoization, the React Compiler (which memoizes automatically), state colocation and splitting context. Vue's fine-grained reactivity tracks dependencies precisely, but deep reactivity over large objects has overhead that shallowRef and markRaw avoid. Angular historically ran change detection across the component tree after every event via zone.js; signals and zoneless change detection make updates targeted. Svelte compiles components into direct DOM updates, and its runes make reactivity explicit.
Whatever the model, the patterns that hurt INP are similar: large state objects updated frequently, global state that many components subscribe to, expensive derived values recomputed on every update, and long lists without virtualization. Concurrent features (React useTransition and useDeferredValue) let urgent updates (typing) proceed while expensive ones (filtering a large list) render in the background.
Architecture 5: Data Loading
Where and when data is fetched often matters more than rendering. Client-side data fetching after hydration creates a waterfall: HTML, then JavaScript, then the data request, then rendering — LCP waits for all of it. Server-side data loading (route loaders, server components, getServerSideProps, Nuxt useFetch on the server, SvelteKit load, Angular resolvers with SSR) puts data in the initial HTML. Parallel loading avoids waterfalls between nested components or routes. Payload serialisation — the state frameworks embed in the HTML for hydration — can become large; Nuxt's payload, Next.js's RSC payload and Angular's transfer state should contain only what the client needs.
Prefetching closes the loop: frameworks prefetch the code and data for likely next routes (Next.js Link prefetching, SvelteKit data-sveltekit-preload-data, Nuxt link prefetching, Angular preloading strategies), so client-side navigations are fast.
Choosing an Approach by Site Type
Different kinds of sites need different defaults. Content sites — blogs, documentation, news, marketing — are mostly static text and images with a few interactive widgets. They benefit most from static generation or cached server rendering with islands or server components, so that almost no JavaScript runs on load. Astro, Next.js with server components, Nuxt with lazy hydration and route rules, and SvelteKit with prerendering all fit. E-commerce sites mix static content (product descriptions, category pages) with dynamic, personalised parts (prices, stock, cart). Partial prerendering or cached shells with streamed dynamic sections fit well, and the product image is almost always the LCP element, so image handling matters more than anything else. Applications — dashboards, editors, internal tools — are dominated by interactivity after load. Their priorities are fast updates (fine-grained reactivity, memoization, virtualization) and route-level code splitting; LCP matters less after the first load, but INP matters throughout the session.
A single framework can serve all three, but the configuration should match. A common failure is treating a content site like an application — client-rendering everything, shipping a large global state library, hydrating static text — which produces slow LCP and poor early INP for no benefit.
Third-Party Scripts in Framework Apps
Framework apps often load analytics, tag managers, consent tools, chat widgets and A/B testing scripts alongside their own bundles. These compete with hydration for the main thread at the worst possible moment. Frameworks provide loading helpers: Next.js next/script with afterInteractive, lazyOnload and worker strategies; Nuxt Scripts with triggers; Astro and Partytown integration to move scripts to a web worker; Angular's ability to load scripts after bootstrap. Use them to load non-critical scripts after hydration or on interaction, and move experiments to the server or edge where possible, so the variant is in the HTML rather than applied by a blocking client script. Measure the effect with Long Animation Frames attribution, which shows which scripts occupy long frames during load.
Fonts and Images Across Frameworks
Fonts and images are where framework components deliver the most value with the least effort. Font modules (Next.js next/font, Nuxt Fonts, Astro's font support) self-host font files, generate size-adjusted fallbacks that prevent layout shifts, and preload the critical subset. Image components generate responsive srcsets, serve modern formats, set dimensions to avoid CLS, and lazy-load offscreen images. The pitfalls are configuration: forgetting to mark the LCP image as priority (so it is lazy-loaded), missing or wrong sizes (so oversized images download), and running every image through an optimisation endpoint that is slow on cache misses. Check the LCP element on key templates in each framework and confirm it is preloaded or high-priority, correctly sized, and served from cache.
Measuring Client-Side Navigations
Frameworks with client-side routing turn later navigations into soft navigations: the URL changes and content updates without a new document load. Core Web Vitals in CrUX measure only full page loads, so soft navigations are invisible there, even though users experience them as page loads. The Soft Navigations API (experimental in Chromium) and framework-specific instrumentation can measure them. Until it is widely available, track route-change timings yourself: time from the click to the new route's main content rendering, and INP for interactions during and after the transition. Fast client-side navigations depend on prefetching route code and data, keeping route chunks small, and avoiding waterfalls in route-level data loading.
Monitoring and CI: Framework-Aware Budgets
Set budgets that match framework outputs: first-load JavaScript per route (compressed), number of client components or islands per page, payload size embedded in HTML, and hydration time in a lab trace. In the field, segment INP by route and interaction target, and use the Long Animation Frames API to attribute long frames to scripts — framework internals versus application code. Track LCP by route and by navigation type (initial load versus client-side navigation, which frameworks report differently).
Upgrades deserve their own checks. Framework releases regularly change defaults (caching behaviour, prefetching, hydration strategies); compare bundle sizes and lab traces before and after upgrading, and read the performance-related notes in release changelogs. Keep a short record of performance-relevant framework settings (rendering mode per route, prefetching, image and font configuration) in the repository, so reviewers can spot when a change alters them and new team members understand why they are set that way.
Reference Implementations
React: marking expensive updates as non-urgent
import { useState, useDeferredValue, memo } from 'react';
function Search({ items }) {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query); // typing stays responsive
return (<>
<input value={query} onChange={(e) => setQuery(e.target.value)} />
<Results items={items} query={deferredQuery} />
</>);
}
const Results = memo(function Results({ items, query }) {
const filtered = items.filter((i) => i.name.toLowerCase().includes(query.toLowerCase()));
return <ul>{filtered.slice(0, 200).map((i) => <li key={i.id}>{i.name}</li>)}</ul>;
});
// trade-off: results lag slightly behind typing on slow devices, which users
// tolerate far better than a frozen input.
Next.js: loading a heavy client component only when needed
import dynamic from 'next/dynamic';
const Chart = dynamic(() => import('../components/Chart'), { ssr: false, loading: () => <div style={{ height: 320 }} /> });
// trade-off: ssr:false means no server HTML for the chart; reserve its height to
// avoid layout shifts, and only use it for components that need browser APIs.
Nuxt: lazy hydration of a below-the-fold component
<template>
<ProductHero :product="product" />
<LazyProductReviews hydrate-on-visible :product-id="product.id" />
</template>
<!-- trade-off: the reviews HTML is server-rendered but not interactive until it
scrolls into view; interactions before hydration need care. -->
Angular: deferring a component until it is visible
@defer (on viewport) {
<app-recommendations [productId]="id" />
} @placeholder (minimum 200ms) {
<div class="recs-skeleton" style="height: 360px"></div>
}
<!-- trade-off: the component's code loads only on visibility; the placeholder must
reserve space to avoid CLS when the real component renders. -->
Astro: an island that hydrates on visibility
---
import Newsletter from '../components/Newsletter.jsx';
---
<article><!-- static HTML, zero JavaScript --></article>
<Newsletter client:visible />
<!-- trade-off: client:visible delays the widget's JavaScript until it scrolls into
view; use client:idle or client:load for widgets needed immediately. -->
Common Pitfalls
- Client-side data fetching for LCP content. The LCP waits for JavaScript and an API call.
- Hydrating static content. Shipping and running components that never change.
- Importing whole libraries. Icon packs, date libraries and utilities inflate shared bundles.
- Global state that re-renders everything. One update re-renders large parts of the app.
- Misconfigured image components. Missing
priorityorsizes, or lazy loading the LCP image. - Third-party scripts loaded eagerly. Tag managers and widgets block the main thread during hydration.
- Large serialised payloads. Hydration state that duplicates the whole database response.
- Ignoring framework upgrades. Missing improvements, or inheriting new defaults unknowingly.
FAQ
Which framework is fastest?
It depends on the site. For content-heavy sites with little interactivity, islands-based frameworks like Astro ship the least JavaScript. For highly interactive apps, the differences are smaller than the differences in how each app is built.
Is server-side rendering always faster?
For LCP on public pages, usually yes, because content arrives in HTML. But SSR adds hydration cost; combine it with partial hydration or server components to keep INP healthy.
What is hydration and why is it slow?
Hydration re-runs components in the browser to attach interactivity to server-rendered HTML. It is slow when many components hydrate at once, regardless of how many are actually interactive.
Do React Server Components reduce JavaScript?
Yes. Server components render on the server and do not ship their code to the client, so only client components contribute to the bundle and hydration.
How do I find what is in my bundle?
Use a bundle analyser for your build tool. Look for large dependencies, duplicated packages and code that belongs on the server.
Should I migrate frameworks for performance?
Rarely as a first step. Most performance problems can be fixed within the current framework using its modern features. Migrations make sense when the architecture fundamentally mismatches the site, such as a heavy SPA for a content site.
Do framework image components guarantee good LCP?
No. They help with sizing, formats and lazy loading, but the LCP image still needs to be marked as priority, use correct sizes, and not depend on client-side rendering.
How do I measure hydration time?
Record a Performance trace with CPU throttling and find the long task after the framework bundle executes; framework DevTools (React Profiler, Vue DevTools, Angular DevTools) can attribute time to components. In the field, Long Animation Frames during the first seconds after load reveal hydration-heavy frames.
Do signals make frameworks faster?
Signals allow fine-grained updates: only computations and DOM nodes that depend on a changed value re-run. Frameworks adopting them (Angular, Vue’s reactivity, Svelte runes, Solid, Preact signals) can avoid re-rendering whole component trees, which helps INP on complex pages.
Is a static site generator always faster than SSR?
For content that is the same for everyone, static generation gives the best TTFB and is trivially cacheable. SSR with good CDN caching can come close. The bigger difference is usually how much JavaScript runs after the HTML arrives.
Topics in This Section
- React rendering performance — wasted renders, memoization, the compiler and concurrent features.
- Next.js performance — first-load JavaScript, fonts, images, scripts and partial prerendering.
- Vue & Nuxt performance — payloads, reactivity, lazy hydration and route rules.
- Angular performance — zoneless change detection, @defer and hydration.
- Astro & SvelteKit performance — islands, directives and preloading.
Related
- JavaScript bundle optimization — framework-independent bundle techniques.
- Rendering & CSS performance — the browser work frameworks trigger.
- Streaming SSR & early flush — server rendering delivery.