Building Effective Client-Side Web Applications: A Simple Guide

Harshit BansalHarshit Bansal
5 min read

As a developer who loves the balance between performance and developer experience, I often choose Next.js for building client-side web applications. While Next.js is known for its SSR (Server-Side Rendering) capabilities, I use it in a slightly different way for client side apps — by statically exporting the app using output: 'export' in the config.

This blog walks you through how and why I use this approach — and how you can too.


Why Next.js with Static Export?

While tools like Vite or Create React App (CRA) are commonly used for building client-side React apps, they rely on a traditional SPA architecture. This means:

  • They inject all the JavaScript into a single root <div> in index.html

  • The entire UI is rendered on the client side

  • The browser has to download, parse, and execute a potentially large JavaScript bundle before anything meaningful appears on screen

This can lead to:

  • Slower first paint and time-to-interactive

  • Performance bottlenecks in larger apps

  • Bloated JS bundles if not optimized carefully


On the other hand, by using Next.js with output: 'export', I get:

  • Pre-rendered static HTML for every page at build time

  • Faster load times, as the browser doesn't have to wait for JS to render basic content

  • All the DX benefits of Next.js (routing, file-based structure, CSS modules, etc.)

So I still build a fully client-side app — but with a structure that’s faster to load, easier to maintain, and ready to deploy anywhere.


Core Principles I Follow

1. Keep Most Components Server-Side (or Static)

Even though I’m building a “client-side” app, I aim to make as many components as possible static or server-rendered at build time. This means:

  • Use pure React components without useEffect, useState, or browser-only logic where not needed

  • Pre-render pages and content where possible

Static components = faster load, less JavaScript


2. Server Components as Children of Client Components

One powerful and often underused feature in Next.js is this:

If you pass a Server Component as a child to a Client Component, it remains a Server Component.

I use this pattern a lot. For example:

<ClientWrapper>
  <ServerOnlyComponent />
</ClientWrapper>

This allows me to keep large or static content server-rendered, while wrapping it in a small interactive layer on the client — best of both worlds.


3. Minimal JavaScript on the Client

Only include client-side logic when absolutely necessary:

  • User interactions (forms, toggles, tabs)

  • Dynamic data fetching

Everything else stays server-side to avoid bloated JS bundles and long hydration times.


4. Break UI Into Reusable Components

I think this goes without saying. I design all pages using small, modular components:

  • Shared layout primitives (Container, Section, Card)

  • Page-specific blocks (Hero, FeatureList, Testimonial)

  • Theme-friendly and responsive by default

This makes scaling and styling far easier as the app grows.


5. Think Static First, Dynamic Later

Before reaching for a client-side hook or fetch, I ask:

“Can I pre-render this during build?”

If yes, I do it. This mindset helps me avoid unnecessary client code, and improves load times and reliability.


Demo Project

Project Github Repository

Project Structure

Project Architecture

  • The HomePage is wrapped with a client component called HomePageProvider.

  • Inside this provider, I use TanStack Query to fetch data from an API.

  • The fetched result — including data, isLoading, and error — is passed into a React Context named HomePageContext.

      "use client";
    
      export const HomePageProvider: React.FC<{ children: React.ReactNode }> = ({
        children,
      }) => {
        const { data, error, isLoading } = useQuery<Beer[]>({
          queryKey: ["beers"],
          queryFn: () =>
            fetch("https://api.sampleapis.com/beers/ale").then((res) =>
              res.json().then((data) => data.slice(0, 20))
            ),
        });
    
        return (
          <HomePageContext value={{ data, error, isLoading }}>
            {children}
          </HomePageContext>
        );
      };
    

This way:

  • Most of the page remains pure static JSX — lightweight and SEO-friendly.

  • Whenever I need to show dynamic data (like user info, stats, or a chart), I create small client components that:

    • Use useHomePageData() hook

    • Read just the data they need (and show loading/error states if required)

        "use client"
      
        export const BeerTableRows = () => {
          const { data, error, isLoading } = useHomePageData();
          return (
            <>
              {data?.map((beer) => (
                <tr key={beer.id}>
                  <td className="px-5">{beer.name}</td>
                </tr>
              ))}
            </>
          );
        };
      

Why this works well

  • Separation of concerns: Fetching logic lives in a single place (the provider).

  • Reusable context: Any nested component can access the fetched data without prop drilling.

  • Minimal hydration: Only the parts that really need to be interactive are client components.

  • Fast static shell: The rest of the page loads instantly from the exported HTML.


Build and Deployment

To build the project for static export, I use the following setting in next.config.js:

import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  output: "export",
};

export default nextConfig;

When I run: npm run build

Next.js will generate an out/ directory that contains:

  • A separate index.html file for each route

  • Static assets like images, CSS, and JS

  • Pre-rendered pages with embedded data wherever possible

Since it's now just plain HTML, CSS, and JS, I can deploy this to any object store like:

  • Amazon S3 — as static files

  • CloudFront — for global edge caching and fast delivery

  • Or any static host (Netlify, GitHub Pages, Firebase Hosting, etc.)


Final Thought

This setup gives me the best of both worlds: a fast, SEO-friendly, fully static site with the flexibility to selectively add dynamic functionality — all while keeping the client-side JavaScript footprint minimal.

0
Subscribe to my newsletter

Read articles from Harshit Bansal directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Harshit Bansal
Harshit Bansal