DEV Community

Cover image for Why Modern SSR Patterns Are Making JavaScript-Heavy Websites Obsolete
Nithin Bharadwaj
Nithin Bharadwaj

Posted on

Why Modern SSR Patterns Are Making JavaScript-Heavy Websites Obsolete

As a best-selling author, I invite you to explore my books on Amazon. Don't forget to follow me on Medium and show your support. Thank you! Your support means the world!

I want to talk about a shift in how we build websites. If you've ever clicked a link and stared at a blank white screen, you've felt the problem. For years, the trend was to build applications that run entirely in your browser, using JavaScript. They're powerful and feel smooth once they load. But that initial load—fetching all that JavaScript, parsing it, and finally painting the screen—can be painfully slow.

This is where the story changes. The old way of doing things, where a server simply sent a complete HTML page, is back. But it’s not a simple return. It has transformed into something smarter, a sophisticated set of patterns designed to give you the best of both worlds: the instant load of a server-rendered page and the smooth, app-like feel of a client-side application. This isn't about choosing one over the other; it's about blending them seamlessly.

Think of it like this. Instead of sending an empty shell and a massive instruction manual (the JavaScript) to the user's browser to build the page, the server now does the heavy lifting upfront. It builds the complete HTML for the initial view. The browser can start showing you content the moment it gets the first packet of data. Then, and only then, does it carefully attach the interactive parts, the JavaScript, like icing on a fully baked cake.

Let me show you what this looks like with modern tools. A powerful new idea is streaming the HTML as it's built. The server doesn't wait to finish the whole page. It sends the critical parts—like the header and main layout—immediately. Then, it streams in the rest, like product details fetched from a database, as they become ready.

// Server-side code using React 18
import { renderToPipeableStream } from 'react-dom/server';
import { Suspense } from 'react';

async function handleRequest(req, res) {
  res.setHeader('Content-Type', 'text/html; charset=utf-8');

  // Write the initial HTML structure immediately
  res.write('<!DOCTYPE html><html><body><div id="root">');

  const App = (
    <html>
      <body>
        <div id="root">
          {/* This header can be sent right away */}
          <Header />
          <main>
            {/* This tells the server: "Wrap this in a loading placeholder for now" */}
            <Suspense fallback={<div>Loading product info...</div>}>
              {/* This component fetches data, which takes time */}
              <ProductDetails id="123" />
            </Suspense>
          </main>
        </div>
      </body>
    </html>
  );

  const { pipe } = renderToPipeableStream(App, {
    onShellReady() {
      // The outer shell (Header, layout) is ready. Start sending it.
      pipe(res);
    }
  });
}

// This component runs on the server
async function ProductDetails({ id }) {
  // This fetch happens on the server, not the user's browser
  const product = await fetchProductFromDatabase(id);
  return <h1>{product.name}</h1>;
}
Enter fullscreen mode Exit fullscreen mode

The user sees the page header instantly. Where the product details will go, they see a "Loading..." message. A moment later, without any page refresh, the real product title slides into place. This feels incredibly fast.

But sending all the JavaScript needed for interactivity can still be heavy. This is where another clever idea comes in: partial hydration. "Hydration" is just a fancy word for attaching JavaScript functionality to the HTML that's already on the screen. We don't have to hydrate everything at once.

Imagine a product page with a main image gallery and a "Recommended Products" section way down at the bottom. The gallery needs to be interactive immediately (swipe, zoom). The recommendations, which the user can't even see yet, don't. So we only hydrate the gallery code right away. The recommendations code loads only when the user scrolls near it.

// Client-side code for partial hydration
import { lazy, Suspense, useEffect, useRef } from 'react';

// This is a heavy, interactive gallery component
const ImageGallery = lazy(() => import('./ImageGallery'));

// This is a non-critical recommendations component
const ProductRecommendations = lazy(() => import('./ProductRecommendations'));

function ProductPage() {
  const recommendationsRef = useRef();

  useEffect(() => {
    const observer = new IntersectionObserver((entries) => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          // User scrolled to the recommendations! Now load its JS.
          import('./ProductRecommendations');
          observer.unobserve(entry.target);
        }
      });
    });

    if (recommendationsRef.current) {
      observer.observe(recommendationsRef.current);
    }

    return () => observer.disconnect();
  }, []);

  return (
    <div>
      {/* Hydrate this crucial component immediately */}
      <Suspense fallback={<div>Loading gallery...</div>}>
        <ImageGallery />
      </Suspense>

      {/* Mark this section for lazy hydration */}
      <section ref={recommendationsRef}>
        {/* Its HTML is already rendered by the server */}
        {/* Its JS will load only when the section is visible */}
        <ProductRecommendations />
      </section>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

This dramatically reduces the amount of JavaScript the browser has to process on first load. The page becomes interactive much faster.

Now, let's push the logic even further back—to the server itself. A groundbreaking pattern is the "Server Component." These are React components that execute only on the server. They never have their code shipped to the browser. This is perfect for parts of your page that are static or rely on secure data.

Why is this a big deal? It means you can freely use large libraries for tasks like formatting dates or processing markdown on the server, and you pay zero cost for it in your browser bundle size. The browser just receives the final HTML.

// This is a Server Component (e.g., in Next.js with .server.js extension)
import { db } from '~/server/db';
import { renderMarkdown } from 'some-large-markdown-library';

export default async function BlogPost({ postId }) {
  // Direct, secure database access. No API endpoint needed.
  const post = await db.post.findUnique({ where: { id: postId } });

  // Use a big library on the server. Its code stays on the server.
  const htmlContent = await renderMarkdown(post.content);

  // This returns plain HTML and data.
  return (
    <article>
      <h1>{post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: htmlContent }} />
      {/* We can embed a Client Component for interactivity */}
      <CommentSection postId={postId} />
    </article>
  );
}

// This is a Client Component (e.g., with 'use client' directive)
'use client';
import { useState } from 'react';

export function CommentSection({ postId }) {
  const [comment, setComment] = useState('');
  // This component's code *will* be sent to the browser for interactivity
  return (
    <form onSubmit={(e) => { /* Handle form submit */ }}>
      <textarea value={comment} onChange={(e) => setComment(e.target.value)} />
      <button type="submit">Add Comment</button>
    </form>
  );
}
Enter fullscreen mode Exit fullscreen mode

The blog post content, processed by a heavy library, is simple HTML. The interactive comment form is a separate, lightweight piece of JavaScript. This separation is incredibly powerful for performance and security.

Speed isn't just about code; it's also about distance. If your server is in Virginia and your user is in Singapore, there's a physical delay. Edge-side rendering solves this by running your rendering logic on a global network of servers (a CDN), as close to the user as possible.

An edge server can personalize the page in real-time based on the user's location, language, or device before sending the first byte.

// Example using an Edge Runtime (like Vercel Edge Functions or Cloudflare Workers)
export const config = { runtime: 'edge' };

export default async function handler(request) {
  // Get user location from the edge network
  const country = request.cf.country;
  const userAgent = request.headers.get('user-agent');

  // Fetch global product data
  const productRes = await fetch(`https://api.internal.com/products/123`);
  const product = await productRes.json();

  // Personalize content at the edge
  let promoMessage = "Worldwide Shipping!";
  if (country === 'DE') promoMessage = "Kostenloser Versand in Deutschland!";
  if (country === 'JP') promoMessage = "日本国内送料無料!";

  // Render the React component to HTML AT THE EDGE
  const html = renderToString(
    <ProductPage product={product} promo={promoMessage} />
  );

  return new Response(html, {
    headers: { 'Content-Type': 'text/html' },
  });
}
Enter fullscreen mode Exit fullscreen mode

The user in Germany gets a page instantly, with a message in German, rendered from a server in Frankfurt. The experience is localized and fast.

Finally, there's a brilliant pattern for content that changes, but not every second. Think of a product page. The price might change once a day. Does it need to be rendered fresh for every single visitor? Incremental Static Regeneration (ISR) says no.

With ISR, you build your product pages as static files for blazing speed. Then, in the background, you set a timer. Maybe every hour, the next person who visits that page triggers a quiet rebuild. They get the old, cached page instantly, while the system generates a new one with the updated price for the next visitor. It's like having a store that restocks its shelves while customers are still shopping.

// In a Next.js page (e.g., pages/products/[id].js)
export async function getStaticProps({ params }) {
  const product = await getProduct(params.id);

  return {
    props: { product },
    // Re-generate this page in the background every 60 seconds
    revalidate: 60,
  };
}

export async function getStaticPaths() {
  // Pre-build the top 100 products at build time
  const products = await getTopProducts(100);
  const paths = products.map((p) => ({ params: { id: p.id } }));

  return { paths, fallback: 'blocking' };
}
Enter fullscreen mode Exit fullscreen mode

If a product's data changes in the database, the page stays fast. Within at most 60 seconds, the change will be live for new users, without any downtime or manual deployment.

So, what does this all mean for building the web? We've moved past the debate of "server vs. client." The new approach is a pipeline, a collaboration. The server (or the edge) does what it does best: secure data access, heavy computation, and sending a meaningful first render. The browser does what it does best: providing a fluid, stateful interface for the parts that need it.

The result is applications that are fast by default. They are accessible to search engines and work even if JavaScript fails or loads slowly. They provide an immediate, valuable experience to the user, which then gracefully enhances into a full application. This is the modern approach to server-side rendering. It’s not a step back, but a significant leap forward in how we think about building for the web. It asks the server to do more, so the user has to wait less. And in the end, that’s what really matters.

📘 Checkout my latest ebook for free on my channel!

Be sure to like, share, comment, and subscribe to the channel!


101 Books

101 Books is an AI-driven publishing company co-founded by author Aarav Joshi. By leveraging advanced AI technology, we keep our publishing costs incredibly low—some books are priced as low as $4—making quality knowledge accessible to everyone.

Check out our book Golang Clean Code available on Amazon.

Stay tuned for updates and exciting news. When shopping for books, search for Aarav Joshi to find more of our titles. Use the provided link to enjoy special discounts!

Our Creations

Be sure to check out our creations:

Investor Central | Investor Central Spanish | Investor Central German | Smart Living | Epochs & Echoes | Puzzling Mysteries | Hindutva | Elite Dev | Java Elite Dev | Golang Elite Dev | Python Elite Dev | JS Elite Dev | JS Schools


We are on Medium

Tech Koala Insights | Epochs & Echoes World | Investor Central Medium | Puzzling Mysteries Medium | Science & Epochs Medium | Modern Hindutva

Top comments (0)