[{"data":1,"prerenderedAt":4},["ShallowReactive",2],{"post-content-react-server-components-dashboard-performance":3},"\u003Cp>Last week I spent three days converting our analytics dashboard to React Server Components. I went in expecting 50% faster load times and zero client-side JavaScript. What I got was messier, more interesting, and ultimately more useful than any benchmark blog post I've read.\u003C\u002Fp>\n\n\u003Cp>The dashboard has 23 components: charts, stats cards, filters, the works. All client-side, all re-rendering like crazy whenever a date range changes. It wasn't slow, exactly, but it felt \u003Cem>heavy\u003C\u002Fem>. Moving it to the server sounded like the obvious win.\u003C\u002Fp>\n\n\u003Cp>Spoiler: it wasn't just \"faster\" or \"slower.\" It was both, depending on where you looked.\u003C\u002Fp>\n\n\u003Ch2>The Numbers\u003C\u002Fh2>\n\n\u003Cp>I was working with Next.js 15 on a medium-sized SaaS app. The dashboard pulls from three different API endpoints — user stats, revenue data, and activity logs. Before the migration, everything was client-side: the page loaded, a useEffect fired, three fetch calls went out, and the data trickled in.\u003C\u002Fp>\n\n\u003Cp>Here are the actual before\u002Fafter numbers, measured on a real production build with a throttled 4G connection:\u003C\u002Fp>\n\n\u003Ctable>\n  \u003Cthead>\n    \u003Ctr>\u003Cth>Metric\u003C\u002Fth>\u003Cth>Client Components\u003C\u002Fth>\u003Cth>Server Components\u003C\u002Fth>\u003C\u002Ftr>\n  \u003C\u002Fthead>\n  \u003Ctbody>\n    \u003Ctr>\u003Ctd>First Contentful Paint\u003C\u002Ftd>\u003Ctd>2.1s\u003C\u002Ftd>\u003Ctd>1.4s\u003C\u002Ftd>\u003C\u002Ftr>\n    \u003Ctr>\u003Ctd>Largest Contentful Paint\u003C\u002Ftd>\u003Ctd>3.8s\u003C\u002Ftd>\u003Ctd>2.2s\u003C\u002Ftd>\u003C\u002Ftr>\n    \u003Ctr>\u003Ctd>Time to Interactive\u003C\u002Ftd>\u003Ctd>4.5s\u003C\u002Ftd>\u003Ctd>3.1s\u003C\u002Ftd>\u003C\u002Ftr>\n    \u003Ctr>\u003Ctd>JS Bundle Size (dashboard page)\u003C\u002Ftd>\u003Ctd>184 KB\u003C\u002Ftd>\u003Ctd>62 KB\u003C\u002Ftd>\u003C\u002Ftr>\n    \u003Ctr>\u003Ctd>Filter change — data refresh\u003C\u002Ftd>\u003Ctd>~600ms\u003C\u002Ftd>\u003Ctd>~1200ms\u003C\u002Ftd>\u003C\u002Ftr>\n  \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\n\u003Cp>Initial load? Way better. Interactive updates? Twice as slow.\u003C\u002Fp>\n\n\u003Cp>That last row is the one nobody talks about.\u003C\u002Fp>\n\n\u003Ch2>The Server Component Waterfall\u003C\u002Fh2>\n\n\u003Cp>Here's what happens when you change a date filter on a server-rendered dashboard: the client sends a request, the server re-renders the whole route, and you wait for the full HTML response to come back before anything updates. On a fast connection it's fine. On anything less than perfect Wi-Fi, it feels sluggish compared to a client-side fetch that only grabs JSON.\u003C\u002Fp>\n\n\u003Cp>I initially had the filters as a server component too. Don't do that. Anything that triggers a state change that affects data needs to live on the client, or your interactions feel like 2005-era page navigation.\u003C\u002Fp>\n\n\u003Cp>The fix was straightforward: make the filter bar a client component and keep the data display on the server. When the filters change, the client component revalidates the server data:\u003C\u002Fp>\n\n\u003Cpre>\u003Ccode class=\"language-tsx\">'use client'\n\nimport { useRouter } from 'next\u002Fnavigation'\nimport { useState, useTransition } from 'react'\n\nexport function FilterBar() {\n  const router = useRouter()\n  const [isPending, startTransition] = useTransition()\n  const [range, setRange] = useState('7d')\n\n  function handleRangeChange(newRange: string) {\n    setRange(newRange)\n    startTransition(() => {\n      router.push(`\u002Fdashboard?range=${newRange}`)\n    })\n  }\n\n  return (\n    \u003Cdiv className={isPending ? 'opacity-50' : ''}>\n      {\u002F* filter buttons *\u002F}\n    \u003C\u002Fdiv>\n  )\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Cp>The \u003Ccode>useTransition\u003C\u002Fcode> hook keeps the UI responsive while the server re-renders. It's not as fast as a client-side fetch, but it's close enough — and you get to keep all the server-side rendering benefits for the initial load.\u003C\u002Fp>\n\n\u003Ch2>What's Actually Worth Moving\u003C\u002Fh2>\n\n\u003Cp>Not all components benefit equally from being on the server. Here's my rule of thumb after this experiment:\u003C\u002Fp>\n\n\u003Cul>\n  \u003Cli>\u003Cstrong>Charts and data displays\u003C\u002Fstrong> — massive win. These are usually the heaviest components in terms of JS, and they don't need much interactivity.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Stat cards with numbers\u003C\u002Fstrong> — obvious win. Pure display, no state, no event handlers. Server components were basically made for these.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Forms and filters\u003C\u002Fstrong> — keep on the client. Anything with onChange or user input is usually better client-side.\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>Anything with animations\u003C\u002Fstrong> — client-side only. Server components can't have useState or useEffect.\u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Cp>The biggest surprise was tables. I thought they'd be a huge win for initial render, and they were. But sorting and pagination on the server adds a round trip for every interaction, which feels worse than doing it client-side on data you already have.\u003C\u002Fp>\n\n\u003Cp>I ended up with a hybrid: the initial table data comes from the server, but once it's loaded, client-side JS takes over sorting and filtering. More code, but the best of both worlds.\u003C\u002Fp>\n\n\u003Ch2>The Part Nobody Warns You About\u003C\u002Fh2>\n\n\u003Cp>Server components can't use browser APIs. That sounds obvious until you're three components deep and realize something trivial, like reading \u003Ccode>window.innerWidth\u003C\u002Fcode> for a responsive chart, breaks the whole thing.\u003C\u002Fp>\n\n\u003Cp>My charts were using a custom hook with \u003Ccode>useEffect\u003C\u002Fcode> and \u003Ccode>ResizeObserver\u003C\u002Fcode>. Server components can't use either.\u003C\u002Fp>\n\n\u003Cp>What actually worked: accept that charts need some client JS, but minimize it. Use a server component for the data fetching and structure, then pass the data down to a tiny client component that handles just the rendering:\u003C\u002Fp>\n\n\u003Cpre>\u003Ccode class=\"language-tsx\">\u002F\u002F app\u002Fdashboard\u002Fchart.tsx — Server Component\nimport { ClientChart } from '.\u002Fclient-chart'\nimport { getRevenueData } from '@\u002Flib\u002Fapi'\n\nexport async function RevenueChart({ range }: { range: string }) {\n  const data = await getRevenueData(range)\n\n  return (\n    \u003Cdiv className=\"bg-white rounded-lg p-6\">\n      \u003Ch3 className=\"font-semibold mb-4\">Revenue\u003C\u002Fh3>\n      \u003CClientChart data={data} type=\"line\" \u002F>\n    \u003C\u002Fdiv>\n  )\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Cp>The \u003Ccode>ClientChart\u003C\u002Fcode> component is the only part that ships JS to the browser. The data fetching, the card wrapper, the title — all server-rendered HTML, zero bytes of JS. It's not a pure \"server component\" win in the ideological sense, but it's the right trade-off.\u003C\u002Fp>\n\n\u003Ch2>So Was It Worth It?\u003C\u002Fh2>\n\n\u003Cp>For our dashboard, yes — but not by the margin I expected. The initial load improvement is real, and for users on slow connections, it's a night-and-day difference. The bundle size drop is nothing to sneeze at either.\u003C\u002Fp>\n\n\u003Cp>But if I'd just optimized the client-side rendering — used React Compiler properly, memoized the right components, lazy-loaded the charts — I probably could have gotten 70% of the benefit with 20% of the work.\u003C\u002Fp>\n\n\u003Cp>Honestly, the biggest win wasn't performance at all. It was forcing me to think clearly about which components actually need interactivity and which are just data display. When every component is a client component by default, you get lazy. Server components make you intentional. That's the real value.\u003C\u002Fp>\n\n\u003Cp>I've been saving all the server\u002Fclient component patterns I figured out into \u003Ca href=\"\u002Fsnippetark\u002F\">Snippet Ark\u003C\u002Fa> — things like the hybrid table approach and the progressive chart loading pattern. When I hit the same problem in the next project, I won't have to re-derive it all from scratch.\u003C\u002Fp>\n\n\u003Cp>If you're about to dive into RSC, my advice is: start with the easiest wins first. Move your stat cards and display components to the server. Leave your forms and interactive elements on the client. Measure before and after. And don't be surprised if the performance story is more complicated than \"server = fast.\"\u003C\u002Fp>\n",1787652455105]