Developing CSR (Client-Side Rendering) for a web application
Client-Side Rendering — rendering the interface entirely in the browser. The server delivers minimal HTML skeleton and JS bundle, the browser executes code and builds the DOM. This is SPA (Single Page Application) architecture: navigation without page reload, rich interactive interfaces, state lives in client memory.
CSR is the right choice for closed applications: dashboards, cabinets, tools, platforms. Where SEO isn't needed, but a responsive interface with local state is.
When CSR is better than SSR
| Criteria | CSR | SSR |
|---|---|---|
| SEO for public content | Poor | Good |
| First load | Slower | Faster |
| Subsequent transitions | Instant | Every request |
| Complex client state | Natural | Complicated |
| Server resources | Browser does work | Need server for render |
| Offline mode (PWA) | Possible | Difficult |
React SPA architecture with Vite
src/
api/ # HTTP clients and types
components/ # Reusable components
features/ # Feature modules (auth, dashboard, reports)
auth/
components/
hooks/
store.ts
hooks/ # Global hooks
lib/ # Utilities, configs
pages/ # Pages = routes
router/ # TanStack Router setup
stores/ # Zustand stores
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { TanStackRouterVite } from '@tanstack/router-plugin/vite';
export default defineConfig({
plugins: [TanStackRouterVite(), react()],
resolve: { alias: { '@': '/src' } },
build: {
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom'],
router: ['@tanstack/react-router'],
query: ['@tanstack/react-query'],
},
},
},
},
});
Routing with TanStack Router
// src/router/routes.ts
import { createRootRoute, createRoute, createRouter } from '@tanstack/react-router';
import { RootLayout } from '@/components/layouts/RootLayout';
const rootRoute = createRootRoute({ component: RootLayout });
const dashboardRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/dashboard',
component: lazy(() => import('@/pages/Dashboard')),
beforeLoad: ({ context }) => {
if (!context.auth.isAuthenticated) {
throw redirect({ to: '/login' });
}
},
});
export const router = createRouter({
routeTree: rootRoute.addChildren([dashboardRoute, ...]),
context: { auth: undefined! },
});
TanStack Router — type-safe routing: route parameters, search parameters and context are typed at TypeScript level.
Managing server state
TanStack Query for all async data:
// features/products/hooks/useProducts.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { productsApi } from '@/api/products';
export function useProducts(filters: ProductFilters) {
return useQuery({
queryKey: ['products', filters],
queryFn: () => productsApi.getList(filters),
staleTime: 5 * 60 * 1000, // 5 minutes before refetch
placeholderData: (prev) => prev, // Keep old data on filter change
});
}
export function useUpdateProduct() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: productsApi.update,
onSuccess: (updated) => {
// Update specific item in cache without refetch
queryClient.setQueryData(
['products', updated.id],
updated
);
// Invalidate list
queryClient.invalidateQueries({ queryKey: ['products'] });
},
});
}
Bundle optimization
CSR apps suffer from large initial bundle. Strategies:
Code splitting by routes:
const DashboardPage = lazy(() => import('@/pages/Dashboard'));
const ReportsPage = lazy(() => import('@/pages/Reports'));
Bundle analysis:
npx vite-bundle-analyzer
Dynamic import of heavy libraries:
async function exportToExcel(data: Row[]) {
// xlsx loads only on "Export" click
const { utils, writeFile } = await import('xlsx');
const ws = utils.json_to_sheet(data);
const wb = utils.book_new();
utils.book_append_sheet(wb, ws, 'Data');
writeFile(wb, 'export.xlsx');
}
PWA and offline mode
// vite.config.ts — Vite PWA plugin
import { VitePWA } from 'vite-plugin-pwa';
VitePWA({
registerType: 'autoUpdate',
workbox: {
globPatterns: ['**/*.{js,css,html,ico,png,svg,woff2}'],
runtimeCaching: [
{
urlPattern: /^https:\/\/api\.example\.com\/products/,
handler: 'StaleWhileRevalidate',
options: {
cacheName: 'api-products',
expiration: { maxAgeSeconds: 24 * 60 * 60 },
},
},
],
},
})
Implementation timeline
- Week 1: Vite + TypeScript + Router, authentication (JWT/OAuth), project structure
- Week 2–3: main pages, TanStack Query for API, forms (React Hook Form + Zod)
- Week 4: bundle optimization, code splitting, testing (Vitest + Testing Library)
For closed SPA applications with dozens of routes and complex logic, CSR remains the most productive architecture — no SSR limitations, full control over state.







