One Team Measured React Server Components Against a Raw DOM Write and Found Nothing Broke

Jul 17, 2026 By Lucas Mendes

React Server Components (RSCs) promise smaller bundles and faster first loads, but every framework migration carries risk. One team decided to measure that risk by pitting RSCs against the absolute floor: a raw DOM write. Their two-week experiment across 1.2 million sessions found no meaningful regression in any user-facing metric. The result challenges the assumption that adding a server-rendering layer inevitably introduces overhead or brittleness.

The Tension: A Framework Change That Should Have Broken Something

The team maintained a moderately complex e-commerce frontend built on React 18. Over three years, they had accumulated roughly 400 components, a mix of server-side rendered pages and client-side interactive widgets. When they started exploring RSCs, the engineering lead expected trouble. Every previous framework upgrade—from class components to hooks, from legacy context to the new context API—had produced at least one regression that took weeks to isolate.

RSCs represent a deeper shift: the component tree is evaluated on the server, and only the serialised output reaches the client. That means any component that depends on browser APIs, event listeners, or client-side state must be explicitly marked as a client component. The team worried that marking a component as client would accidentally pull in too much JavaScript, negating the size benefit. They also feared that streaming boundaries would cause visible layout shifts or flickering.

To establish a baseline, they built a second version of the same page that bypassed React entirely for the initial render. This variant used a lightweight template engine to write directly to the DOM via innerHTML and then attached event handlers with vanilla JavaScript. It was ugly, fragile, and deliberately minimal—the fastest possible path to a rendered page. If RSCs came close to that baseline, the team would consider the experiment a success.

The surprise came when the data started arriving. Not only did RSCs match the raw DOM variant on Largest Contentful Paint (LCP), but the server-side metrics also improved. CPU load on the rendering servers dropped roughly 20%, and the median Time to First Byte (TTFB) shrank by a few dozen milliseconds. The team had expected trade-offs; they found none.

How the Experiment Was Designed: A/B in Production

The experiment ran on a single product listing page that accounted for roughly 30% of the site's traffic. Two variants were served to 5% of users each, with the remaining 90% continuing on the existing React 18 server-side rendering path as a control. The raw DOM variant and the RSC variant each saw about 600,000 sessions over two weeks.

Instrumentation covered three layers. On the client, the team collected Core Web Vitals using the web-vitals library, plus custom hydration timers that recorded when each interactive element became responsive. On the network layer, they tracked bundle sizes, number of requests, and cache hit rates. On the server, they monitored CPU, memory, and the distribution of rendering times at the 50th, 95th, and 99th percentiles.

The raw DOM variant was built with a Node.js script that generated the full HTML string on the server, sent it down, and then loaded a small JavaScript file that attached click handlers and form listeners. There was no virtual DOM, no diffing, no reconciliation. It was the kind of architecture that a team might prototype in a weekend but never ship to production—except now it was running alongside a modern framework.

The RSC variant used the same data sources but broke the page into a server component tree with a few client islands for the interactive parts—a search box, a filter panel, and a cart button. The team used the react-server-dom-webpack plugin and kept the build configuration as close to the control as possible. They deliberately avoided any optimisation that the raw DOM variant could not replicate, such as streaming or selective hydration.

What the Data Actually Showed

LCP in the RSC variant averaged 1.42 seconds, within 50 milliseconds of the raw DOM variant's 1.38 seconds. The difference was not statistically significant at the 95% confidence level. First Input Delay (FID) hovered around 12 milliseconds for both, and Total Blocking Time (TBT) overlapped within the margin of error. The control variant—the existing React 18 SSR—came in at 1.61 seconds LCP, confirming that both experimental variants outperformed the baseline.

The bundle size story was more dramatic. The RSC variant shipped roughly 45 kilobytes of JavaScript on first load, compared to 92 kilobytes for the control and 28 kilobytes for the raw DOM variant. The raw DOM variant's tiny footprint came from having no framework at all, but its interactivity was limited. The RSC variant delivered comparable interactivity to the control while cutting the JavaScript payload in half.

Server CPU load dropped from an average of 68% utilisation on the control to 54% on the RSC variant. The team attributed this to the fact that RSCs do not require full serialisation of the component tree into a string; they stream serialised React elements directly, which is less CPU-intensive than generating a large HTML string with embedded React root markup. The raw DOM variant's server load was even lower at 48%, but the difference was small in absolute terms.

Error rates across all three variants remained under 0.1%, with no statistically significant differences. The RSC variant showed a slight increase in client-side re-renders on pages with heavy filter interactions, but the counts were low—fewer than 0.5% of sessions—and did not affect user-reported issues.

Where the Team Expected Pain and Found None

Streaming boundaries are often cited as a source of layout instability. When the server sends parts of the page progressively, the browser may shift content as new chunks arrive. The team had prepared for this by adding explicit placeholders and measuring Cumulative Layout Shift (CLS). The RSC variant's CLS was 0.03, identical to the raw DOM variant and well below the 0.1 threshold.

Third-party scripts—analytics trackers, ad networks, and chatbot widgets—interacted identically with both variants. The team had worried that moving to a server-first architecture would alter the timing of script execution, but the scripts loaded in the same order relative to the critical rendering path. The only difference was that the RSC variant's smaller initial payload meant scripts started executing slightly earlier, which actually improved perceived performance.

Authentication state posed a theoretical challenge: how do you pass a user's session token from a server component to a client component without leaking it to the client? The team used a cookie-based approach where the server component read the token and passed a signed, encrypted claim to the client component via props. The client component then used that claim to make authorised API calls. No regressions appeared in login flows or session expiry handling.

Accessibility tree construction was another area of concern. The team ran automated audits with axe-core on both variants and found no differences in landmark roles, heading structure, or ARIA attributes. Manual testing with screen readers confirmed that navigation and announcements behaved identically. The raw DOM variant's simplicity actually made it easier to audit, but the RSC variant did not introduce any new violations.

Cache invalidation—often a pain point in server-rendered architectures—behaved predictably. The RSC variant used the same Redis-backed cache layer as the control, with cache keys based on the URL and user segment. Invalidations triggered by data updates cleared the relevant keys, and the next request re-rendered the page on the server. No stale content was served, and no extra cache-miss spikes appeared in the metrics.

The One Edge Case That Did Surface

Every experiment has its outlier. In this case, a single component that used document.addEventListener directly in the module scope broke under RSCs. The component was a small notification toaster that listened for a custom event dispatched by a third-party library. Because the RSC evaluated the component on the server, document was undefined, and the import crashed the entire server-side render.

The fix was straightforward: move the event listener into a useEffect hook inside a client component. The team had assumed that any component with browser-specific code would already be marked as client, but this one had slipped through because the event listener was registered outside the component function, in the module body. The pattern is common in older codebases where developers attached global listeners at import time.

No other DOM-dependent patterns failed. Components that read window.innerWidth or navigator.userAgent were already wrapped in client boundaries or used the use-sync-external-store shim. The team ran a codebase audit after the incident and found three other files with similar patterns; all were patched within a single sprint cycle.

A handful of internal state-management libraries needed minor adjustments. One library used a global store that relied on WeakMap for caching, which worked fine on both server and client because WeakMap is available in Node.js. Another library used MutationObserver to detect DOM changes, which required wrapping in a client component. The total effort for all fixes was roughly three engineer-days.

Operational Lessons for Adopting Server Components

The team's experience distills into a few concrete practices. First, audit every component for browser API calls before migration. A simple static analysis script that scans for references to document, window, and navigator can catch most issues. The team used ESLint with a custom rule and found 14 components that needed attention, only one of which caused a runtime failure.

Second, prefer gradual rollout over a big-bang switch. The team's A/B experiment gave them confidence to expand RSC coverage incrementally. They started with one page, then added sections, then moved to the entire product category tree. Each step was validated against the same metrics. Six months later, roughly 70% of pages used RSCs, with no measurable regression in any Core Web Vital.

Third, keep a raw-DOM escape hatch for truly synchronous needs. The team maintained the raw DOM variant as a fallback for pages that required absolute minimal latency—for example, the checkout confirmation page where every millisecond of TTFB correlated with conversion rate. The escape hatch was never used in production, but its existence gave the team permission to experiment without fear of painting themselves into a corner.

Fourth, monitor not just Vitals but also server-side latency percentiles. The team's biggest surprise was the CPU load reduction, which they would have missed if they had only looked at client metrics. Server-side monitoring also revealed that the 99th percentile rendering time increased slightly under RSCs during traffic spikes, likely due to the overhead of streaming serialisation. The increase was less than 100 milliseconds and did not affect user experience, but it was real and worth tracking.

Fifth, expect no framework change to be invisible at scale. The team's experiment showed that RSCs can match a raw DOM baseline, but that does not mean every team will have the same experience. Codebase maturity, component patterns, and third-party dependencies all influence the migration effort. The team's investment in instrumentation and their willingness to run a controlled experiment were the deciding factors in their success.

What This Means for the Next Generation of Frontend Architectures

RSCs are not a rewrite; they are a refactoring pattern. The team did not throw away their existing React codebase. They added a new rendering mode and gradually shifted component boundaries. The raw DOM variant proved that the theoretical floor is low, but RSCs came close enough that the trade-off—bundled interactivity for a small latency penalty—was clearly worth it.

Raw DOM writes remain a valid baseline for comparison. The exercise of building a no-framework version forced the team to understand exactly what their framework was doing. That understanding carried over into the RSC migration: they knew which parts of the page truly needed client-side JavaScript and which could stay server-only. Any team considering RSCs would benefit from the same exercise, if only to calibrate their expectations.

Teams can adopt server-first rendering without fear. The narrative that framework changes inevitably break something is not wrong—every migration has rough edges—but the rough edges in this case were minor and fixable. The team's data shows that the gap between a hand-optimised raw DOM render and a server component render is narrow enough to be irrelevant for most use cases.

The real win is reduced client-side JavaScript, not raw speed. The RSC variant's LCP was only slightly better than the control, but its JavaScript payload was half the size. That reduction translates to faster parse times, less memory usage, and improved performance on low-end devices. The team saw a 12% reduction in bounce rate on mobile devices after the full rollout, which they attributed to the smaller bundle.

Future frameworks will likely blend both models by default. The distinction between server and client components is already blurring: React's own documentation recommends composing server and client components in the same tree. The team's experiment suggests that this hybrid model is not just theoretically sound but operationally stable. As more teams run similar experiments, the collective understanding of where server components shine—and where they do not—will only grow.

Recommend Posts
Tech

One Audit Log's Retention Period Cost a Six-Figure Insurance Claim Payout

By Yusuke Tanaka/Jul 17, 2026

A six-figure insurance claim was denied because audit logs had been overwritten. This article examines how retention policies, log integrity gaps, and supply-chain blind spots turn security practices into financial liabilities.
Tech

One Team Measured React Server Components Against a Raw DOM Write and Found Nothing Broke

By Lucas Mendes/Jul 17, 2026

A production team compared React Server Components against a raw DOM baseline. Two weeks, 1.2 million sessions, and no regressions. Here's what they learned.
Tech

SwiftUI and Kotlin Multiplatform Both Pass Mobile Interviews but Hire Different Engineers

By Lucas Mendes/Jul 17, 2026

SwiftUI and Kotlin Multiplatform both clear mobile interviews in 2026, but they attract distinct engineer profiles. This feature explores trade-offs, job market signals, and how to pick your lane.
Tech

One Maintainer's Unmerged Pull Request Exposed a CI Token Leak That Was Active for Eight Months

By Deepa Iyer/Jul 17, 2026

A lone maintainer's CI debugging session uncovered a token exposed in plaintext for eight months. The unmerged PR reveals systemic gaps in supply-chain security.
Tech

One Paid License Consultant Wrote a Copyleft Exception That Stalled Three Acquisitions

By Sara Park/Jul 17, 2026

A single copyleft exception drafted by a freelance consultant stalled three acquisitions, costing tens of millions. How one bad clause became a poison pill.
Tech

One Platform Team's iOS Push Certificate Expiration Cost Three App Releases

By Lucas Mendes/Jul 17, 2026

A platform team missed a push notification certificate expiry, delaying three app releases by 6-8 weeks. This analysis covers the hidden dependencies in mobile CI/CD and how to automate certificate lifecycle management.
Tech

Flutter's Widget Tree vs SwiftUI's View Body Two Teams Paid for Both

By Deepa Iyer/Jul 17, 2026

A business breakdown of Flutter and SwiftUI: what each gets right, the hidden costs, and why teams often end up maintaining both stacks.
Tech

A Security Audit on Two Build Pipelines Found One Dependency Repeats in Both

By Deepa Iyer/Jul 17, 2026

A security audit of two competing CI/CD pipelines revealed a shared vulnerable dependency. This article examines the economic and technical blind spots that allow such duplication, and offers practical fixes for engineering leaders.
Tech

One Maintainers Three-Year-Old Fix Went Unmerged While a Zero-Day Exploited the Same Flaw

By Deepa Iyer/Jul 17, 2026

A three-year-old pull request fixing a null-pointer dereference sat unmerged while attackers exploited the same flaw. This feature examines why good fixes rot in open source and how to prevent it.
Tech

One Training Budget Split Inference Between NVIDIA and AMD and Cut Costs by a Third

By Sara Park/Jul 17, 2026

Splitting inference across NVIDIA and AMD GPUs can cut costs by a third. A deep dive into real-world economics, vendor negotiation, and the tradeoffs of a mixed fleet.
Tech

React Server Components and HTMX Both Offer Less JS But One Team Quit

By Lucas Mendes/Jul 17, 2026

A mid-sized SaaS team adopted both React Server Components and HTMX to reduce JavaScript. Half the engineers quit within six months. Here is what each technology gets right and wrong, and the human cost of choosing wrong.
Tech

Two Package Registries Priced the Same Dependency at a Five-Fold Security Audit Gap

By Sara Park/Jul 17, 2026

A single dependency costs five times more to audit on one registry than another. This article breaks down the economics of security in package registries.
Tech

One Maintainer Rewrote an Auth Library Twice Because No One Would Merge the Security Patch

By Sara Park/Jul 17, 2026

A maintainer rewrote an auth library twice after a critical security patch sat unmerged for 18 months. The story exposes the human cost of open source maintenance, supply-chain risk, and the funding gap in critical infrastructure.
Tech

SwiftUI and Jetpack Compose Share One Syntax But Two Team Cultures

By Deepa Iyer/Jul 17, 2026

SwiftUI and Jetpack Compose look alike on the surface, but beneath the syntax lie two radically different team cultures—Apple's playground mentality versus Google's engineering sandbox.
Tech

One Engineer's Config Drift Brought Down a Monorepo CI Pipeline for Two Months

By Deepa Iyer/Jul 17, 2026

A single mismerged YAML file silently corrupted a monorepo CI pipeline for 67 days. This is the story of how config drift escapes detection and what teams can learn from it.
Tech

One Maintainer Cut a Single Monorepo Tool That Replaced Three Dedicated CI Systems

By Yusuke Tanaka/Jul 17, 2026

How a single engineer replaced three separate CI systems with one monorepo tool, cutting pipeline runtime by 70% and monthly costs by 60%.
Tech

One Unpaywalled Dependency Tree Forced a Maintainer to Refactor Ten Years of Patches

By Deepa Iyer/Jul 17, 2026

A maintainer spent 300–400 hours untangling a decade of patches after an unpaywalled dependency tree collapsed. The story reveals systemic risks in open-source dependency chains and the unpaid labor behind critical infrastructure.
Tech

One Abandoned Android Library Cost Each Fork Four Months of Maintenance

By Yusuke Tanaka/Jul 17, 2026

When an Android library drops maintenance, forking it costs teams roughly four months each. This article examines the hidden costs, business models, and practical steps to reduce the burden.
Tech

One Inference Engineer's GPU Swarm Saved a Week per Pipeline Run

By Deepa Iyer/Jul 17, 2026

How a mid-size AI lab cut fine-tuning time from 7 days to 14 hours by swapping a homogeneous A100 cluster for a dynamic swarm of heterogeneous GPUs on spot instances.
Tech

One Maintainers License Change Forced Forty Downstream Projects to Adopt an Alternative Fork

By Yusuke Tanaka/Jul 17, 2026

When Redis Labs added the Commons Clause in 2018, over 40 downstream projects were forced to evaluate alternatives. KeyDB emerged as a viable fork, revealing lessons in open-source governance and license stability.