Are You Breaking Progressive Enhancement with Next.js Streaming?

Are You Breaking Progressive Enhancement with Next.js Streaming?

Summary

  • Next.js streaming improves perceived performance but breaks progressive enhancement, showing only loading skeletons when JavaScript is disabled—a critical issue as all users experience slow connections at times.
  • This happens because streaming relies on client-side JavaScript to replace fallback content, a deliberate trade-off by Next.js that prioritizes an interactive feel over universal resilience.
  • To build resilient applications, use traditional server-side rendering (SSR) for critical pages, wrap only non-essential enhancements in <Suspense>, and build core functionality like forms with an HTML-first approach.
  • Ensuring your site's technical foundation is flawless for both users and search engine crawlers is critical for growth. Synscribe's technical SEO implementation helps resolve complex issues to ensure your application is both performant and accessible.

You've meticulously built your Next.js app, leveraging the power of streaming Server Components for a snappy UI. But then you disable JavaScript and refresh. All you see are lifeless skeletons. Your forms, your data—all gone. As one developer on Reddit put it, "When I use streaming for server components and javascript is disabled, then I only can see skeletons, no data."

This experience highlights a growing tension in modern web development between cutting-edge performance features and the time-tested principle of progressive enhancement. The question becomes unavoidable: Does Next.js streaming fundamentally break progressive enhancement? What are the trade-offs for performance versus accessibility? And most importantly, how can we navigate this landscape to build applications that are both fast and resilient?

Back to Basics: What is Progressive Enhancement?

Progressive enhancement is a design philosophy that focuses on providing a baseline of essential content and functionality to as many users as possible while offering an enhanced experience to users with modern browsers and devices.

At its core, progressive enhancement consists of three key principles:

  1. Baseline Experience First: Deliver essential HTML content and functionality that is accessible to all, regardless of browser capability or connection speed.
  2. Layered Enhancements: Add more complex features (CSS, JavaScript) as layers on top of the solid foundation.
  3. Feature Detection: Use techniques to check if a browser can handle advanced functionality before attempting to run it.

Progressive enhancement differs from its counterpart, graceful degradation. While graceful degradation starts with a full-featured experience and strips features away for older browsers, progressive enhancement builds from the ground up, ensuring a solid foundation for all users.

Despite being a decades-old concept, progressive enhancement remains relevant today for several reasons:

  • Resilience: As React Router's documentation notes, "100% of users experience slow connections at times." An app should work without JavaScript while it's loading.
  • SEO & Accessibility: It ensures search engine crawlers and assistive technologies can access and understand your core content without relying on client-side execution.

The Next.js Approach: Streaming and Suspense Explained

Next.js implements streaming as "a data transfer technique that breaks routes into smaller 'chunks' that are progressively streamed from the server to the client as they become ready." This approach aims to improve perceived performance and user experience in two significant ways:

  1. It prevents a single slow data request from blocking the entire page render.
  2. It allows for "interruptable navigation," where users can see and interact with parts of the UI before the whole page has finished loading.

There are two primary ways to implement streaming in Next.js:

  1. Page Level (loading.tsx): Create a loading.tsx file to automatically show a fallback UI (like a skeleton) for a route while its content loads.
  2. Component Level (<Suspense>): For more granular control, wrap specific components in React's <Suspense> boundary. This allows parts of a page to load independently.
import { Suspense } from 'react';
import { RevenueChartSkeleton } from '@/app/ui/skeletons';
import RevenueChart from '@/app/ui/dashboard/revenue-chart';

export default function Page() {
  return (
    //...
    <Suspense fallback={<RevenueChartSkeleton />}>
      <RevenueChart />
    </Suspense>
    //...
  );
}

Loading skeletons are simplified, static versions of the UI that improve perceived performance by showing the user that content is on its way. They're a crucial part of the streaming approach, providing visual feedback during data fetching and component rendering.

The Collision Course: Where Streaming and Progressive Enhancement Clash

Now, let's directly address the question that many developers are asking: "Do streaming can work when JavaScript is disabled?"

The short answer is no.

When JavaScript is disabled, the browser receives the initial HTML payload, which includes the fallback UI (the content of loading.tsx or the fallback prop of <Suspense>). However, the mechanism to replace this fallback with the streamed, final content is a client-side JavaScript operation. Without JS, the user is stuck with the loading state.

This creates what we might call the loading.tsx paradox:

  • Without loading.tsx, you get what one developer described as "a very unresponsive page when using SSR." The page load feels slow and clunky.
  • With loading.tsx, you get immediate visual feedback, but as another developer noted, "it all breaks when you have a loading.tsx because it can't be replaced for the page content without JavaScript."

This puts developers in a difficult position. One developer expressed frustration: "my form can't even show up without JS if it's on a page that has a loading.tsx." This is a direct violation of progressive enhancement principles, where the baseline experience isn't just "less enhanced"—it's broken.

The broader developer community has voiced concerns as well. On Hacker News, many developers feel Next.js has become overly complex, with one user calling it a "bad implementation" of React. The trade-offs have been acknowledged by the Next.js team themselves, who stated: "If you don't want or need streaming, then Next.js might not be the right choice for your app." This explicitly frames streaming as a choice that prioritizes certain performance goals, sometimes at the expense of other principles.

It's worth noting that this approach can also impact Core Web Vitals. While streaming aims to improve Largest Contentful Paint (LCP), improper implementation can worsen Cumulative Layout Shift (CLS) as content "pops" into place, replacing skeletons.

Strategies for Building Resilient Applications with Next.js

Many developers express a common frustration: "I cannot find a way to achieve good experience for both the un-enhanced AND the enhanced users." This section offers pragmatic compromises to help navigate this challenging landscape.

Strategy 1: Use Traditional SSR for Critical Routes

For pages where the core functionality must work without JavaScript (e.g., a login form, a contact page, or critical e-commerce checkout steps), consider opting out of streaming altogether.

Use dynamic rendering to ensure the full page is rendered on the server for each request. This provides a complete HTML document as the baseline.

export const dynamic = 'force-dynamic'; // Enforces dynamic rendering for the route

export default async function Dashboard() {
  const data = await fetch('https://api.example.com/dashboard-data').then(r => r.json());
  return <div><h1>Dashboard</h1>{/* Render with data */}</div>;
}

While this approach may sacrifice some perceived performance for JavaScript-enabled users, it ensures that the critical functionality works for everyone.

Strategy 2: Isolate Non-Essential Enhancements with <Suspense>

Rather than using loading.tsx for an entire route when its core content must be accessible, consider a more targeted approach:

  1. Let the main page content render via SSR
  2. Wrap only the slow, non-critical "enhancements" (like a sidebar with recommendations, a complex chart, or a social media feed) in <Suspense> boundaries

This way, the baseline content is always present, and the streamed components are true enhancements rather than requirements for basic functionality.

Strategy 3: Build with an HTML-First Mindset

Even within a React world, think like it's 2003. A principle from the React Router team is key here: build features to work without JavaScript first.

For forms, this means starting with a standard HTML <form> that can submit to an endpoint. JavaScript can then enhance this process to provide a smoother client-side experience, but the baseline functionality remains intact.

This directly addresses the user pain: "my form can't even show up without JS." By not hiding the form behind a streamed component, you ensure it's available to all users.

Strategy 4: Re-evaluate Your Fallbacks

Instead of generic loading skeletons, consider whether your fallback could provide some static information or a link to an alternative view. This ensures that even users without JavaScript have something meaningful to interact with.

For example, a product recommendation component might show a static "top sellers" list as its fallback, rather than an empty skeleton.

A Conscious Trade-Off

Next.js streaming, in its most common implementations (loading.tsx), fundamentally breaks the principles of progressive enhancement by creating a hard dependency on JavaScript to render primary content.

This isn't an accident; it's a trade-off. Next.js prioritizes the perceived performance and interactive feel for the 99%+ of users with JavaScript enabled. For many applications, especially complex dashboards or highly interactive UIs, this may be the right choice.

The key is to be intentional. Critically assess your application's needs:

Struggling with web resilience?

  • Is it a complex dashboard where perceived speed is paramount?
  • Or is it a content site where accessibility and resilience are non-negotiable?

Don't apply streaming universally. Use the strategies outlined above to make deliberate choices on a per-page, per-component basis.

The debate isn't about whether streaming is "good" or "bad." It's about understanding its costs and benefits, and using it responsibly to build the best possible experience for all your users, not just the ones with the fastest devices and connections.

True modern web development isn't about blindly adopting new features, but understanding their implications and building responsibly.

Frequently Asked Questions

What is progressive enhancement and why is it important?

Progressive enhancement is a web design strategy that starts with a baseline of essential content and functionality accessible to all users, then adds more advanced features as layers. It's important because it ensures your application is resilient, working even on slow networks or for users without JavaScript. This approach also benefits SEO and accessibility by making core content available to search engine crawlers and assistive technologies.

Why doesn't Next.js streaming work when JavaScript is disabled?

Next.js streaming fails without JavaScript because the process of replacing the initial fallback UI (like a loading skeleton) with the final, streamed content is handled by a client-side JavaScript script. When JavaScript is disabled, the browser receives the initial HTML with the fallback, but it never receives the instructions to update the page with the complete content, leaving the user stuck in a loading state.

How can I make my Next.js forms work without JavaScript?

To ensure forms work without JavaScript, you must build them with an HTML-first mindset. Start with a standard HTML <form> element that can submit data to a server endpoint. This creates a functional baseline. You can then use JavaScript to enhance the form with client-side validation and submission for a smoother user experience, following the principle of progressive enhancement. Crucially, avoid wrapping essential forms inside a streamed component that relies on JavaScript to render.

What's the difference between using loading.tsx and <Suspense>?

Both loading.tsx and <Suspense> are used to implement streaming in Next.js, but they operate at different levels. loading.tsx is a file-based convention that automatically creates a loading boundary for an entire page or route segment. <Suspense>, on the other hand, is a React component that allows you to apply streaming boundaries with more granular control to specific components within a page.

When should I use traditional SSR instead of streaming in Next.js?

You should use traditional Server-Side Rendering (SSR) for critical routes where functionality must be guaranteed without JavaScript. This includes pages like login forms, e-commerce checkout processes, or any content that is essential for the user's primary goal. Forcing dynamic rendering for these routes ensures a complete, interactive HTML page is sent from the server, prioritizing resilience over the perceived performance gains of streaming.

Does using Next.js streaming negatively impact my website's SEO?

It can, if not implemented carefully. Search engine crawlers may not execute JavaScript, so if your primary content is hidden behind a streamed component, crawlers might only index the loading skeleton. This can negatively impact your search rankings. To mitigate this, ensure that critical, indexable content is delivered in the initial HTML payload using traditional SSR, and reserve streaming for non-essential enhancements.

Tags:
Published on April 13, 2026

Dominate ChatGPT and Google Search

Synscribe helps B2B companies with SEO & GEO using programmatic SEO approach. Book a call to find out how we help you win.