React Suspense and Concurrent Mode in Next.js – performance and production traps

In-depth analysis of React Suspense and Concurrent Mode in Next.js and practical tips on how to avoid the most common performance issues in production.

React Suspense i Concurrent Mode w Next.js – wydajność i pułapki produkcyjne

React Suspense and Concurrent Mode are among the most important features of the modern React ecosystem, and their integration with Next.js opens new possibilities for optimizing data loading and rendering. In this article we will examine how React Suspense works in Next.js, what issues may arise in production, and how to resolve them to maintain high application performance.

React Suspense Mechanism – from concept to implementation in Next.js

Suspense was introduced as an abstraction layer that allows components to “suspend” rendering until certain conditions are met – most commonly data fetching or code loading. In practice this means that instead of manually managing loading state, we can wrap an asynchronous fragment in <Suspense fallback={…}>. When the Promise resolves, React automatically refreshes the tree.

Next.js uses Suspense in two ways: (1) for dynamic component imports (next/dynamic) and (2) in combination with React.lazy and use (experimental). This lets us define loading boundaries both at the component level and for whole pages.

Simple usage example:

import dynamic from 'next/dynamic';

const Chart = dynamic(() => import('../components/Chart'), {
  suspense: true,
});

function Dashboard() {
  return (
    <Suspense fallback={<div>Loading chart…</div>}>
      <Chart />
    </Suspense>
  );
}

In the code above the Chart component will not be rendered until its code is loaded. This is the basic mechanism on which we build more advanced scenarios.

Concurrent Mode – what it is and how to enable it in Next.js

Concurrent Mode is a set of changes to the React scheduler that allows concurrent processing of multiple rendering tasks. This enables React to pause and resume work, resulting in smoother UI and shorter “time‑to‑interactive”. In Next.js, enabling Concurrent Mode is done via the experimental reactStrictMode flag and concurrentFeatures in next.config.js:

module.exports = {
  reactStrictMode: true,
  experimental: {
    concurrentFeatures: true,
  },
};

Once enabled, all components run in concurrent mode by default, and Suspense becomes a key tool for controlling split points.

When to use Suspense and Concurrent Mode, and when not to

  • Use when the application has many heavy components (e.g., charts, maps) and you want to load them asynchronously to avoid blocking the initial render.
  • Use if you need smooth scrolling and want to minimize jank in interactive lists (e.g., virtualized lists).
  • Avoid on simple landing‑page sites where the cost of adding an extra scheduler outweighs the benefits.
  • Avoid when data dependencies are tightly coupled and cannot be split into independent fragments – in such cases Suspense may introduce unnecessary delays.

Production issues with Concurrent Mode – common pitfalls

While Concurrent Mode brings measurable benefits, practical problems can arise that are not always obvious during development. The most important ones are:

  • Incompatible UI libraries – some components (e.g., certain versions of Material‑UI) rely on synchronizing effects in useLayoutEffect, which in concurrent mode can cause unexpected flashes.
  • Race conditions during data fetching – when two components request the same endpoint and one is deferred, duplicate fetching may occur.
  • SSR issues – Next.js initially renders on the server and then switches to client‑side mode. Not all Suspense APIs are fully supported in SSR, which can lead to “hydration mismatch”.

Solutions include:

  • Using use (experimental) or react-query with suspense: true – the library manages the cache and avoids duplicate requests.
  • Testing components in concurrent mode using ReactDOM.createRoot in a local environment.
  • Enabling unstable_skipHydration at critical points where synchronizing state makes no sense.

Data loading optimization – Suspense vs traditional approach in Next.js

Traditionally in Next.js we loaded data in getStaticProps or getServerSideProps. This approach is simple but forces a blocking fetch before the page renders. Suspense allows separating this process – the first render can happen immediately, and the missing data will “fill” the UI when it’s ready.

Comparison:

// Traditional getStaticProps
export async function getStaticProps() {
  const data = await fetch('https://api.example.com/items').then(r => r.json());
  return { props: { data } };
}

// Suspense with react-query
import { useQuery } from '@tanstack/react-query';
function Items() {
  const { data } = useQuery(['items'], () => fetch('/api/items').then(r => r.json()), {
    suspense: true,
  });
  return (
    {data.map(i => <li key={i.id}>{i.name}</li>)}
); }

In the Suspense approach the Items component can be rendered in the first phase, and react-query handles caching and background data refreshing. This reduces TTI (time‑to‑interactive) and enables better scaling as the number of endpoints grows.

“Modern front‑end requires a balance between control over asynchrony and code simplicity – Suspense and Concurrent Mode provide that control, but only with conscious architectural design.”

Practical checklist – implementing Suspense and Concurrent Mode in production

  • Enable concurrentFeatures in next.config.js and test in development mode.
  • Make sure all dynamic imports have suspense: true.
  • Choose a data‑fetching library with native Suspense support (e.g., react-query, tanstack/query).
  • Add minimal‑size fallbacks (e.g., skeleton) – avoid large components as fallbacks.
  • Check UI‑library compatibility – test useLayoutEffect in concurrent mode.
  • Monitor metrics: FCP, LCP, CLS and the number of duplicate fetches in production.

Common mistakes and architectural trade‑offs

The most frequent mistake is overusing Suspense at the <App> level, which leads to a single global fallback and blocks the entire app on the slowest request. A better approach is to limit fallbacks to component level, allowing partial rendering.

Another trade‑off is deciding between prefetching data in getStaticProps vs. using Suspense. Prefetching provides full HTML immediately, which is beneficial for SEO, but at the cost of longer page‑generation time. Suspense, on the other hand, enables fast HTML + dynamic loading, but requires additional JavaScript and can affect SEO metrics if next/head and link rel="preload" aren’t configured properly.

In practice, both approaches are often combined: critical data loaded in getStaticProps, less critical data loaded with Suspense.

In summary, React Suspense and Concurrent Mode in Next.js are powerful tools that, when applied correctly, boost performance and user experience. They also demand thoughtful design, testing, and monitoring to avoid production pitfalls. If you need help migrating an existing application or designing an architecture based on these technologies, contact Coderia.it – together we’ll build a solution that’s fast, stable, and ready for future challenges.

Let’s start

Got a project in mind?

Describe it in a few sentences. I reply within 24 hours with a free quote and a proposed stack.