Next.js fundamentals

Next.js vs React Performance: How to Run a Fair App Comparison

A reproducible method for comparing React client rendering with specific Next.js rendering modes—without declaring a universal winner.

Editorial illustration for Next.js vs React Performance: How to Run a Fair App Comparison

A useful Next.js vs React performance test compares work done in different places, not framework names on two starter screens. A client-rendered React page builds its content in the browser; a server-rendered Next.js page sends generated HTML before client-side initialization finishes. Static generation moves HTML generation to build time instead. Those choices can change both when content appears and where the cost is paid. Treat each Next.js rendering mode as a separate test variant, including Next.js client rendering if that is a choice your team is considering.

That distinction is central to a comparison using Create React App and Next.js: its author explicitly frames the experiment as client-side rendering versus server-side rendering, not a verdict on every way to use either technology. Its Next.js example uses the Pages Router-era getInitialProps; it is not an App Router recipe.

Specify the variants before building them

Pin the installed React and Next.js versions and identify the client-rendered baseline’s tooling. For each Next.js variant, record the router, rendering mode, runtime, and deployment target. Name the actual route being tested. “Next.js” alone does not say whether that route is rendered per request, generated ahead of time, or rendered on the client.

Use a matrix like this as a blank plan, not as a results table:

Variant | React version | Next.js version | Router | Render mode | Runtime | Host/region | Workload | Test conditions
A: React baseline | [pin] | n/a | n/a | browser CSR | [browser + serving runtime] | [host/region] | [page ID] | [profile ID]
B: Next.js SSR | [pin] | [pin] | [App or Pages] | per-request SSR | [record] | [host/region] | [page ID] | [profile ID]
C: Next.js static | [pin] | [pin] | [App or Pages] | build-time static | [record] | [host/region] | [page ID] | [profile ID]

Keep App Router and Pages Router implementations identified rather than treating their APIs as interchangeable. If you compare both routers, give each its own row. Record how the static page’s data is produced and when it is updated; otherwise a static snapshot and a request-time data fetch may answer different product questions. Static generation trades build-time work for content that can be served without SSR’s per-request rendering overhead, as Toptal’s rendering overview explains.

Give every variant the same job

An empty page mostly tests an empty page. Build one representative, data-backed screen with the same visible content, assets, API response, and user task in every variant. The CRA–Next.js comparison deliberately used shared API data, matching functionality and UI, non-trivial content, and JavaScript weight. Its fetches still ran in different places: in the browser for CRA and on the server for the Next.js first load. That execution difference is part of what a rendering-strategy test measures.

Here is a workload specification, not an application implementation:

Page: product directory
Data: the same pinned API response, with 200 products in the same order
Initial UI: identical heading, product fields, images, and first visible rows
Interaction: enter "lamp" in a filter; verify the same resulting rows
Assets: identical image files, dimensions, and displayed sizes
Record differences: transferred JavaScript, data-fetch location, and any UI code
Static variant: record the build snapshot and its update procedure

The numbers and page details above define a possible experiment; they are not measured results. If an implementation cannot use identical code, document the difference rather than quietly changing the workload. In an App Router comparison, also record which UI is server-rendered and which requires client-side JavaScript. A visible server-rendered control is not necessarily usable yet: hydration attaches the event handling and state needed for interaction. This rendering discussion distinguishes the arrival of server-generated HTML from the later completion of client-side initialization.

Hold the environment steady

Use the same backend and data snapshot. Keep the browser, device profile, network conditions, cache state, client location, and test tooling consistent. Record both the application’s hosting region and the API’s region, along with the runtime used by each variant. Geography is not a footnote: the CRA–Next.js test placed its apps and API in Sydney and its client browser in Boulder, so its timings include that particular distance.

Write down settings before the first run. For example:

Tool: [Lighthouse or WebPageTest; record tool/version]
Browser and device profile: [record]
Mobile emulation: [on/off and configuration]
Network and CPU throttling: [record method and settings]
Client location: [record]
App/API hosting region and runtime: [record]
Storage: [cleared for cold loads; specify any warm-load series separately]
Performance test: [same option and settings for every variant]
Runs: [set count per variant before testing]

Mobile settings, simulated throttling, cleared storage, and the Performance option were disclosed in the earlier Lighthouse comparison. Use its disclosure as a model, not its deployment or settings as a substitute for recording your own. Run cold and warm loads as separate series if both matter; mixing them obscures what was reused.

Separate response, paint, and interaction

Report TTFB for the initial response, FCP for the first visible content, and LCP for the largest visible content milestone. Add TBT to show main-thread blocking during the measured load. These metrics describe different parts of the experience; the metric overview here identifies all four alongside TTI.

Then measure the actual user task separately. Define interaction readiness—for example, elapsed time from navigation until the filter can accept input and produce the correct rows—and also record latency from the filter action to its visible result. State how you detected both endpoints and apply the same procedure to every variant. If you publish TTI instead, name the tool and the definition it used; do not silently equate it with your filter test. Keep controlled lab timings distinct from any qualitative feedback. One research abstract explicitly distinguishes FCP and TTI measurements from user feedback.

For every matrix row, publish the exact conditions, run count, individual measured values or an accessible results table, and a summary such as the median and spread. Do not select the best run. Label results as lab measurements or user feedback, and include the page, versions, router, rendering mode, and deployment details with the chart.

The interpretation should follow the measurements. An earlier FCP does not establish that the filter becomes usable sooner. SSR may make HTML visible before hydration while adding server work to a request; static output avoids that per-request rendering work but has its own content-update requirements. A well-run comparison can tell you which tradeoff helped these pages under these conditions. It cannot turn one SSR-versus-CSR result into a ranking of React and every Next.js mode.

Find a note

Search by topic, title, or keyword.