
<Suspense>, and build core functionality like forms with an HTML-first approach.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?
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:
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:
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:
There are two primary ways to implement streaming in Next.js:
loading.tsx): Create a loading.tsx file to automatically show a fallback UI (like a skeleton) for a route while its content loads.<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.
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:
loading.tsx, you get what one developer described as "a very unresponsive page when using SSR." The page load feels slow and clunky.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.
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.
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.
<Suspense>Rather than using loading.tsx for an entire route when its core content must be accessible, consider a more targeted approach:
<Suspense> boundariesThis way, the baseline content is always present, and the streamed components are true enhancements rather than requirements for basic functionality.
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.
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.
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:
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.
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.
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.
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.
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.
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.
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.
Synscribe helps B2B companies with SEO & GEO using programmatic SEO approach. Book a call to find out how we help you win.