<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Billy Okeyo</title>
    <description>The latest articles on DEV Community by Billy Okeyo (@billy_de_cartel).</description>
    <link>https://dev.to/billy_de_cartel</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F363015%2Fb7cad6bd-0cc8-4b82-b298-5fcdfcfe48cf.jpg</url>
      <title>DEV Community: Billy Okeyo</title>
      <link>https://dev.to/billy_de_cartel</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/billy_de_cartel"/>
    <language>en</language>
    <item>
      <title>Re-Renders Explained: What Actually Happens When Frontend State Changes</title>
      <dc:creator>Billy Okeyo</dc:creator>
      <pubDate>Fri, 11 Sep 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/billy_de_cartel/re-renders-explained-what-actually-happens-when-frontend-state-changes-1bp3</link>
      <guid>https://dev.to/billy_de_cartel/re-renders-explained-what-actually-happens-when-frontend-state-changes-1bp3</guid>
      <description>&lt;p&gt;One of the most common pieces of advice in frontend development is:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;Avoid unnecessary re-renders.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You hear it when working with React, when discussing component performance, and when someone suggests &lt;code&gt;memo&lt;/code&gt;, &lt;code&gt;useMemo&lt;/code&gt;, or &lt;code&gt;useCallback&lt;/code&gt;; eventually, it can start sounding as though re-rendering is inherently bad, but it isn’t.&lt;/p&gt;

&lt;p&gt;Re-rendering is how modern UI frameworks keep the screen synchronized with application state.&lt;/p&gt;

&lt;p&gt;Consider a simple counter:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;function Counter() {
  const [count, setCount] = useState(0);

  return (
    &amp;lt;button onClick={() =&amp;gt; setCount(count + 1)}&amp;gt;
      Count: {count}
    &amp;lt;/button&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The browser initially displays:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Count: 0
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Then the user clicks the button.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Count: 0
    │
    │ click
    ▼
setCount(1)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The component re-renders.&lt;/p&gt;

&lt;p&gt;Eventually, the browser displays:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Count: 1
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;That process sounds simple, but there are several steps hidden in between.&lt;/p&gt;

&lt;p&gt;React doesn’t immediately replace the entire DOM.&lt;/p&gt;

&lt;p&gt;The browser doesn’t repaint the entire page just because a state variable changed.&lt;/p&gt;

&lt;p&gt;Instead, the framework performs work to determine what the UI &lt;strong&gt;should now look like&lt;/strong&gt;, compares that result with what existed before, and updates only the parts of the real DOM that actually changed.&lt;/p&gt;

&lt;p&gt;A simplified mental model looks like this:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;State Change
    │
    ▼
Component Re-renders
    │
    ▼
New UI Description
    │
    ▼
Compare With Previous Result
    │
    ▼
Determine Actual Changes
    │
    ▼
Update DOM
    │
    ▼
Browser Rendering Pipeline
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Understanding this process is important because it helps separate two ideas developers often confuse: &lt;strong&gt;React rendering&lt;/strong&gt; and &lt;strong&gt;browser rendering&lt;/strong&gt;, which are not the same thing.&lt;/p&gt;





&lt;h2 id="react-rendering-is-not-browser-rendering"&gt;React Rendering Is Not Browser Rendering&lt;/h2&gt;

&lt;p&gt;In the first article in this series, we explored the browser rendering pipeline:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;DOM
 │
 ▼
Style
 │
 ▼
Layout
 │
 ▼
Paint
 │
 ▼
Composite
 │
 ▼
Pixels
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;That process belongs to the browser.&lt;/p&gt;

&lt;p&gt;React’s rendering happens before that.&lt;/p&gt;

&lt;p&gt;React decides &lt;strong&gt;what the DOM should look like&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The browser decides &lt;strong&gt;how that DOM becomes pixels&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Application State
      │
      ▼
     React
      │
      ▼
DOM Changes
      │
      ▼
   Browser
      │
      ▼
Layout / Paint / Composite
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This distinction matters.&lt;/p&gt;

&lt;p&gt;A React component can re-render without causing any DOM update at all.&lt;/p&gt;

&lt;p&gt;And if the DOM doesn’t change, the browser may have very little visual work to do.&lt;/p&gt;





&lt;h2 id="what-happens-when-state-changes"&gt;What Happens When State Changes?&lt;/h2&gt;

&lt;p&gt;Let’s use a slightly larger example.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;function Profile() {
  const [name, setName] = useState("Billy");

  return (
    &amp;lt;section&amp;gt;
      &amp;lt;h1&amp;gt;Hello, {name}&amp;lt;/h1&amp;gt;

      &amp;lt;button onClick={() =&amp;gt; setName("William")}&amp;gt;
        Change Name
      &amp;lt;/button&amp;gt;
    &amp;lt;/section&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Initially, React produces a UI description conceptually similar to:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;section
├── h1
│   └── "Hello, Billy"
└── button
    └── "Change Name"
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;After:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;setName("William");
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;React renders the component again.&lt;/p&gt;

&lt;p&gt;Now the result looks like:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;section
├── h1
│   └── "Hello, William"
└── button
    └── "Change Name"
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Most of the structure is identical.&lt;/p&gt;

&lt;p&gt;Only one text value changed.&lt;/p&gt;

&lt;p&gt;So React does not need to rebuild the entire DOM.&lt;/p&gt;

&lt;p&gt;It only needs to update:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;"Hello, Billy"
       ↓
"Hello, William"
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The process can be understood as three broad stages:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Render
   │
   ▼
Reconciliation
   │
   ▼
Commit
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Let’s look at each one.&lt;/p&gt;





&lt;h2 id="the-render-phase"&gt;The Render Phase&lt;/h2&gt;

&lt;p&gt;When a component re-renders, React executes the component function again.&lt;/p&gt;

&lt;p&gt;That point is worth emphasizing.&lt;/p&gt;

&lt;p&gt;Consider:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;function Counter() {
  console.log("Counter rendered");

  const [count, setCount] = useState(0);

  return &amp;lt;p&amp;gt;{count}&amp;lt;/p&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Whenever React renders &lt;code&gt;Counter&lt;/code&gt;, the function executes.&lt;/p&gt;

&lt;p&gt;That means:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;console.log("Counter rendered");
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;runs again.&lt;/p&gt;

&lt;p&gt;So rendering does &lt;strong&gt;not&lt;/strong&gt; mean React simply reuses the previous function result.&lt;/p&gt;

&lt;p&gt;The component runs again to determine what the UI should look like for the current state and props.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;State = 0

Counter()
   │
   ▼
&amp;lt;p&amp;gt;0&amp;lt;/p&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Then:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;State = 1

Counter()
   │
   ▼
&amp;lt;p&amp;gt;1&amp;lt;/p&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;React now has a new description of the UI.&lt;/p&gt;

&lt;p&gt;But no DOM changes necessarily happened yet.&lt;/p&gt;

&lt;p&gt;That distinction becomes very important.&lt;/p&gt;





&lt;h2 id="reconciliation"&gt;Reconciliation&lt;/h2&gt;

&lt;p&gt;Once React has the new result, it compares it with the previous one.&lt;/p&gt;

&lt;p&gt;This process is known as &lt;strong&gt;reconciliation&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Suppose before:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;lt;div&amp;gt;
  &amp;lt;h1&amp;gt;Products&amp;lt;/h1&amp;gt;
  &amp;lt;p&amp;gt;3 items&amp;lt;/p&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;and after:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;lt;div&amp;gt;
  &amp;lt;h1&amp;gt;Products&amp;lt;/h1&amp;gt;
  &amp;lt;p&amp;gt;4 items&amp;lt;/p&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The structure did not change.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;div&lt;/code&gt; still exists.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;h1&lt;/code&gt; is the same.&lt;/p&gt;

&lt;p&gt;Only the paragraph text changed.&lt;/p&gt;

&lt;p&gt;React can therefore conclude:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Keep div
Keep h1
Keep p
Update text
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;instead of:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Delete everything
Recreate everything
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This is the heart of reconciliation.&lt;/p&gt;

&lt;p&gt;React is determining the minimum DOM work needed to make the actual interface match the new component output.&lt;/p&gt;





&lt;h2 id="the-commit-phase"&gt;The Commit Phase&lt;/h2&gt;

&lt;p&gt;Once React knows what needs to change, it performs those mutations during the &lt;strong&gt;commit phase&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This is where the real DOM is updated.&lt;/p&gt;

&lt;p&gt;So the flow becomes:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;State Change
    │
    ▼
Render Phase
Component functions run
    │
    ▼
Reconciliation
Compare old and new
    │
    ▼
Commit Phase
Update actual DOM
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Only after actual DOM changes occur does the browser potentially need to perform its own rendering work.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;React Commit
     │
     ▼
DOM Changed
     │
     ▼
Browser
     │
     ├── Style?
     ├── Layout?
     ├── Paint?
     └── Composite?
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This connects directly to everything we learned about reflow and repaint.&lt;/p&gt;





&lt;h2 id="re-rendering-does-not-mean-dom-mutation"&gt;Re-Rendering Does Not Mean DOM Mutation&lt;/h2&gt;

&lt;p&gt;This is one of the biggest misconceptions in React performance discussions.&lt;/p&gt;

&lt;p&gt;Suppose:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;function Greeting({ name }) {
  console.log("render");

  return &amp;lt;h1&amp;gt;Hello {name}&amp;lt;/h1&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;React may execute this component again.&lt;/p&gt;

&lt;p&gt;But if:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;name = "Billy"
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;both before and after the render, the resulting UI may be identical.&lt;/p&gt;

&lt;p&gt;React can determine:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Previous: &amp;lt;h1&amp;gt;Hello Billy&amp;lt;/h1&amp;gt;

New:      &amp;lt;h1&amp;gt;Hello Billy&amp;lt;/h1&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;No meaningful DOM change is required.&lt;/p&gt;

&lt;p&gt;So:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Component Render
      ≠
DOM Update
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;And:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;DOM Update
      ≠
Full Browser Repaint
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;These are separate stages.&lt;/p&gt;

&lt;p&gt;That is why the statement:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“This component rendered again!”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;doesn’t automatically mean:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“We have a serious performance problem.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The cost depends on what happens during that render and what actual work follows.&lt;/p&gt;





&lt;h2 id="why-child-components-re-render"&gt;Why Child Components Re-Render&lt;/h2&gt;

&lt;p&gt;This is another area that confuses many developers.&lt;/p&gt;

&lt;p&gt;Consider:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;function App() {
  const [count, setCount] = useState(0);

  return (
    &amp;lt;&amp;gt;
      &amp;lt;button onClick={() =&amp;gt; setCount(count + 1)}&amp;gt;
        Increment
      &amp;lt;/button&amp;gt;

      &amp;lt;Header /&amp;gt;
    &amp;lt;/&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;code&gt;Header&lt;/code&gt; doesn’t use &lt;code&gt;count&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Yet when &lt;code&gt;App&lt;/code&gt; re-renders, &lt;code&gt;Header&lt;/code&gt; may also render again.&lt;/p&gt;

&lt;p&gt;Why?&lt;/p&gt;

&lt;p&gt;Because &lt;code&gt;Header&lt;/code&gt; is part of the component tree produced by &lt;code&gt;App&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;App re-renders
    │
    ├── Button
    │
    └── Header
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;React walks through that subtree to determine what the updated UI should look like.&lt;/p&gt;

&lt;p&gt;Again, this doesn’t necessarily mean the DOM represented by &lt;code&gt;Header&lt;/code&gt; changes.&lt;/p&gt;

&lt;p&gt;The component can execute again while producing exactly the same result.&lt;/p&gt;





&lt;h2 id="parent-re-render-does-not-mean-everything-changed"&gt;Parent Re-Render Does Not Mean Everything Changed&lt;/h2&gt;

&lt;p&gt;Suppose:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;function Header() {
  return (
    &amp;lt;header&amp;gt;
      &amp;lt;h1&amp;gt;My Store&amp;lt;/h1&amp;gt;
    &amp;lt;/header&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Every time the parent renders, this may execute again.&lt;/p&gt;

&lt;p&gt;But React compares:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Previous Header

&amp;lt;header&amp;gt;
  &amp;lt;h1&amp;gt;My Store&amp;lt;/h1&amp;gt;
&amp;lt;/header&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;with:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;New Header

&amp;lt;header&amp;gt;
  &amp;lt;h1&amp;gt;My Store&amp;lt;/h1&amp;gt;
&amp;lt;/header&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Nothing changed.&lt;/p&gt;

&lt;p&gt;So no DOM mutation is required.&lt;/p&gt;

&lt;p&gt;This is why React can tolerate far more component renders than developers often assume.&lt;/p&gt;

&lt;p&gt;The framework is designed around the idea that rendering should generally be cheap.&lt;/p&gt;

&lt;p&gt;Problems appear when rendering itself becomes expensive.&lt;/p&gt;





&lt;h2 id="when-re-renders-actually-become-expensive"&gt;When Re-Renders Actually Become Expensive&lt;/h2&gt;

&lt;p&gt;Imagine a component does this:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;function Analytics({ transactions }) {
  const report = generateComplexReport(transactions);

  return &amp;lt;ReportView report={report} /&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;If &lt;code&gt;generateComplexReport()&lt;/code&gt; processes hundreds of thousands of records, every render can become expensive.&lt;/p&gt;

&lt;p&gt;Now suppose a parent re-renders frequently.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Parent changes
     │
     ▼
Analytics renders
     │
     ▼
Expensive calculation
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Even if the resulting DOM doesn’t change, the expensive JavaScript already ran.&lt;/p&gt;

&lt;p&gt;This is where unnecessary rendering becomes a real performance problem.&lt;/p&gt;

&lt;p&gt;The issue isn’t:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;DOM updated too much
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;It might instead be:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;JavaScript executed too much
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;That connects directly to the Event Loop article.&lt;/p&gt;

&lt;p&gt;Heavy component rendering occupies the main thread just like any other JavaScript.&lt;/p&gt;





&lt;h2 id="a-simple-example-of-expensive-rendering"&gt;A Simple Example of Expensive Rendering&lt;/h2&gt;

&lt;p&gt;Consider:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;function ProductList({ products, search }) {
  const results = products
    .filter(product =&amp;gt;
      product.name
        .toLowerCase()
        .includes(search.toLowerCase())
    )
    .sort((a, b) =&amp;gt; a.price - b.price);

  return (
    &amp;lt;ul&amp;gt;
      {results.map(product =&amp;gt; (
        &amp;lt;li key={product.id}&amp;gt;
          {product.name}
        &amp;lt;/li&amp;gt;
      ))}
    &amp;lt;/ul&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;For:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;50 products
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;this probably doesn’t matter.&lt;/p&gt;

&lt;p&gt;For:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;500,000 products
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;it might.&lt;/p&gt;

&lt;p&gt;Now imagine an unrelated state change causes the component to render repeatedly.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Theme toggled
     │
     ▼
Parent re-render
     │
     ▼
ProductList executes
     │
     ▼
Filter 500,000 products
     │
     ▼
Sort 500,000 products
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Even if the product list itself didn’t actually change, substantial work occurred.&lt;/p&gt;

&lt;p&gt;This is where memoization can help.&lt;/p&gt;





&lt;h2 id="what-usememo-actually-does"&gt;What &lt;code&gt;useMemo&lt;/code&gt; Actually Does&lt;/h2&gt;

&lt;p&gt;Consider:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;const filteredProducts = useMemo(() =&amp;gt; {
  return products.filter(product =&amp;gt;
    product.name.includes(search)
  );
}, [products, search]);
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The goal of &lt;code&gt;useMemo&lt;/code&gt; is not:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Make React faster.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Its purpose is more specific.&lt;/p&gt;

&lt;p&gt;It says:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Recalculate this value only when these dependencies change.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Conceptually:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Render
  │
  ▼
Did products or search change?
      │
   ┌──┴───┐
   │      │
  Yes     No
   │      │
   ▼      ▼
Calculate  Reuse
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;If some unrelated state changes:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;sidebarOpen
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;the component may still re-render.&lt;/p&gt;

&lt;p&gt;But the expensive product filtering can be skipped.&lt;/p&gt;

&lt;p&gt;This distinction is important.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;useMemo&lt;/code&gt; doesn’t necessarily prevent rendering.&lt;/p&gt;

&lt;p&gt;It prevents recalculating a memoized result when dependencies haven’t changed.&lt;/p&gt;





&lt;h2 id="do-not-memoize-everything"&gt;Do Not Memoize Everything&lt;/h2&gt;

&lt;p&gt;After learning about &lt;code&gt;useMemo&lt;/code&gt;, it’s tempting to do this:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;const name = useMemo(() =&amp;gt; {
  return `${firstName} ${lastName}`;
}, [firstName, lastName]);
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;But calculating:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;`${firstName} ${lastName}`
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;is extremely cheap.&lt;/p&gt;

&lt;p&gt;Now we’ve added:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Memoization logic
Dependency tracking
Additional code
Mental overhead
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;to avoid a trivial string concatenation.&lt;/p&gt;

&lt;p&gt;Optimization itself has a cost.&lt;/p&gt;

&lt;p&gt;A useful rule is:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;Memoize expensive computations or values whose identity meaningfully matters, not every expression in your component.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Measure before making everything more complicated.&lt;/p&gt;





&lt;h2 id="reactmemo"&gt;&lt;code&gt;React.memo&lt;/code&gt;&lt;/h2&gt;

&lt;p&gt;Now suppose the expensive work happens inside a child component.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;function Dashboard() {
  const [sidebarOpen, setSidebarOpen] = useState(false);

  return (
    &amp;lt;&amp;gt;
      &amp;lt;Sidebar open={sidebarOpen} /&amp;gt;
      &amp;lt;ExpensiveChart /&amp;gt;
    &amp;lt;/&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;When &lt;code&gt;sidebarOpen&lt;/code&gt; changes:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Dashboard re-renders
      │
      ├── Sidebar
      └── ExpensiveChart
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;But &lt;code&gt;ExpensiveChart&lt;/code&gt; receives no changing props.&lt;/p&gt;

&lt;p&gt;We may wrap it:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;const ExpensiveChart = React.memo(function ExpensiveChart() {
  return &amp;lt;Chart /&amp;gt;;
});
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Now React can compare its props and potentially skip rendering the component when those props haven’t changed.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Parent re-renders
      │
      ▼
Check child props
      │
   ┌──┴───┐
   │      │
Changed  Same
   │      │
   ▼      ▼
Render   Skip
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Again, the point isn’t that every child should use &lt;code&gt;React.memo&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It is useful when:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;The component renders frequently&lt;/li&gt;
  &lt;li&gt;Rendering is meaningfully expensive&lt;/li&gt;
  &lt;li&gt;Its props often remain unchanged&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Otherwise, the additional complexity may provide little benefit.&lt;/p&gt;





&lt;h2 id="why-reactmemo-sometimes-doesnt-work"&gt;Why &lt;code&gt;React.memo&lt;/code&gt; Sometimes Doesn’t Work&lt;/h2&gt;

&lt;p&gt;Consider:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;const ProductCard = React.memo(function ProductCard({
  product,
  onSelect
}) {
  return (
    &amp;lt;button onClick={() =&amp;gt; onSelect(product.id)}&amp;gt;
      {product.name}
    &amp;lt;/button&amp;gt;
  );
});
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The parent does:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;lt;ProductCard
  product={product}
  onSelect={(id) =&amp;gt; setSelectedId(id)}
/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Every parent render creates a new function:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;(id) =&amp;gt; setSelectedId(id)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Even though the function does the same thing, its identity is new.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Previous render:

onSelect = Function A

New render:

onSelect = Function B
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;So from a shallow prop-comparison perspective:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Function A !== Function B
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;and the memoized child may render again.&lt;/p&gt;

&lt;p&gt;This is where &lt;code&gt;useCallback&lt;/code&gt; enters the picture.&lt;/p&gt;





&lt;h2 id="what-usecallback-actually-does"&gt;What &lt;code&gt;useCallback&lt;/code&gt; Actually Does&lt;/h2&gt;

&lt;p&gt;You might write:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;const handleSelect = useCallback((id) =&amp;gt; {
  setSelectedId(id);
}, []);
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Then:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;lt;ProductCard
  product={product}
  onSelect={handleSelect}
/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Now the function reference can remain stable across renders unless its dependencies change.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Render 1
handleSelect = Function A

Render 2
handleSelect = Function A

Render 3
handleSelect = Function A
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This can help when function identity matters, especially when passing callbacks into memoized child components.&lt;/p&gt;

&lt;p&gt;But again:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;&lt;code&gt;useCallback&lt;/code&gt; does not magically make a function faster.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It memoizes the function reference.&lt;/p&gt;

&lt;p&gt;If nothing depends on that identity, it may provide no practical benefit.&lt;/p&gt;





&lt;h2 id="usememo-vs-usecallback"&gt;
&lt;code&gt;useMemo&lt;/code&gt; vs &lt;code&gt;useCallback&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The difference is simple.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;useMemo&lt;/code&gt; memoizes a &lt;strong&gt;value&lt;/strong&gt;.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;const total = useMemo(() =&amp;gt; {
  return calculateTotal(items);
}, [items]);
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;code&gt;useCallback&lt;/code&gt; memoizes a &lt;strong&gt;function reference&lt;/strong&gt;.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;const handleSave = useCallback(() =&amp;gt; {
  save(items);
}, [items]);
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Conceptually:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;useMemo
   │
   ▼
Remember result


useCallback
   │
   ▼
Remember function
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Both are performance tools.&lt;/p&gt;

&lt;p&gt;Neither should automatically appear everywhere.&lt;/p&gt;





&lt;h2 id="object-identity-causes-similar-problems"&gt;Object Identity Causes Similar Problems&lt;/h2&gt;

&lt;p&gt;Suppose:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;lt;Chart
  options={{
    showLegend: true,
    responsive: true
  }}
/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Every render creates a new object.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Render 1 → Object A

Render 2 → Object B
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Even though:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Object A contents
      =
Object B contents
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;their references differ.&lt;/p&gt;

&lt;p&gt;If &lt;code&gt;Chart&lt;/code&gt; is memoized and relies on shallow prop comparison, it may still render again.&lt;/p&gt;

&lt;p&gt;One solution can be:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;const options = useMemo(() =&amp;gt; ({
  showLegend: true,
  responsive: true
}), []);

&amp;lt;Chart options={options} /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Now the object identity remains stable.&lt;/p&gt;

&lt;p&gt;But the same warning applies:&lt;/p&gt;

&lt;p&gt;Only optimize where the identity actually causes meaningful work.&lt;/p&gt;





&lt;h2 id="keys-and-reconciliation"&gt;Keys and Reconciliation&lt;/h2&gt;

&lt;p&gt;Keys are another important part of how React reasons about changes.&lt;/p&gt;

&lt;p&gt;Consider:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;{products.map(product =&amp;gt; (
  &amp;lt;ProductCard
    key={product.id}
    product={product}
  /&amp;gt;
))}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The key helps React identify which item corresponds to which previous item.&lt;/p&gt;

&lt;p&gt;Suppose:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Before

A
B
C
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;then:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;After

A
X
B
C
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;With stable keys, React can understand:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;A → same
X → new
B → same
C → same
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Without useful identity, React has a harder time reasoning about which items moved, appeared, or disappeared.&lt;/p&gt;

&lt;p&gt;This matters not only for performance but also for preserving component state correctly.&lt;/p&gt;





&lt;h2 id="why-array-index-keys-can-be-problematic"&gt;Why Array Index Keys Can Be Problematic&lt;/h2&gt;

&lt;p&gt;Consider:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;items.map((item, index) =&amp;gt; (
  &amp;lt;Row key={index} item={item} /&amp;gt;
));
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Suppose:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;0 → Alice
1 → Billy
2 → John
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Then Alice is removed.&lt;/p&gt;

&lt;p&gt;Now:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;0 → Billy
1 → John
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;React sees:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;key 0 still exists
key 1 still exists
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;even though the underlying items associated with those positions changed.&lt;/p&gt;

&lt;p&gt;This can cause surprising state behaviour in interactive lists.&lt;/p&gt;

&lt;p&gt;Stable business identifiers are usually better:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;lt;Row
  key={user.id}
  item={user}
/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Keys are fundamentally about identity.&lt;/p&gt;

&lt;p&gt;They tell React:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;This element represents the same conceptual thing as before.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;





&lt;h2 id="state-position-matters"&gt;State Position Matters&lt;/h2&gt;

&lt;p&gt;React associates state with a component’s position and identity in the rendered tree.&lt;/p&gt;

&lt;p&gt;Imagine:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;{loggedIn ? (
  &amp;lt;Dashboard /&amp;gt;
) : (
  &amp;lt;Login /&amp;gt;
)}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;When the condition changes, one component leaves and another enters.&lt;/p&gt;

&lt;p&gt;Its state lifecycle changes accordingly.&lt;/p&gt;

&lt;p&gt;Similarly, changing a component’s key can cause React to treat it as a new component.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;lt;Profile key={userId} userId={userId} /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Changing:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;userId = 1
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;to:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;userId = 2
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;can intentionally reset &lt;code&gt;Profile&lt;/code&gt; state because React treats the new key as a different identity.&lt;/p&gt;

&lt;p&gt;This can be useful for cases such as resetting forms when switching records.&lt;/p&gt;





&lt;h2 id="render-phase-vs-effects"&gt;Render Phase vs Effects&lt;/h2&gt;

&lt;p&gt;Now consider:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;function Profile({ userId }) {
  const [user, setUser] = useState(null);

  useEffect(() =&amp;gt; {
    fetchUser(userId).then(setUser);
  }, [userId]);

  return &amp;lt;ProfileView user={user} /&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The effect doesn’t run in the middle of calculating the component’s JSX.&lt;/p&gt;

&lt;p&gt;Rendering determines what the UI should look like.&lt;/p&gt;

&lt;p&gt;Effects are for synchronizing with things outside the pure render calculation, such as:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Network
Browser APIs
Subscriptions
Timers
Third-party libraries
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This distinction helps explain why render functions should ideally remain pure.&lt;/p&gt;

&lt;p&gt;Given the same state and props:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;same input
   │
   ▼
same UI description
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;That makes React’s rendering model much easier to reason about.&lt;/p&gt;





&lt;h2 id="why-side-effects-during-render-are-dangerous"&gt;Why Side Effects During Render Are Dangerous&lt;/h2&gt;

&lt;p&gt;Imagine:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;function BadComponent() {
  fetch("/api/analytics");

  return &amp;lt;div&amp;gt;Hello&amp;lt;/div&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Every render triggers a network request.&lt;/p&gt;

&lt;p&gt;If the component renders three times:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Render 1 → request

Render 2 → request

Render 3 → request
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Now application behavior depends on how often React happens to render.&lt;/p&gt;

&lt;p&gt;That is fragile.&lt;/p&gt;

&lt;p&gt;Rendering should describe UI, not perform unrelated side effects.&lt;/p&gt;

&lt;p&gt;Those operations belong in appropriate event handlers, effects, or data-fetching mechanisms.&lt;/p&gt;





&lt;h2 id="strict-mode-can-make-this-more-visible"&gt;Strict Mode Can Make This More Visible&lt;/h2&gt;

&lt;p&gt;During development, React Strict Mode may intentionally invoke certain logic more than once to help expose impure rendering and incorrect effect handling.&lt;/p&gt;

&lt;p&gt;Developers sometimes see:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Why is this component rendering twice?
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;and assume React is broken.&lt;/p&gt;

&lt;p&gt;But development behavior may deliberately stress-test assumptions.&lt;/p&gt;

&lt;p&gt;That is another reason not to attach important side effects directly to render execution.&lt;/p&gt;

&lt;p&gt;A component should remain safe to evaluate when React needs to understand what the UI should look like.&lt;/p&gt;





&lt;h2 id="re-renders-and-context"&gt;Re-Renders and Context&lt;/h2&gt;

&lt;p&gt;Suppose:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;lt;AppContext.Provider
  value={{
    user,
    theme,
    cart,
    notifications
  }}
&amp;gt;
  &amp;lt;App /&amp;gt;
&amp;lt;/AppContext.Provider&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Many components consume this context.&lt;/p&gt;

&lt;p&gt;Then:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;notifications changes
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The provider’s value changes.&lt;/p&gt;

&lt;p&gt;Consumers may need to render again, including ones primarily interested in other parts of that context.&lt;/p&gt;

&lt;p&gt;This is why very large contexts can sometimes become performance hotspots.&lt;/p&gt;

&lt;p&gt;A better architecture might separate concerns:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;UserContext

ThemeContext

CartContext

NotificationContext
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;or use a store with selector-based subscriptions.&lt;/p&gt;

&lt;p&gt;Again, the principle from the previous state-management articles applies:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;State distribution affects how much of your component tree responds when state changes.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;





&lt;h2 id="global-state-and-selectors"&gt;Global State and Selectors&lt;/h2&gt;

&lt;p&gt;Imagine a store containing:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;user
cart
theme
notifications
sidebar
products
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;A component needs only:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;cart.length
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;A selector lets it subscribe to that specific information.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Global Store
    │
    ├── Component A → user
    │
    ├── Component B → cart count
    │
    └── Component C → theme
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Now:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;user changes
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;doesn’t necessarily require the cart-count component to render.&lt;/p&gt;

&lt;p&gt;This is one reason state architecture and rendering performance are deeply connected.&lt;/p&gt;

&lt;p&gt;The question isn’t only:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Where do we store state?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It is also:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;Which components are notified when that state changes?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;





&lt;h2 id="server-state-can-trigger-re-renders-too"&gt;Server State Can Trigger Re-Renders Too&lt;/h2&gt;

&lt;p&gt;From the previous article, we know server-state libraries maintain cached data.&lt;/p&gt;

&lt;p&gt;Suppose:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;const { data: products } = useQuery({
  queryKey: ["products"],
  queryFn: fetchProducts
});
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The cache receives updated server data.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Old Products
      │
      ▼
Query Refetch
      │
      ▼
New Products
      │
      ▼
Subscribed Components Re-render
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;That is expected.&lt;/p&gt;

&lt;p&gt;The important thing is that components interested in unrelated queries don’t need to respond.&lt;/p&gt;

&lt;p&gt;Well-designed subscriptions help contain updates to the parts of the interface that actually care about them.&lt;/p&gt;





&lt;h2 id="rendering-large-lists"&gt;Rendering Large Lists&lt;/h2&gt;

&lt;p&gt;One place re-render costs become very visible is large collections.&lt;/p&gt;

&lt;p&gt;Suppose:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;{transactions.map(transaction =&amp;gt; (
  &amp;lt;TransactionRow
    key={transaction.id}
    transaction={transaction}
  /&amp;gt;
))}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;For:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;20 transactions
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;probably fine.&lt;/p&gt;

&lt;p&gt;For:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;100,000 transactions
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;you have a different problem.&lt;/p&gt;

&lt;p&gt;Even if React efficiently reconciles the list, rendering and maintaining tens of thousands of DOM elements is expensive.&lt;/p&gt;

&lt;p&gt;This is where &lt;strong&gt;virtualization&lt;/strong&gt; becomes useful.&lt;/p&gt;

&lt;p&gt;Instead of rendering every row:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Rows 1 → 100,000
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;render only the ones currently visible:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Rows 420 → 450
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Conceptually:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Dataset

100,000 rows
     │
     ▼
Virtualizer
     │
     ▼
Visible viewport
     │
     ▼
~30 DOM rows
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This reduces both framework rendering work and browser DOM/rendering work.&lt;/p&gt;

&lt;p&gt;Libraries such as TanStack Virtual and react-window use this approach.&lt;/p&gt;





&lt;h2 id="component-boundaries-matter"&gt;Component Boundaries Matter&lt;/h2&gt;

&lt;p&gt;Imagine one enormous component:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;function Dashboard() {
  // 2,000 lines of state, calculations and JSX
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Any state change causes that component’s render logic to execute again.&lt;/p&gt;

&lt;p&gt;Breaking the interface into meaningful components can create clearer boundaries:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Dashboard
│
├── Header
├── BalanceCard
├── Transactions
├── SpendingChart
└── Goals
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This does not automatically guarantee fewer renders.&lt;/p&gt;

&lt;p&gt;But it creates opportunities for:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Independent state ownership
Memoization
Selective subscriptions
Smaller render workloads
Clearer architecture
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Good component design is therefore partly about defining sensible rendering boundaries.&lt;/p&gt;





&lt;h2 id="should-you-fear-re-renders"&gt;Should You Fear Re-Renders?&lt;/h2&gt;

&lt;p&gt;No.&lt;/p&gt;

&lt;p&gt;This is probably the most important practical lesson in the article.&lt;/p&gt;

&lt;p&gt;A component doing this:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;function Name({ name }) {
  return &amp;lt;p&amp;gt;{name}&amp;lt;/p&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;can execute extremely quickly.&lt;/p&gt;

&lt;p&gt;Spending hours preventing that component from rendering again may make your code harder to maintain without producing any measurable user benefit.&lt;/p&gt;

&lt;p&gt;Meanwhile, a single component containing:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Expensive calculations
Large lists
Complex charts
Heavy parsing
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;can create real problems.&lt;/p&gt;

&lt;p&gt;So instead of:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;How do I stop all re-renders?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;ask:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;Which renders are actually expensive?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is a much healthier performance mindset.&lt;/p&gt;





&lt;h2 id="the-wrong-way-to-optimize"&gt;The Wrong Way to Optimize&lt;/h2&gt;

&lt;p&gt;A codebase can quickly become:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;const Component = memo(({ value }) =&amp;gt; {
  const result = useMemo(() =&amp;gt; calculate(value), [value]);

  const onClick = useCallback(() =&amp;gt; {
    ...
  }, []);

  ...
});
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;everywhere.&lt;/p&gt;

&lt;p&gt;Memoization becomes the default rather than a targeted optimization.&lt;/p&gt;

&lt;p&gt;Now every developer has to reason about:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Dependencies
Stable references
Memoization boundaries
Stale closures
Prop identity
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;even when the underlying render takes microseconds.&lt;/p&gt;

&lt;p&gt;You’ve optimized the framework workload while increasing human workload.&lt;/p&gt;

&lt;p&gt;That’s not always a good trade.&lt;/p&gt;





&lt;h2 id="measure-before-optimizing"&gt;Measure Before Optimizing&lt;/h2&gt;

&lt;p&gt;The browser and React both provide tools for identifying performance problems.&lt;/p&gt;

&lt;p&gt;React DevTools includes profiling capabilities that can help answer questions such as:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Which component rendered?

How long did it take?

Why did it render?

Which parts of the tree were expensive?
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Browser performance tools can then show what happened afterward:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;JavaScript
     │
     ▼
DOM Commit
     │
     ▼
Layout
     │
     ▼
Paint
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Combining the two gives you a much clearer picture.&lt;/p&gt;

&lt;p&gt;Perhaps React rendering is expensive.&lt;/p&gt;

&lt;p&gt;Perhaps React is fast and browser layout is the real problem.&lt;/p&gt;

&lt;p&gt;Perhaps neither is the bottleneck and you’re waiting on a network request.&lt;/p&gt;

&lt;p&gt;Optimization should follow evidence.&lt;/p&gt;





&lt;h2 id="a-practical-example"&gt;A Practical Example&lt;/h2&gt;

&lt;p&gt;Suppose a dashboard has:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;function Dashboard() {
  const [sidebarOpen, setSidebarOpen] = useState(false);

  return (
    &amp;lt;&amp;gt;
      &amp;lt;Sidebar
        open={sidebarOpen}
        onClose={() =&amp;gt; setSidebarOpen(false)}
      /&amp;gt;

      &amp;lt;TransactionsTable /&amp;gt;

      &amp;lt;LargeAnalyticsChart /&amp;gt;
    &amp;lt;/&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Toggling the sidebar causes the dashboard to re-render.&lt;/p&gt;

&lt;p&gt;If both &lt;code&gt;TransactionsTable&lt;/code&gt; and &lt;code&gt;LargeAnalyticsChart&lt;/code&gt; are cheap, stop there.&lt;/p&gt;

&lt;p&gt;There may be nothing worth optimizing.&lt;/p&gt;

&lt;p&gt;But suppose profiling reveals:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;LargeAnalyticsChart render

180ms
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;every time the sidebar changes.&lt;/p&gt;

&lt;p&gt;Now there is a measurable problem.&lt;/p&gt;

&lt;p&gt;If the chart’s props remain unchanged, memoization may be appropriate:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;const LargeAnalyticsChart = memo(
  function LargeAnalyticsChart({ data }) {
    return &amp;lt;Chart data={data} /&amp;gt;;
  }
);
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Perhaps chart data also requires an expensive transformation:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;const chartData = useMemo(() =&amp;gt; {
  return calculateAnalytics(transactions);
}, [transactions]);
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Now the optimization has a reason.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Before

Sidebar toggle
     │
     ▼
Expensive analytics calculation
     │
     ▼
Chart render


After

Sidebar toggle
     │
     ▼
Cached analytics
     │
     ▼
Chart skipped / cheaper work
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;That’s much better than adding memoization everywhere “just in case.”&lt;/p&gt;





&lt;h2 id="rendering-vs-committing"&gt;Rendering vs Committing&lt;/h2&gt;

&lt;p&gt;Another useful distinction is that React may perform rendering work that never reaches the DOM.&lt;/p&gt;

&lt;p&gt;In modern React, rendering work can sometimes be interrupted, restarted, or discarded before it is committed.&lt;/p&gt;

&lt;p&gt;That means:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Render work
    │
    ▼
Does React commit it?
    │
 ┌──┴───┐
 │      │
Yes     No
 │      │
 ▼      ▼
DOM    Discard
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This is one reason render logic must remain pure.&lt;/p&gt;

&lt;p&gt;React may evaluate a component without guaranteeing that the result becomes visible.&lt;/p&gt;

&lt;p&gt;Side effects during rendering would therefore become unpredictable.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;commit phase&lt;/strong&gt; is the point at which actual DOM changes are applied.&lt;/p&gt;





&lt;h2 id="re-renders-and-the-event-loop"&gt;Re-Renders and the Event Loop&lt;/h2&gt;

&lt;p&gt;Everything we’ve discussed ultimately happens as JavaScript work on the main thread.&lt;/p&gt;

&lt;p&gt;Suppose several expensive components render after one state update.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;State Update
     │
     ▼
Component A     30ms
     │
Component B     40ms
     │
Component C     50ms
     │
     ▼
Total JS        120ms
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The Event Loop article taught us what that means.&lt;/p&gt;

&lt;p&gt;While this work runs:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;User click       waiting
Animation        waiting
Rendering        waiting
Other JS         waiting
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The issue isn’t merely that React “rendered too many times.”&lt;/p&gt;

&lt;p&gt;The user-visible problem is that the main thread was occupied for too long.&lt;/p&gt;

&lt;p&gt;That is why frontend performance concepts keep connecting back to one another.&lt;/p&gt;





&lt;h2 id="re-renders-and-the-browser-rendering-pipeline"&gt;Re-Renders and the Browser Rendering Pipeline&lt;/h2&gt;

&lt;p&gt;Now suppose React finishes reconciliation and commits:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;width: 300px
      │
      ▼
width: 600px
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;React’s work is finished.&lt;/p&gt;

&lt;p&gt;But the browser may now need:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Style
  │
  ▼
Layout
  │
  ▼
Paint
  │
  ▼
Composite
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Alternatively, React might commit:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;opacity: 0
     │
     ▼
opacity: 1
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;which may be handled much more cheaply.&lt;/p&gt;

&lt;p&gt;So frontend performance has at least two separate dimensions:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Framework Work

Component rendering
Reconciliation
DOM updates

        +

Browser Work

Style
Layout
Paint
Composite
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Optimizing only one side can leave the actual bottleneck untouched.&lt;/p&gt;





&lt;h2 id="a-better-mental-model"&gt;A Better Mental Model&lt;/h2&gt;

&lt;p&gt;When state changes, don’t imagine:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;State changes
     │
     ▼
Entire page rebuilt
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Think:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;State changes
     │
     ▼
Affected component tree evaluated
     │
     ▼
New UI description
     │
     ▼
React reconciles
     │
     ▼
Actual differences identified
     │
     ▼
Necessary DOM mutations committed
     │
     ▼
Browser processes those changes
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Every arrow represents a potential cost.&lt;/p&gt;

&lt;p&gt;But none should automatically be assumed to be expensive.&lt;/p&gt;





&lt;h2 id="practical-rules-for-re-render-performance"&gt;Practical Rules for Re-Render Performance&lt;/h2&gt;

&lt;p&gt;A few principles go a long way: keep state as close as practical to the components that actually need it, avoid placing rapidly changing local state unnecessarily high in the tree, and avoid repeating expensive calculations on every render when memoization or restructuring can prevent that work.&lt;/p&gt;

&lt;p&gt;Use stable keys for lists, apply memoization when profiling shows meaningful savings, design Context boundaries and global store subscriptions intentionally, virtualize genuinely large lists, and avoid side effects during render.&lt;/p&gt;

&lt;p&gt;Above all, do not optimize purely based on how many renders a console statement reports.&lt;/p&gt;

&lt;p&gt;A render is only a problem when the work involved is actually expensive enough to matter.&lt;/p&gt;





&lt;h2 id="bringing-it-all-together"&gt;Bringing It All Together&lt;/h2&gt;

&lt;p&gt;A state update begins inside your application:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;State
  │
  ▼
Re-render
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;That re-render produces a new description of the UI.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Component Function
       │
       ▼
New Element Tree
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;React compares it with what came before.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Previous Tree
      │
      ├────► Reconciliation
      │
New Tree
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Only necessary changes are committed.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Difference
    │
    ▼
DOM Mutation
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Then the browser takes over.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;DOM
 │
 ▼
Style
 │
 ▼
Layout
 │
 ▼
Paint
 │
 ▼
Composite
 │
 ▼
Pixels
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;That gives us the full chain:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;State Change
      │
      ▼
Component Render
      │
      ▼
Reconciliation
      │
      ▼
Commit
      │
      ▼
DOM Change
      │
      ▼
Browser Rendering
      │
      ▼
Updated Pixels
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Once you understand those boundaries, performance discussions become much clearer.&lt;/p&gt;

&lt;p&gt;You can ask:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Is the component render expensive?

Is reconciliation expensive?

Are we committing too many DOM changes?

Is layout the real problem?

Is painting expensive?

Is the main thread blocked?
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Those are much better questions than simply asking:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Why is React re-rendering?&lt;/p&gt;
&lt;/blockquote&gt;





&lt;h2 id="final-thoughts"&gt;Final Thoughts&lt;/h2&gt;

&lt;p&gt;Re-renders have a reputation they do not entirely deserve: they are not bugs, they are not automatically performance problems, and preventing every possible render is not the goal of frontend engineering.&lt;/p&gt;

&lt;p&gt;Rendering is how declarative UI frameworks work.&lt;/p&gt;

&lt;p&gt;You describe:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;What the UI should look like
for the current state.
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Then the framework figures out how to get there.&lt;/p&gt;

&lt;p&gt;The real performance question is not whether components render, but whether the amount of work happening during those renders prevents the application from delivering a smooth experience.&lt;/p&gt;

&lt;p&gt;Sometimes the answer is yes: a large calculation runs repeatedly, a huge list is rendered unnecessarily, or a global state update wakes up hundreds of components, and those cases deserve optimization.&lt;/p&gt;

&lt;p&gt;But often, rendering is already cheap, and in those cases adding layers of memoization can make the code harder to understand without making the application noticeably faster.&lt;/p&gt;

&lt;p&gt;So the next time you see a component render again, don’t immediately ask:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;“How do I stop this?”&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Ask:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;“What work is this render actually doing, and is that work expensive enough to matter?”&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is the question that leads to better frontend performance decisions.&lt;/p&gt;





&lt;h2 id="whats-next"&gt;What’s Next?&lt;/h2&gt;

&lt;p&gt;We’ve now explored how frontend state changes propagate through a component tree and eventually reach the browser.&lt;/p&gt;

&lt;p&gt;Many of the expensive calculations we discussed share another important characteristic: sometimes the problem is not that the calculation is too slow, but that we are doing the &lt;strong&gt;same calculation repeatedly&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That brings us to another important frontend concept:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;Memoization Explained: When Caching Computation Actually Helps&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;We’ll explore how memoization works, referential equality, memoized components, &lt;code&gt;useMemo&lt;/code&gt;, &lt;code&gt;useCallback&lt;/code&gt;, selectors, cache invalidation, and why adding memoization everywhere can sometimes make an application more complicated without making it any faster.&lt;/p&gt;

&lt;p&gt;Because caching work is useful only when the work was worth avoiding in the first place.&lt;/p&gt;

</description>
      <category>frontend</category>
      <category>performance</category>
    </item>
    <item>
      <title>State Management Explained: Why Frontend State Gets Complicated</title>
      <dc:creator>Billy Okeyo</dc:creator>
      <pubDate>Fri, 04 Sep 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/billy_de_cartel/state-management-explained-why-frontend-state-gets-complicated-a07</link>
      <guid>https://dev.to/billy_de_cartel/state-management-explained-why-frontend-state-gets-complicated-a07</guid>
      <description>&lt;blockquote&gt;
  &lt;p&gt;&lt;em&gt;“State management becomes difficult not because applications have state, but because different kinds of state have different owners, lifetimes, and sources of truth.”&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Imagine you’re building a simple product page.&lt;/p&gt;

&lt;p&gt;At first, there isn’t much to manage.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;const [quantity, setQuantity] = useState(1);
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Easy.&lt;/p&gt;

&lt;p&gt;Then you add a product variant.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;const [selectedColor, setSelectedColor] = useState("black");
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Then a modal.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;const [isModalOpen, setIsModalOpen] = useState(false);
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Then authentication, the shopping cart, filters, pagination, API responses, loading states, and errors. The URL needs to remember the filters, another component needs access to the cart, and when the user navigates away and comes back, some state should remain while other state should disappear.&lt;/p&gt;

&lt;p&gt;Before long, your application looks like this:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Frontend Application
       │
       ├── UI State
       ├── Form State
       ├── Server State
       ├── URL State
       ├── Authentication
       ├── Cached Data
       ├── Global State
       └── Derived State
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;And suddenly someone says:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“We need a state management library.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;But that’s often jumping ahead. Before choosing Redux, Zustand, Pinia, Context, TanStack Query, signals, or anything else, there’s a more important question:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;What kind of state are we actually trying to manage?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Not everything called “state” is the same problem, and once you understand that, frontend state management becomes much easier to reason about.&lt;/p&gt;





&lt;h2 id="what-is-state"&gt;What Is State?&lt;/h2&gt;

&lt;p&gt;At its simplest, state is information that can change over time and affects what your application does or displays.&lt;/p&gt;

&lt;p&gt;Consider a counter:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;const [count, setCount] = useState(0);
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The UI depends on &lt;code&gt;count&lt;/code&gt;.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;lt;button onClick={() =&amp;gt; setCount(count + 1)}&amp;gt;
    Count: {count}
&amp;lt;/button&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;At one moment:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;count = 0
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;After a click:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;count = 1
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The interface changes because the underlying state changed.&lt;/p&gt;

&lt;p&gt;We can think of the UI as a function of state:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;State
  │
  ▼
Render
  │
  ▼
UI
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Change the state:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;New State
    │
    ▼
Re-render
    │
    ▼
New UI
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This state-driven model is fundamental to frameworks such as React, Vue, Svelte, and others. The problem isn’t the idea itself. The problem begins when an application has &lt;strong&gt;many different sources of changing information&lt;/strong&gt;.&lt;/p&gt;





&lt;h2 id="a-small-application-doesnt-need-state-management"&gt;A Small Application Doesn’t Need “State Management”&lt;/h2&gt;

&lt;p&gt;Consider a dropdown.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;function Dropdown() {
    const [open, setOpen] = useState(false);

    return (
        &amp;lt;div&amp;gt;
            &amp;lt;button onClick={() =&amp;gt; setOpen(!open)}&amp;gt;
                Menu
            &amp;lt;/button&amp;gt;

            {open &amp;amp;&amp;amp; (
                &amp;lt;div&amp;gt;
                    Profile
                    Settings
                    Logout
                &amp;lt;/div&amp;gt;
            )}
        &amp;lt;/div&amp;gt;
    );
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The state belongs naturally to the dropdown.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Dropdown
   │
   └── open
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Nobody else needs it.&lt;/p&gt;

&lt;p&gt;When the dropdown disappears, we probably don’t care about preserving that state.&lt;/p&gt;

&lt;p&gt;This is &lt;strong&gt;local component state&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;And local state is often the best kind of state.&lt;/p&gt;

&lt;p&gt;The closer state lives to the code that actually needs it, the easier the application is usually to understand.&lt;/p&gt;

&lt;p&gt;The trouble starts when state begins travelling.&lt;/p&gt;





&lt;h2 id="when-state-needs-to-be-shared"&gt;When State Needs to Be Shared&lt;/h2&gt;

&lt;p&gt;Imagine two components:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Header
  │
  └── CartIcon

ProductPage
  │
  └── AddToCartButton
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;When the user clicks:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Add to Cart
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;the header needs to update:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Cart (0)
   │
   ▼
Cart (1)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Now the cart state cannot live exclusively inside &lt;code&gt;AddToCartButton&lt;/code&gt;. Both components need access to it. A common solution is to move the state upward.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;           App
            │
        cartItems
        /       \
       ▼         ▼
   Header    ProductPage
      │           │
 CartIcon    AddToCart
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This is commonly called &lt;strong&gt;lifting state up&lt;/strong&gt;: move shared state to a common owner and pass the necessary values or update functions downward. React’s documentation recommends this pattern when multiple components need coordinated state. (&lt;a href="https://react.dev/learn/managing-state?utm_source=chatgpt.com" title="Managing State - React" rel="noopener noreferrer"&gt;React&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;That works well.&lt;/p&gt;

&lt;p&gt;Until the component tree gets larger.&lt;/p&gt;





&lt;h2 id="prop-drilling-appears"&gt;Prop Drilling Appears&lt;/h2&gt;

&lt;p&gt;Suppose the structure becomes:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;App
 │
 ├── Header
 │    └── Navigation
 │         └── CartButton
 │
 └── Main
      └── ProductPage
           └── ProductDetails
                └── AddToCartButton
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The cart state lives in &lt;code&gt;App&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;But &lt;code&gt;AddToCartButton&lt;/code&gt; needs it.&lt;/p&gt;

&lt;p&gt;You may end up passing:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;lt;App&amp;gt;
    &amp;lt;Main cart={cart}&amp;gt;
        &amp;lt;ProductPage cart={cart}&amp;gt;
            &amp;lt;ProductDetails cart={cart}&amp;gt;
                &amp;lt;AddToCartButton cart={cart} /&amp;gt;
            &amp;lt;/ProductDetails&amp;gt;
        &amp;lt;/ProductPage&amp;gt;
    &amp;lt;/Main&amp;gt;
&amp;lt;/App&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;code&gt;Main&lt;/code&gt; doesn’t care about the cart, and &lt;code&gt;ProductPage&lt;/code&gt; may not care either. They’re simply transporting information.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;App
 │
 │ cart
 ▼
Main
 │
 │ cart
 ▼
ProductPage
 │
 │ cart
 ▼
ProductDetails
 │
 │ cart
 ▼
AddToCartButton
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This is commonly called &lt;strong&gt;prop drilling&lt;/strong&gt;, and it’s often the moment developers begin looking for global state management. But before we reach for a global store, we need to make another distinction.&lt;/p&gt;





&lt;h2 id="not-all-state-is-application-state"&gt;Not All State Is Application State&lt;/h2&gt;

&lt;p&gt;Consider this dashboard:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;┌─────────────────────────────────────┐
│ Dashboard                           │
│                                     │
│ Search: [ laptop             ]      │
│                                     │
│ Status: Active                      │
│                                     │
│ Customers                           │
│ ┌─────────────────────────────────┐ │
│ │ Alice                           │ │
│ │ Billy                           │ │
│ │ James                           │ │
│ └─────────────────────────────────┘ │
└─────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;What state exists here?&lt;/p&gt;

&lt;p&gt;Potentially:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;searchText
selectedStatus
customers
isLoading
error
currentUser
sidebarOpen
currentPage
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;It’s tempting to put all of that into:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Global Store
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;But those values represent very different things.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;sidebarOpen
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;belongs to the UI.&lt;/p&gt;

&lt;p&gt;While:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;customers
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;probably came from a server.&lt;/p&gt;

&lt;p&gt;And:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;currentPage
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;might belong in the URL.&lt;/p&gt;

&lt;p&gt;Treating them identically creates unnecessary complexity.&lt;/p&gt;

&lt;p&gt;A useful state architecture starts by classifying state.&lt;/p&gt;





&lt;h2 id="1-local-ui-state"&gt;1. Local UI State&lt;/h2&gt;

&lt;p&gt;Local UI state represents temporary interaction details.&lt;/p&gt;

&lt;p&gt;Examples include:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Modal open?
Dropdown expanded?
Selected tab?
Hovered item?
Accordion expanded?
Tooltip visible?
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;const [isOpen, setIsOpen] = useState(false);
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This state is usually:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Temporary&lt;/li&gt;
  &lt;li&gt;Owned by one component or a small subtree&lt;/li&gt;
  &lt;li&gt;Not important outside that UI&lt;/li&gt;
  &lt;li&gt;Safe to discard when the component disappears&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It should usually stay local.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Modal
  │
  └── isOpen
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Moving every boolean into a global store:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;store.modalOpen
store.dropdownOpen
store.tooltipVisible
store.settingsTab
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;can turn a simple application into a state-management bureaucracy.&lt;/p&gt;





&lt;h2 id="2-shared-client-state"&gt;2. Shared Client State&lt;/h2&gt;

&lt;p&gt;Some state genuinely needs to be shared across different parts of the application.&lt;/p&gt;

&lt;p&gt;Examples might include:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Shopping cart
Application theme
Feature preferences
Complex editor state
Currently selected workspace
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Imagine:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;             Application
                  │
              Cart State
            /     |      \
           ▼      ▼       ▼
        Header Product Checkout
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This is where mechanisms such as:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Context
Redux
Zustand
Pinia
Signals
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;can become useful depending on the framework and complexity.&lt;/p&gt;

&lt;p&gt;The important point is:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;Global state should be global because its ownership is global, not because passing props became mildly inconvenient.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;





&lt;h2 id="3-server-state"&gt;3. Server State&lt;/h2&gt;

&lt;p&gt;Now consider:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;const [products, setProducts] = useState([]);
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;At first glance, this looks like ordinary state.&lt;/p&gt;

&lt;p&gt;But where did those products come from?&lt;/p&gt;

&lt;p&gt;Probably:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Database
   │
   ▼
Backend
   │
   ▼
API
   │
   ▼
Frontend
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The frontend does not actually own this data. The server does, and the browser has a &lt;strong&gt;local copy&lt;/strong&gt;. That creates completely different problems.&lt;/p&gt;

&lt;p&gt;Suppose you fetch:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;GET /api/products
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;and receive:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;[
    {
        "id": 1,
        "name": "Keyboard",
        "stock": 12
    }
]
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Five seconds later, another customer buys two keyboards.&lt;/p&gt;

&lt;p&gt;The server now has:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;stock = 10
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Your frontend still has:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;stock = 12
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Now your state is &lt;strong&gt;stale&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That’s not usually a problem with a modal boolean.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;isModalOpen = true
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;doesn’t suddenly become outdated because another computer changed something.&lt;/p&gt;

&lt;p&gt;Server state does.&lt;/p&gt;





&lt;h2 id="server-state-has-different-problems"&gt;Server State Has Different Problems&lt;/h2&gt;

&lt;p&gt;Managing server state means thinking about:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Fetching
Caching
Staleness
Retries
Refetching
Deduplication
Pagination
Mutations
Optimistic updates
Synchronization
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Consider:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;useEffect(() =&amp;gt; {
    fetch("/api/products")
        .then(response =&amp;gt; response.json())
        .then(setProducts);
}, []);
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Looks simple, until requirements arrive. We need loading state.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;const [loading, setLoading] = useState(true);
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Then errors.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;const [error, setError] = useState(null);
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Then retries, caching, refetching when the user returns to the tab, avoiding duplicate requests, and invalidating products after an update.&lt;/p&gt;

&lt;p&gt;What looked like:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;products
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;was actually a synchronization problem between:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Server Truth
     │
     ▼
Client Cache
     │
     ▼
UI
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;That’s why tools such as &lt;strong&gt;TanStack Query&lt;/strong&gt;, SWR, Apollo Client, and framework-native data layers exist. They aren’t simply storing variables. They’re managing &lt;strong&gt;remote data lifecycles&lt;/strong&gt;.&lt;/p&gt;





&lt;h2 id="server-state-shouldnt-automatically-live-in-your-global-store"&gt;Server State Shouldn’t Automatically Live in Your Global Store&lt;/h2&gt;

&lt;p&gt;A common architecture used to look like:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;API
 │
 ▼
Redux
 │
 ▼
Components
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Every API response was copied into the application’s global state.&lt;/p&gt;

&lt;p&gt;For some applications, that can still be appropriate.&lt;/p&gt;

&lt;p&gt;But often you’re making your client store responsible for problems that a server-state cache is specifically designed to solve.&lt;/p&gt;

&lt;p&gt;A modern architecture may instead look like:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;                Frontend
                   │
        ┌──────────┴──────────┐
        │                     │
        ▼                     ▼
 Client State            Server State
        │                     │
    Zustand /             Query Cache
     Redux /                  │
    Context                   ▼
                         Backend API
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Different problems.&lt;/p&gt;

&lt;p&gt;Different tools.&lt;/p&gt;





&lt;h2 id="4-url-state"&gt;4. URL State&lt;/h2&gt;

&lt;p&gt;Suppose your product page has filters:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Category: Laptops
Price: 20,000 - 100,000
Sort: Price Low to High
Page: 3
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;You could store them as:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;const [category, setCategory] = useState("laptops");
const [sort, setSort] = useState("price");
const [page, setPage] = useState(3);
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;But what happens when the user refreshes? The state disappears. What happens when they copy the URL and send it to someone? The other person sees the default filters. What happens when they press the browser’s Back button? Potentially, nothing useful. Maybe this state belongs somewhere else: the URL.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;/products?category=laptops&amp;amp;sort=price&amp;amp;page=3
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Now:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;URL
 │
 ├── category=laptops
 ├── sort=price
 └── page=3
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;becomes the source of truth.&lt;/p&gt;

&lt;p&gt;This gives us useful behavior almost automatically:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Refresh        ✓
Share link     ✓
Bookmark       ✓
Back button    ✓
Forward button ✓
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This is why the URL itself should be thought of as a state container.&lt;/p&gt;





&lt;h2 id="dont-duplicate-url-state"&gt;Don’t Duplicate URL State&lt;/h2&gt;

&lt;p&gt;A common mistake is:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;URL
 │
 └── page=3

AND

React State
 │
 └── page=3
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Now you have two sources of truth.&lt;/p&gt;

&lt;p&gt;What happens if:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;URL page = 3

but

React page = 2
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Which one wins?&lt;/p&gt;

&lt;p&gt;You’ve created a synchronization problem that didn’t need to exist.&lt;/p&gt;

&lt;p&gt;A better model is:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;URL
 │
 ▼
page
 │
 ▼
UI
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;If the URL owns the value, read it from there.&lt;/p&gt;





&lt;h2 id="5-form-state"&gt;5. Form State&lt;/h2&gt;

&lt;p&gt;Forms deserve their own category because they can become surprisingly complicated.&lt;/p&gt;

&lt;p&gt;A simple form:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;const [email, setEmail] = useState("");
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;is easy.&lt;/p&gt;

&lt;p&gt;Now add:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Name
Email
Phone
Country
Address
Password
Confirm Password
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Then:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Validation
Touched fields
Dirty fields
Submitting
Server errors
Conditional fields
Resetting
Default values
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Suddenly, “email” isn’t the only state.&lt;/p&gt;

&lt;p&gt;You have:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;value
valid?
touched?
dirty?
error?
submitting?
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;for potentially dozens of fields.&lt;/p&gt;

&lt;p&gt;That’s why libraries such as React Hook Form, Formik, and framework-specific form systems exist.&lt;/p&gt;

&lt;p&gt;Again, the lesson isn’t:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Use a library for every form.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It’s:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;Recognize that form state has a particular lifecycle and shouldn’t automatically become global application state.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;





&lt;h2 id="6-derived-state"&gt;6. Derived State&lt;/h2&gt;

&lt;p&gt;Derived state is one of the most common sources of unnecessary complexity.&lt;/p&gt;

&lt;p&gt;Imagine:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;const [firstName, setFirstName] = useState("Billy");
const [lastName, setLastName] = useState("Okeyo");
const [fullName, setFullName] = useState("Billy Okeyo");
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;We now have three pieces of state.&lt;/p&gt;

&lt;p&gt;But do we really?&lt;/p&gt;

&lt;p&gt;&lt;code&gt;fullName&lt;/code&gt; can be calculated:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;const fullName = `${firstName} ${lastName}`;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;So our actual state is:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;firstName
lastName
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;and:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;fullName
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;is derived.&lt;/p&gt;

&lt;p&gt;React’s own guidance recommends avoiding redundant state when a value can be calculated from existing props or state during rendering. (&lt;a href="https://react.dev/learn/choosing-the-state-structure?utm_source=chatgpt.com" title="Choosing the State Structure - React" rel="noopener noreferrer"&gt;React&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;Why?&lt;/p&gt;

&lt;p&gt;Because duplication creates synchronization problems.&lt;/p&gt;





&lt;h2 id="the-synchronization-trap"&gt;The Synchronization Trap&lt;/h2&gt;

&lt;p&gt;Suppose we store all three:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;const [firstName, setFirstName] = useState("Billy");
const [lastName, setLastName] = useState("Okeyo");
const [fullName, setFullName] = useState("Billy Okeyo");
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Now someone writes:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;setFirstName("John");
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;but forgets:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;setFullName("John Okeyo");
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;We get:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;firstName = John
lastName  = Okeyo
fullName  = Billy Okeyo
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Your application contradicts itself.&lt;/p&gt;

&lt;p&gt;The problem isn’t React.&lt;/p&gt;

&lt;p&gt;The problem is that we stored information that could have been calculated.&lt;/p&gt;

&lt;p&gt;A good rule is:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;Store the minimum information required to describe the application. Derive everything else.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;React describes this as finding the minimal but complete representation of UI state. (&lt;a href="https://react.dev/learn/thinking-in-react?utm_source=chatgpt.com" title="Thinking in React - React" rel="noopener noreferrer"&gt;React&lt;/a&gt;)&lt;/p&gt;





&lt;h2 id="another-derived-state-example"&gt;Another Derived State Example&lt;/h2&gt;

&lt;p&gt;Suppose:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;const [products, setProducts] = useState([...]);
const [search, setSearch] = useState("");
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Should we also store:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;const [filteredProducts, setFilteredProducts] = useState([]);
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Probably not.&lt;/p&gt;

&lt;p&gt;We can derive it:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;const filteredProducts = products.filter(product =&amp;gt;
    product.name
        .toLowerCase()
        .includes(search.toLowerCase())
);
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Our model becomes:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;products ──────┐
               │
               ▼
             Filter ───► filteredProducts
               ▲
               │
search ─────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;code&gt;filteredProducts&lt;/code&gt; is an output.&lt;/p&gt;

&lt;p&gt;Not necessarily state.&lt;/p&gt;





&lt;h2 id="state-gets-complicated-when-we-duplicate-truth"&gt;State Gets Complicated When We Duplicate Truth&lt;/h2&gt;

&lt;p&gt;This is one of the biggest themes in frontend state management.&lt;/p&gt;

&lt;p&gt;Imagine:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;API Response
    │
    ├── Redux Store
    │
    ├── Component State
    │
    ├── Form State
    │
    └── localStorage
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Now the same information exists in four places.&lt;/p&gt;

&lt;p&gt;Every update creates a question:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Which one is correct?
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Then:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Which one should update first?
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Then:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;What if one update fails?
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Then:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;What happens after refresh?
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;A large percentage of state-management complexity is actually &lt;strong&gt;state synchronization complexity&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The fewer copies of truth you maintain, the fewer things you need to synchronize.&lt;/p&gt;





&lt;h2 id="impossible-states"&gt;Impossible States&lt;/h2&gt;

&lt;p&gt;Poorly structured state can also represent combinations that should never exist.&lt;/p&gt;

&lt;p&gt;Consider:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;const [isLoading, setIsLoading] = useState(false);
const [isSuccess, setIsSuccess] = useState(false);
const [isError, setIsError] = useState(false);
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Nothing technically prevents:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;isLoading = true
isSuccess = true
isError   = true
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;But what does that mean?&lt;/p&gt;

&lt;p&gt;We’re loading successfully while failing?&lt;/p&gt;

&lt;p&gt;Instead, maybe the application actually has one state:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;const [status, setStatus] = useState("idle");
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;with possible values:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;idle
loading
success
error
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Now:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;status = loading
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;cannot simultaneously be:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;status = success
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The data structure itself prevents invalid combinations. React’s state-structure guidance specifically recommends avoiding contradictory state for this reason. (&lt;a href="https://react.dev/learn/choosing-the-state-structure?utm_source=chatgpt.com" title="Choosing the State Structure - React" rel="noopener noreferrer"&gt;React&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;This idea becomes extremely powerful in complex UIs.&lt;/p&gt;





&lt;h2 id="think-in-state-machines"&gt;Think in State Machines&lt;/h2&gt;

&lt;p&gt;Consider a payment flow.&lt;/p&gt;

&lt;p&gt;You might start with:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;const [loading, setLoading] = useState(false);
const [paid, setPaid] = useState(false);
const [failed, setFailed] = useState(false);
const [cancelled, setCancelled] = useState(false);
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;But conceptually, the payment probably behaves more like:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;            ┌───────────┐
            │   IDLE    │
            └─────┬─────┘
                  │ Pay
                  ▼
            ┌───────────┐
            │PROCESSING │
            └─────┬─────┘
                  │
          ┌───────┼────────┐
          ▼       ▼        ▼
      SUCCESS   FAILED   CANCELLED
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Representing it as:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;const [status, setStatus] = useState("idle");
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;makes the model much closer to reality.&lt;/p&gt;

&lt;p&gt;Good state management often starts with good &lt;strong&gt;data modelling&lt;/strong&gt;, not better libraries.&lt;/p&gt;





&lt;h2 id="state-ownership-matters"&gt;State Ownership Matters&lt;/h2&gt;

&lt;p&gt;Imagine three components:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;ProductPage
 ├── ProductGallery
 ├── ProductDetails
 └── AddToCart
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Only &lt;code&gt;ProductGallery&lt;/code&gt; needs:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;currentImage
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Where should it live?&lt;/p&gt;

&lt;p&gt;Probably:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;ProductGallery
      │
      └── currentImage
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Not:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Global Store
      │
      └── currentImage
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Now suppose both &lt;code&gt;ProductDetails&lt;/code&gt; and &lt;code&gt;AddToCart&lt;/code&gt; need:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;selectedVariant
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Then perhaps:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;ProductPage
      │
      └── selectedVariant
             │
        ┌────┴─────┐
        ▼          ▼
ProductDetails  AddToCart
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;State should generally live at the &lt;strong&gt;lowest level that owns all consumers that need it&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Move it upward when necessary.&lt;/p&gt;

&lt;p&gt;Not before.&lt;/p&gt;





&lt;h2 id="state-lifetime-matters-too"&gt;State Lifetime Matters Too&lt;/h2&gt;

&lt;p&gt;Different state should survive for different amounts of time.&lt;/p&gt;

&lt;p&gt;Consider:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Tooltip open
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Lifetime:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Seconds
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Shopping cart:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Minutes / Days
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Authentication session:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Hours / Days
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;URL filter:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;As long as URL exists
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Server cache:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Until stale / invalidated
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;User preferences:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Months
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Putting all of these into one store ignores their fundamentally different lifecycles.&lt;/p&gt;

&lt;p&gt;A useful way to think about state is:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;State
 │
 ├── Who owns it?
 │
 ├── Who needs it?
 │
 ├── How long should it live?
 │
 ├── Where is the source of truth?
 │
 └── What causes it to change?
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Answer those questions first.&lt;/p&gt;

&lt;p&gt;Then choose the storage mechanism.&lt;/p&gt;





&lt;h2 id="persistence-is-not-the-same-as-state-management"&gt;Persistence Is Not the Same as State Management&lt;/h2&gt;

&lt;p&gt;Suppose we want the theme to survive refreshes.&lt;/p&gt;

&lt;p&gt;We might use:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;localStorage.setItem("theme", "dark");
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Now some developers immediately put all application state into &lt;code&gt;localStorage&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;But persistence creates another source of truth.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Application State
       │
       ▼
localStorage
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;What happens when the application’s expected data structure changes? What happens when the stored value is invalid? What happens when the user logs out? What happens when data becomes stale? Persistence should be intentional. Not every state deserves immortality.&lt;/p&gt;





&lt;h2 id="context-is-not-automatically-a-state-manager"&gt;Context Is Not Automatically a State Manager&lt;/h2&gt;

&lt;p&gt;In React, Context is often introduced when prop drilling becomes uncomfortable.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;lt;CartContext.Provider value={cart}&amp;gt;
    &amp;lt;App /&amp;gt;
&amp;lt;/CartContext.Provider&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Then:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;const cart = useContext(CartContext);
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This solves a &lt;strong&gt;distribution problem&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Instead of:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;App
 │ props
 ▼
A
 │ props
 ▼
B
 │ props
 ▼
C
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;we get:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;      Context
      /  |  \
     ▼   ▼   ▼
     A   B   C
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;That’s useful.&lt;/p&gt;

&lt;p&gt;But Context doesn’t automatically solve:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Caching
Server synchronization
Persistence
Complex update logic
Optimistic updates
Normalized entities
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;It’s a mechanism for making values available through a component tree. Sometimes that’s all you need. Sometimes it isn’t.&lt;/p&gt;





&lt;h2 id="why-global-stores-exist"&gt;Why Global Stores Exist&lt;/h2&gt;

&lt;p&gt;As applications grow, some client state can become genuinely complicated.&lt;/p&gt;

&lt;p&gt;Imagine an image editor.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Canvas
 │
 ├── Selected objects
 ├── Layers
 ├── History
 ├── Zoom
 ├── Tools
 ├── Clipboard
 └── Document state
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Many distant components may need to read and modify the same information.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;              Editor State
          /       |       \
         ▼        ▼        ▼
      Toolbar   Canvas   Layers
         │        │        │
         ▼        ▼        ▼
      Buttons   Objects   Panel
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;A centralized or shared store can provide:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;One ownership model
Predictable updates
Subscriptions
Selectors
Debugging tools
Middleware
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This is where Redux, Zustand, Pinia and similar solutions can shine. The problem isn’t global state. The problem is &lt;strong&gt;unnecessarily global state&lt;/strong&gt;.&lt;/p&gt;





&lt;h2 id="redux-isnt-the-answer-to-every-state-problem"&gt;Redux Isn’t the Answer to Every State Problem&lt;/h2&gt;

&lt;p&gt;Suppose your application has:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;const [modalOpen, setModalOpen] = useState(false);
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Moving it into Redux:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;dispatch(openModal());
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;doesn’t automatically improve the architecture.&lt;/p&gt;

&lt;p&gt;Likewise, storing an API response in Redux doesn’t automatically solve server-state caching.&lt;/p&gt;

&lt;p&gt;And storing filters in Redux may be worse than putting them in the URL.&lt;/p&gt;

&lt;p&gt;Instead of asking:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Which state library should we use?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Ask:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;Where should this specific piece of state live?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Then the answer may be:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Component
URL
Server cache
Form manager
Context
Global store
Browser storage
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The tool follows the ownership model.&lt;/p&gt;





&lt;h2 id="a-better-state-architecture"&gt;A Better State Architecture&lt;/h2&gt;

&lt;p&gt;Imagine an e-commerce application.&lt;/p&gt;

&lt;p&gt;Instead of:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;EVERYTHING
    │
    ▼
Redux
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;we might have:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;                    Application
                         │
       ┌─────────────────┼─────────────────┐
       │                 │                 │
       ▼                 ▼                 ▼
   UI State          URL State        Server State
       │                 │                 │
    useState           Router          Query Cache
       │                                   │
       ▼                                   ▼
 Components                              API

                         │
                         ▼
                  Shared Client State
                         │
                         ▼
                    Global Store
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Now each type of state uses the mechanism suited to its job.&lt;/p&gt;

&lt;p&gt;This can actually make an application simpler even though we’re using several different tools.&lt;/p&gt;

&lt;p&gt;Because each tool has a clear responsibility.&lt;/p&gt;





&lt;h2 id="example-building-a-product-search-page"&gt;Example: Building a Product Search Page&lt;/h2&gt;

&lt;p&gt;Let’s put this into practice.&lt;/p&gt;

&lt;p&gt;Suppose we’re building:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;/products
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The page contains:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Search
Category filter
Sort order
Products
Cart
Filter drawer
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Where should everything live?&lt;/p&gt;

&lt;h4 id="filter-drawer"&gt;Filter Drawer&lt;/h4&gt;

&lt;pre&gt;&lt;code&gt;isFilterDrawerOpen
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This is local UI state.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;const [isFilterDrawerOpen, setFilterDrawerOpen] =
    useState(false);
&lt;/code&gt;&lt;/pre&gt;





&lt;h4 id="search-and-filters"&gt;Search and Filters&lt;/h4&gt;

&lt;p&gt;These affect what page the user is viewing and should be shareable.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;/products?q=keyboard&amp;amp;category=accessories&amp;amp;sort=price
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;So:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;URL State
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;is a strong choice.&lt;/p&gt;





&lt;h4 id="products"&gt;Products&lt;/h4&gt;

&lt;p&gt;Products come from:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Backend API
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;So they’re:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Server State
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;A query cache could manage them.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;useQuery({
    queryKey: ["products", search, category, sort],
    queryFn: fetchProducts
});
&lt;/code&gt;&lt;/pre&gt;





&lt;h4 id="shopping-cart"&gt;Shopping Cart&lt;/h4&gt;

&lt;p&gt;The cart may be needed by:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Header
Product Page
Checkout
Sidebar
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This could be:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Shared Client State
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;or server state depending on how the cart is persisted and synchronized.&lt;/p&gt;

&lt;p&gt;That’s an architectural decision.&lt;/p&gt;





&lt;h4 id="number-of-products"&gt;Number of Products&lt;/h4&gt;

&lt;p&gt;Suppose we already have:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;products
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Don’t necessarily store:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;productCount
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Just derive:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;const productCount = products.length;
&lt;/code&gt;&lt;/pre&gt;





&lt;p&gt;Now our architecture is much clearer:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Product Search Page
       │
       ├── Filter drawer ──► Local State
       │
       ├── Search ─────────► URL
       │
       ├── Category ───────► URL
       │
       ├── Sort ───────────► URL
       │
       ├── Products ───────► Server Cache
       │
       ├── Cart ───────────► Shared State
       │
       └── Product Count ──► Derived
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;That’s state management.&lt;/p&gt;

&lt;p&gt;Notice that we haven’t needed one giant store.&lt;/p&gt;





&lt;h2 id="why-state-libraries-sometimes-make-things-worse"&gt;Why State Libraries Sometimes Make Things Worse&lt;/h2&gt;

&lt;p&gt;Suppose a team decides:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“Everything goes in Redux.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Now adding a modal requires:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Action
Reducer
Selector
Dispatch
Store
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;even though:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;useState(false)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;would have solved the problem.&lt;/p&gt;

&lt;p&gt;Or the team puts every API response into the store and manually implements:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Loading
Caching
Retries
Invalidation
Staleness
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;They end up rebuilding a server-state library.&lt;/p&gt;

&lt;p&gt;Or they store pagination in global state and then write custom logic to synchronize it with the URL.&lt;/p&gt;

&lt;p&gt;They’ve now created:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;URL
  ↕
Redux
  ↕
Components
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;when:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;URL
  ↓
Components
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;would have been simpler.&lt;/p&gt;

&lt;p&gt;More powerful tools don’t automatically create simpler architecture.&lt;/p&gt;





&lt;h2 id="why-state-libraries-sometimes-make-things-much-better"&gt;Why State Libraries Sometimes Make Things Much Better&lt;/h2&gt;

&lt;p&gt;The opposite mistake is pretending every application can survive indefinitely on:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;useState + props
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Imagine a complex trading interface.&lt;/p&gt;

&lt;p&gt;Dozens of components need access to:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Selected instrument
Open positions
Workspace layout
Active panels
Watchlists
Notifications
User preferences
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Trying to thread everything through component hierarchies can become difficult.&lt;/p&gt;

&lt;p&gt;A well-designed store can create:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;                 Store
        ┌──────────┼──────────┐
        ▼          ▼          ▼
     Chart      Orders     Watchlist
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Components subscribe only to the state they need.&lt;/p&gt;

&lt;p&gt;The state transitions can be centralized and inspected.&lt;/p&gt;

&lt;p&gt;The lesson isn’t:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Global stores are bad.
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;It’s:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Use global stores for genuinely shared
client-side state.
&lt;/code&gt;&lt;/pre&gt;





&lt;h2 id="state-and-re-renders"&gt;State and Re-Renders&lt;/h2&gt;

&lt;p&gt;State architecture also affects rendering performance.&lt;/p&gt;

&lt;p&gt;Suppose a global context contains:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;{
    user,
    cart,
    theme,
    notifications,
    sidebar,
    preferences
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;A change to:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;sidebar
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;may cause consumers of that context to re-evaluate even if they only care about:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;user
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;depending on how the architecture is implemented.&lt;/p&gt;

&lt;p&gt;This is one reason state libraries often provide selectors.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Global Store
    │
    ├── Component A subscribes to cart
    │
    ├── Component B subscribes to user
    │
    └── Component C subscribes to theme
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;When:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;cart changes
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;we ideally want to notify only consumers that care about the cart. State management isn’t only about storing information. It’s also about efficiently distributing changes.&lt;/p&gt;





&lt;h2 id="state-has-identity"&gt;State Has Identity&lt;/h2&gt;

&lt;p&gt;There’s another subtle issue.&lt;/p&gt;

&lt;p&gt;Consider:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;const user = {
    name: "Billy"
};
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Then:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;const anotherUser = {
    name: "Billy"
};
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Their contents look identical.&lt;/p&gt;

&lt;p&gt;But:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;user === anotherUser
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;is:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;false
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;because they’re different objects.&lt;/p&gt;

&lt;p&gt;This matters in reactive systems because object identity can influence:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Change detection
Memoization
Dependencies
Selectors
Re-renders
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Carelessly recreating objects can therefore produce unnecessary updates. State management isn’t only about &lt;strong&gt;what values exist&lt;/strong&gt;. Sometimes it’s also about &lt;strong&gt;whether the system considers those values changed&lt;/strong&gt;.&lt;/p&gt;





&lt;h2 id="immutable-updates"&gt;Immutable Updates&lt;/h2&gt;

&lt;p&gt;React developers often encounter patterns such as:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;setUser({
    ...user,
    name: "John"
});
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;instead of:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;user.name = "John";
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Why?&lt;/p&gt;

&lt;p&gt;Because frameworks need reliable ways to understand that state changed.&lt;/p&gt;

&lt;p&gt;With immutable-style updates:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Old State
   │
   ▼
Create New State
   │
   ▼
New Reference
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;the transition is easier to reason about.&lt;/p&gt;

&lt;p&gt;For arrays:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;setProducts(
    products.map(product =&amp;gt;
        product.id === updated.id
            ? updated
            : product
    )
);
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;rather than mutating the existing array.&lt;/p&gt;

&lt;p&gt;Different frameworks have different reactivity models, but the broader principle remains:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;Understand how your framework detects change.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;





&lt;h2 id="deeply-nested-state-gets-painful"&gt;Deeply Nested State Gets Painful&lt;/h2&gt;

&lt;p&gt;Consider:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;const state = {
    user: {
        profile: {
            address: {
                city: "Nairobi"
            }
        }
    }
};
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Updating the city immutably can become awkward:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;setState({
    ...state,
    user: {
        ...state.user,
        profile: {
            ...state.user.profile,
            address: {
                ...state.user.profile.address,
                city: "Mombasa"
            }
        }
    }
});
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The problem is partly the update syntax.&lt;/p&gt;

&lt;p&gt;But it’s also the state shape.&lt;/p&gt;

&lt;p&gt;React’s guidance suggests flattening deeply nested state where practical because it makes updates easier and helps reduce duplication. (&lt;a href="https://react.dev/learn/choosing-the-state-structure?utm_source=chatgpt.com" title="Choosing the State Structure - React" rel="noopener noreferrer"&gt;React&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;Instead of thinking only:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;How do I update this deeply nested object?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;sometimes ask:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;Should my state be shaped like this in the first place?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;





&lt;h2 id="state-should-have-a-source-of-truth"&gt;State Should Have a Source of Truth&lt;/h2&gt;

&lt;p&gt;Suppose a user’s name exists in:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Server
Redux
Header component
Profile form
localStorage
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;That’s five versions of:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Billy
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Now the profile is changed.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Billy
  │
  ▼
William
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Which copies update?&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Server          William
Redux           William
Header          Billy
Profile Form    William
localStorage    Billy
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Now the UI disagrees with itself.&lt;/p&gt;

&lt;p&gt;A better architecture clearly defines:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Source of Truth
      │
      ▼
Other representations
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Server
  │
  ▼
Query Cache
  │
  ├── Header
  ├── Profile
  └── Settings
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;One authoritative source.&lt;/p&gt;

&lt;p&gt;Multiple consumers.&lt;/p&gt;





&lt;h2 id="the-state-management-decision-tree"&gt;The State Management Decision Tree&lt;/h2&gt;

&lt;p&gt;Before adding state, ask:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can this value be calculated from existing information?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If yes:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Derive it.
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;If no:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does only one component need it?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If yes:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Keep it local.
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;If multiple nearby components need it:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Lift it to their common owner.
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;If it represents navigation or shareable page configuration:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Consider the URL.
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;If it came from a backend:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Treat it as server state.
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;If it’s complex form interaction:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Treat it as form state.
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;If it’s genuinely shared client information:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Consider Context or a store.
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;If it must survive sessions:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Consider persistence deliberately.
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The process looks like:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;                   New State
                       │
                       ▼
              Can it be derived?
                 /           \
               Yes            No
                │              │
             Derive       Who owns it?
                              │
              ┌───────────────┼───────────────┐
              ▼               ▼               ▼
          Component          URL            Server
              │                               │
              ▼                               ▼
         Local State                     Query Cache

                 Shared broadly?
                       │
                       ▼
                  Global Store
&lt;/code&gt;&lt;/pre&gt;





&lt;h2 id="the-best-state-is-often-no-state"&gt;The Best State Is Often No State&lt;/h2&gt;

&lt;p&gt;This sounds strange in an article about state management.&lt;/p&gt;

&lt;p&gt;But one of the strongest state-management techniques is simply &lt;strong&gt;removing unnecessary state&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Instead of:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;const [items, setItems] = useState([]);
const [itemCount, setItemCount] = useState(0);
const [hasItems, setHasItems] = useState(false);
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;store:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;const [items, setItems] = useState([]);
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;and derive:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;const itemCount = items.length;
const hasItems = items.length &amp;gt; 0;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;We’ve gone from:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;3 synchronized variables
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;to:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;1 source of truth
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;React’s documentation emphasizes avoiding redundant and duplicate state precisely because fewer independent pieces of state are easier to keep consistent. (&lt;a href="https://react.dev/learn/choosing-the-state-structure?utm_source=chatgpt.com" title="Choosing the State Structure - React" rel="noopener noreferrer"&gt;React&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;That’s a lesson that applies far beyond React.&lt;/p&gt;





&lt;h2 id="bringing-it-all-together"&gt;Bringing It All Together&lt;/h2&gt;

&lt;p&gt;Frontend state becomes complicated because we’re often using the word &lt;strong&gt;state&lt;/strong&gt; to describe many different problems.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;                     STATE
                       │
       ┌───────────────┼────────────────┐
       │               │                │
       ▼               ▼                ▼
   UI State        Server State      URL State
       │               │                │
       ▼               ▼                ▼
 Component          Query Cache        Router

       ┌───────────────┼────────────────┐
       │               │                │
       ▼               ▼                ▼
  Form State      Shared State     Derived Data
       │               │                │
       ▼               ▼                ▼
 Form Manager      Global Store      Calculate
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Trying to force all of these into one state-management solution creates complexity.&lt;/p&gt;

&lt;p&gt;The better approach is to understand the ownership and lifecycle of each value.&lt;/p&gt;

&lt;p&gt;Ask:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Where did this value come from?

Who owns it?

Who needs it?

How long should it live?

Can it be derived?

Does it need to survive navigation?

Does it belong in the URL?

Is the server actually the source of truth?
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Once those questions are answered, the technology choice becomes much easier.&lt;/p&gt;





&lt;h2 id="final-thoughts"&gt;Final Thoughts&lt;/h2&gt;

&lt;p&gt;State management is often presented as a library problem. React developers debate Redux versus Zustand, Vue developers discuss Pinia, and other ecosystems introduce signals, stores, atoms, observables, and new reactive primitives. Those tools matter, but they come &lt;strong&gt;after&lt;/strong&gt; the more important architectural decision.&lt;/p&gt;

&lt;p&gt;You first need to model your state correctly. A badly designed state model doesn’t become good because you put it into Redux. Duplicated state doesn’t stop being duplicated because Zustand stores it. Server state doesn’t stop becoming stale because Context distributes it. And a filter that belongs in the URL doesn’t suddenly belong somewhere else because your state library has persistence.&lt;/p&gt;

&lt;p&gt;Good frontend state management is mostly about maintaining clear ownership and minimizing sources of truth.&lt;/p&gt;

&lt;p&gt;So instead of beginning with:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;“Which state management library should we use?”&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;begin with:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;“What kind of state is this, and where does it naturally belong?”&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That question solves far more problems.&lt;/p&gt;





&lt;h2 id="whats-next"&gt;What’s Next?&lt;/h2&gt;

&lt;p&gt;So far in &lt;strong&gt;Beyond the UI&lt;/strong&gt;, we’ve moved through several layers of the frontend:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Browser Rendering Pipeline
          │
          ▼
Reflow and Repaint
          │
          ▼
JavaScript Event Loop
          │
          ▼
CSR vs SSR
          │
          ▼
Hydration
          │
          ▼
State Management
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;We’ve now seen how the browser renders an application, how JavaScript gets scheduled, how applications can be rendered on the server, how hydration makes them interactive, and how their changing data can be organized.&lt;/p&gt;

&lt;p&gt;But there’s another performance problem hiding inside modern frontend applications. You update one small piece of state:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;setCount(count + 1);
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;and suddenly multiple components execute again. Why? What does “re-render” actually mean? Does re-rendering mean the DOM was rebuilt? When does React actually touch the browser DOM? And when should you use tools such as &lt;code&gt;memo&lt;/code&gt;, &lt;code&gt;useMemo&lt;/code&gt;, and &lt;code&gt;useCallback&lt;/code&gt;?&lt;/p&gt;

&lt;p&gt;That’s where we’ll go next:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;Re-Renders Explained: What Actually Happens When Frontend State Changes&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Because before trying to prevent re-renders, you need to understand what a re-render actually costs.&lt;/p&gt;

</description>
      <category>frontend</category>
      <category>performance</category>
    </item>
    <item>
      <title>Hydration Explained: How Server-Rendered Pages Become Interactive</title>
      <dc:creator>Billy Okeyo</dc:creator>
      <pubDate>Mon, 31 Aug 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/billy_de_cartel/hydration-explained-how-server-rendered-pages-become-interactive-1551</link>
      <guid>https://dev.to/billy_de_cartel/hydration-explained-how-server-rendered-pages-become-interactive-1551</guid>
      <description>&lt;blockquote&gt;
  &lt;p&gt;&lt;em&gt;“Server-side rendering can make a page visible before JavaScript arrives. Hydration is what turns that visible HTML into an interactive application.”&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In the previous article in &lt;strong&gt;Beyond the UI&lt;/strong&gt;, we compared Client-Side Rendering and Server-Side Rendering.&lt;/p&gt;

&lt;p&gt;With Client-Side Rendering, the browser might initially receive something as small as:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;lt;div id="root"&amp;gt;&amp;lt;/div&amp;gt;
&amp;lt;script src="https://billyokeyo.dev/app.js"&amp;gt;&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;JavaScript then constructs the interface in the browser.&lt;/p&gt;

&lt;p&gt;With Server-Side Rendering, the server can send meaningful HTML immediately:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;lt;article class="product"&amp;gt;
    &amp;lt;h1&amp;gt;Mechanical Keyboard&amp;lt;/h1&amp;gt;
    &amp;lt;p&amp;gt;KES 12,000&amp;lt;/p&amp;gt;

    &amp;lt;button&amp;gt;
        Add to Cart
    &amp;lt;/button&amp;gt;
&amp;lt;/article&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The browser can parse and display that HTML without waiting for React, Vue, or another framework to construct it.&lt;/p&gt;

&lt;p&gt;That sounds like the problem is solved. The user can see the product, the button is visible, and the page looks complete. But then the user clicks:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Add to Cart
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;and nothing happens. Why?&lt;/p&gt;

&lt;p&gt;Because HTML can describe the button, but the server-rendered document doesn’t automatically contain the JavaScript behavior that your application expects.&lt;/p&gt;

&lt;p&gt;The browser may know:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;There is a button here.
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;But your framework still needs to establish:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;When this button is clicked,
update the shopping cart.
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;That transition, from &lt;strong&gt;server-rendered HTML&lt;/strong&gt; to &lt;strong&gt;interactive application&lt;/strong&gt;, is where hydration comes in.&lt;/p&gt;

&lt;p&gt;At a high level:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Server
   │
   ▼
Render Components
   │
   ▼
HTML
   │
   ▼
Browser Displays HTML
   │
   ▼
JavaScript Loads
   │
   ▼
Hydration
   │
   ▼
Interactive Application
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Hydration sounds simple, but it introduces some of the most interesting performance and architecture challenges in modern frontend development. So let’s follow a page through the process.&lt;/p&gt;





&lt;h2 id="first-what-exactly-is-hydration"&gt;First, What Exactly Is Hydration?&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Hydration is the process through which client-side JavaScript attaches application behavior to HTML that was already rendered on the server.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Imagine a React component:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;function Counter() {
    const [count, setCount] = useState(0);

    return (
        &amp;lt;button onClick={() =&amp;gt; setCount(count + 1)}&amp;gt;
            Count: {count}
        &amp;lt;/button&amp;gt;
    );
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;When server-rendered, the browser may initially receive:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;lt;button&amp;gt;
    Count: 0
&amp;lt;/button&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;That’s perfectly valid HTML. The browser can display it.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;┌────────────────┐
│    Count: 0    │
└────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;But the HTML response itself doesn’t contain React’s &lt;code&gt;setCount&lt;/code&gt; function or the component’s runtime state. The browser still needs the relevant JavaScript. Once that JavaScript arrives, React can connect the existing DOM to the application.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Existing HTML

&amp;lt;button&amp;gt;
    Count: 0
&amp;lt;/button&amp;gt;

        +

Client JavaScript

Counter()
useState()
onClick()

        │
        ▼

     Hydration

        │
        ▼

Interactive Button
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;After hydration, clicking the button can update the state:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Count: 0
   │
   │ click
   ▼
Count: 1
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The key idea is that the framework isn’t necessarily throwing away the server-generated HTML and starting again. It attempts to &lt;strong&gt;reuse the existing DOM and attach the behavior needed to make it interactive&lt;/strong&gt;.&lt;/p&gt;





&lt;h2 id="why-do-we-need-hydration"&gt;Why Do We Need Hydration?&lt;/h2&gt;

&lt;p&gt;Why not simply let the server render the page and leave it there? For some websites, that’s perfectly reasonable. A simple article might require almost no JavaScript. But modern applications often contain interactions such as:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Add to Cart
Like Post
Open Modal
Submit Form
Filter Results
Expand Menu
Update Counter
Drag Item
Search
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Those interactions require application logic somewhere.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;function AddToCart({ product }) {
    const [adding, setAdding] = useState(false);

    async function addToCart() {
        setAdding(true);

        await api.addToCart(product.id);

        setAdding(false);
    }

    return (
        &amp;lt;button onClick={addToCart}&amp;gt;
            {adding ? "Adding..." : "Add to Cart"}
        &amp;lt;/button&amp;gt;
    );
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The server can render:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;lt;button&amp;gt;
    Add to Cart
&amp;lt;/button&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;But the browser still needs the JavaScript responsible for:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Click
  │
  ▼
Set loading state
  │
  ▼
Call API
  │
  ▼
Update cart
  │
  ▼
Update button
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Hydration bridges the gap between &lt;strong&gt;HTML generated elsewhere&lt;/strong&gt; and &lt;strong&gt;behavior running in the browser&lt;/strong&gt;.&lt;/p&gt;





&lt;h2 id="the-full-server-rendering-journey"&gt;The Full Server-Rendering Journey&lt;/h2&gt;

&lt;p&gt;Let’s look at the entire process. Suppose a user requests:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;/products/42
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The server receives the request.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Browser
   │
   │ GET /products/42
   ▼
Server
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The server fetches the product:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;const product = await database.products.findById(42);
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;It renders the application:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;lt;ProductPage product={product} /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;which becomes HTML:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;lt;main&amp;gt;
    &amp;lt;h1&amp;gt;Mechanical Keyboard&amp;lt;/h1&amp;gt;

    &amp;lt;p&amp;gt;KES 12,000&amp;lt;/p&amp;gt;

    &amp;lt;button&amp;gt;
        Add to Cart
    &amp;lt;/button&amp;gt;
&amp;lt;/main&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The response reaches the browser. Now everything we’ve discussed earlier in this series begins happening.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;HTML
 │
 ▼
DOM
 │
 ▼
Layout
 │
 ▼
Paint
 │
 ▼
Pixels
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The user sees the page. Meanwhile, JavaScript required by the application is downloaded.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;HTML visible
     │
     ├──────────────► User sees content
     │
     ▼
JavaScript downloads
     │
     ▼
JavaScript parses
     │
     ▼
JavaScript executes
     │
     ▼
Hydration
     │
     ▼
Application interactive
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This creates an important distinction. A page can be:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;Visible&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;without yet being:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;Fully interactive&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;





&lt;h2 id="visible-does-not-mean-interactive"&gt;Visible Does Not Mean Interactive&lt;/h2&gt;

&lt;p&gt;This is one of the most important concepts to understand about hydration.&lt;/p&gt;

&lt;p&gt;Imagine the following timeline:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;0ms
User requests page

      │
      ▼

300ms
HTML arrives

      │
      ▼

400ms
Content visible

      │
      ▼

800ms
JavaScript downloaded

      │
      ▼

1100ms
JavaScript executed

      │
      ▼

1300ms
Hydration completed
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Between roughly:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;400ms → 1300ms
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;the page may &lt;strong&gt;look ready&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The user can see:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Product

Mechanical Keyboard

KES 12,000

[ Add to Cart ]
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;But depending on the application and framework behavior, the client-side interaction may not yet be ready. The user sees a button. Naturally, they click it. If the required JavaScript hasn’t become operational yet, that interaction can be delayed or require special handling by the framework.&lt;/p&gt;

&lt;p&gt;This creates an interesting UX problem:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;A page can visually promise interactivity before it is ready to deliver it.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;





&lt;h2 id="hydration-is-not-free"&gt;Hydration Is Not Free&lt;/h2&gt;

&lt;p&gt;SSR is sometimes described as though it simply moves rendering from the browser to the server. But with traditional hydration-heavy architectures, the browser can still have significant work to perform.&lt;/p&gt;

&lt;p&gt;Consider this application:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Server
   │
   ▼
Render React Application
   │
   ▼
Send HTML
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Then:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Browser
   │
   ├── Parse HTML
   ├── Render page
   ├── Download React
   ├── Download application JS
   ├── Parse JavaScript
   ├── Execute JavaScript
   └── Hydrate application
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The server rendered the interface. But the browser may still need much of the JavaScript representation of that application so the framework can make it interactive.&lt;/p&gt;

&lt;p&gt;In simplified form:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;SERVER                         CLIENT

Render Components
       │
       ▼
      HTML ──────────────────► Display HTML
                                │
JavaScript ──────────────────► Download
                                │
                                ▼
                              Parse
                                │
                                ▼
                             Execute
                                │
                                ▼
                             Hydrate
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;For a small application, this may be cheap. For a large application, hydration can involve a significant amount of work.&lt;/p&gt;





&lt;h2 id="the-double-work-problem"&gt;The Double-Work Problem&lt;/h2&gt;

&lt;p&gt;This leads to one criticism of traditional hydration architectures.&lt;/p&gt;

&lt;p&gt;The server does work:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Components
    │
    ▼
HTML
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Then the client receives JavaScript describing much of the same application and performs additional work to reconstruct the application’s runtime representation and connect it to the existing DOM.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;SERVER

Component Tree
     │
     ▼
HTML


CLIENT

HTML
 +
Component JavaScript
 +
Application State
     │
     ▼
Hydration
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The server rendering was useful because the user received content earlier. But the client hasn’t escaped JavaScript execution.&lt;/p&gt;

&lt;p&gt;In some architectures, we’ve effectively said:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“Render this application on the server so users can see it quickly, then send enough JavaScript for the browser to understand much of that application again.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This trade-off becomes increasingly noticeable as applications grow.&lt;/p&gt;





&lt;h2 id="a-react-hydration-example"&gt;A React Hydration Example&lt;/h2&gt;

&lt;p&gt;Consider a small server-rendered React application.&lt;/p&gt;

&lt;p&gt;On the server, conceptually:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;const html = renderToString(&amp;lt;App /&amp;gt;);
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The resulting HTML is sent to the browser.&lt;/p&gt;

&lt;p&gt;On the client, instead of creating a completely new DOM tree with:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;createRoot(root).render(&amp;lt;App /&amp;gt;);
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;a server-rendered React application can hydrate the existing DOM:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;import { hydrateRoot } from "react-dom/client";

hydrateRoot(
    document.getElementById("root"),
    &amp;lt;App /&amp;gt;
);
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The distinction is important.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;createRoot()&lt;/code&gt; effectively says:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Build this client application in this root.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;code&gt;hydrateRoot()&lt;/code&gt; says:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;There is already server-generated HTML here. Connect React to it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Conceptually:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Server HTML

&amp;lt;div id="root"&amp;gt;
    &amp;lt;button&amp;gt;Count: 0&amp;lt;/button&amp;gt;
&amp;lt;/div&amp;gt;

        │
        ▼

hydrateRoot(...)

        │
        ▼

React connects to
existing DOM

        │
        ▼

Interactive Application
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This works well when the client expects the same interface the server produced. But what happens when it doesn’t?&lt;/p&gt;





&lt;h2 id="hydration-mismatches"&gt;Hydration Mismatches&lt;/h2&gt;

&lt;p&gt;Suppose the server renders:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;lt;p&amp;gt;Welcome back, Billy&amp;lt;/p&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;but when the client begins hydrating, React expects:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;lt;p&amp;gt;Welcome back, Guest&amp;lt;/p&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Now the server and client disagree. This is a &lt;strong&gt;hydration mismatch&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;SERVER HTML

Welcome back, Billy

        ≠

CLIENT EXPECTATION

Welcome back, Guest
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Frameworks may warn about this because hydration depends on the initial server and client output being compatible. A mismatch can cause the framework to repair or re-render portions of the UI, and it can lead to confusing bugs.&lt;/p&gt;





&lt;h2 id="how-hydration-mismatches-happen"&gt;How Hydration Mismatches Happen&lt;/h2&gt;

&lt;p&gt;Some mismatches are surprisingly easy to create.&lt;/p&gt;

&lt;p&gt;Consider:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;function CurrentTime() {
    return &amp;lt;p&amp;gt;{new Date().toLocaleTimeString()}&amp;lt;/p&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The server renders at:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;10:42:01
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;By the time the browser hydrates:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;10:42:03
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The output may differ.&lt;/p&gt;

&lt;p&gt;Another classic example is random values:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;function Identifier() {
    return &amp;lt;p&amp;gt;{Math.random()}&amp;lt;/p&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The server might produce:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;0.21843
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;while the client produces:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;0.79216
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;That’s a mismatch.&lt;/p&gt;

&lt;p&gt;Browser-only information can cause similar problems.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;window.innerWidth
localStorage
navigator.language
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Those values may not exist on the server or may differ from what the server assumed.&lt;/p&gt;





&lt;h2 id="a-more-subtle-example-authentication"&gt;A More Subtle Example: Authentication&lt;/h2&gt;

&lt;p&gt;Suppose the server knows the user is authenticated. It renders:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;lt;nav&amp;gt;
    &amp;lt;span&amp;gt;Welcome Billy&amp;lt;/span&amp;gt;
    &amp;lt;button&amp;gt;Logout&amp;lt;/button&amp;gt;
&amp;lt;/nav&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;But your client-side authentication store initially starts with:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;const user = null;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The client expects:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;lt;nav&amp;gt;
    &amp;lt;button&amp;gt;Login&amp;lt;/button&amp;gt;
&amp;lt;/nav&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Again:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Server
   │
   └── Authenticated

Client initial state
   │
   └── Not authenticated

        ↓

Hydration mismatch
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The solution is often to make sure the initial client state is derived from the same data used to generate the server output.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Server Data
    │
    ├── Render HTML
    │
    └── Serialize initial state
              │
              ▼
            Client
              │
              ▼
           Hydration
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The server and client need to agree about what the initial page represents.&lt;/p&gt;





&lt;h2 id="why-hydration-can-become-expensive"&gt;Why Hydration Can Become Expensive&lt;/h2&gt;

&lt;p&gt;Imagine a large e-commerce homepage containing:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Header
Navigation
Search
Hero
Recommendations
Categories
Products
Reviews
Newsletter
Footer
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Suppose only a few parts are actually interactive:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Search
Cart
Product carousel
Newsletter form
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;With a traditional full-page hydration model, the browser may still receive JavaScript for a much larger component tree.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Page
│
├── Header
├── Navigation
├── Hero
├── Categories
├── Products
├── Reviews
├── Newsletter
└── Footer

        │
        ▼

Hydrate Everything
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;But much of the page may simply be content. The hero doesn’t necessarily need client-side JavaScript, the footer probably doesn’t need React state, and a product description may never change after rendering. So why hydrate everything?&lt;/p&gt;

&lt;p&gt;That question has driven several newer frontend architecture ideas.&lt;/p&gt;





&lt;h2 id="partial-hydration"&gt;Partial Hydration&lt;/h2&gt;

&lt;p&gt;Instead of hydrating the entire page, what if we hydrate only the interactive parts?&lt;/p&gt;

&lt;p&gt;Imagine:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Page
│
├── Header
│
├── Search ◄──── HYDRATE
│
├── Hero
│
├── Product Grid
│
├── Cart Button ◄ HYDRATE
│
├── Article Content
│
└── Footer
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Now static content remains HTML. Interactive pieces receive JavaScript.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Server HTML
│
├── Static
├── Static
├── Interactive Island + JS
├── Static
├── Interactive Island + JS
└── Static
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This is broadly the idea behind &lt;strong&gt;partial hydration&lt;/strong&gt; and related “islands” architectures.&lt;/p&gt;

&lt;p&gt;The goal is straightforward:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;Don’t ship JavaScript for parts of the page that don’t need JavaScript.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;





&lt;h2 id="islands-architecture"&gt;Islands Architecture&lt;/h2&gt;

&lt;p&gt;Astro is well known for popularizing this model.&lt;/p&gt;

&lt;p&gt;Imagine a page:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;┌───────────────────────────────────────┐
│              Header                   │
├───────────────────────────────────────┤
│                                       │
│          Static Article               │
│                                       │
│       No client JS required           │
│                                       │
├───────────────────────────────────────┤
│       Interactive Comments            │ ◄── JS
├───────────────────────────────────────┤
│              Footer                   │
└───────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The interactive component becomes an &lt;strong&gt;island&lt;/strong&gt; inside mostly static HTML.&lt;/p&gt;

&lt;p&gt;Instead of:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Entire Page
    │
    ▼
Hydrate Everything
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;we get:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Static HTML
    │
    ├── Interactive Island
    │        │
    │        ▼
    │     Hydrate
    │
    └── Static HTML
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;For content-heavy sites, this can dramatically reduce the amount of client JavaScript required.&lt;/p&gt;





&lt;h2 id="lazy-hydration"&gt;Lazy Hydration&lt;/h2&gt;

&lt;p&gt;Even when a component needs JavaScript, it doesn’t necessarily need it immediately. Consider a comments section at the bottom of a long article. The user might never scroll that far. Hydrating it immediately means spending resources on something the user may never interact with. Instead, hydration can be delayed until a condition is met.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Page Loads
    │
    ▼
Comments not visible
    │
    ▼
Do nothing
    │
    │
User scrolls
    │
    ▼
Comments approach viewport
    │
    ▼
Load / Hydrate
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Other triggers could include:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;When visible
When browser is idle
When user interacts
After important content is ready
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This shifts hydration from:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Hydrate everything now.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;to:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Hydrate something when there’s a reason to.&lt;/p&gt;
&lt;/blockquote&gt;





&lt;h2 id="selective-hydration"&gt;Selective Hydration&lt;/h2&gt;

&lt;p&gt;Another approach is &lt;strong&gt;selective hydration&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Suppose a page has several sections waiting to hydrate.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Header
Product Details
Reviews
Recommendations
Footer
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The user clicks something in the header before everything else has completed. Instead of requiring hydration to proceed strictly from top to bottom, a framework can prioritize the part relevant to the user’s interaction.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Hydration Queue

Product Details
Reviews
Recommendations
Header
Footer

User clicks Header

        │
        ▼

Prioritize Header
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The broader principle is important:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;Not all parts of a page are equally urgent.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Modern rendering architectures increasingly try to schedule work according to what matters most to the user.&lt;/p&gt;





&lt;h2 id="streaming-makes-things-even-more-interesting"&gt;Streaming Makes Things Even More Interesting&lt;/h2&gt;

&lt;p&gt;Traditional SSR can look like:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Request
   │
   ▼
Fetch EVERYTHING
   │
   ▼
Render EVERYTHING
   │
   ▼
Send HTML
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;If one slow data source takes two seconds, the entire page might wait. Streaming SSR allows the server to begin sending useful parts of the page earlier.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Request
   │
   ▼
Server

Header ready ───────────────► Browser

Main content ready ─────────► Browser

Recommendations still loading...

Recommendations ready ──────► Browser
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Instead of one giant response arriving after everything is complete, HTML can arrive progressively. Combine that with modern hydration techniques and you get something closer to:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;HTML arrives
     │
     ▼
Content appears
     │
     ▼
More HTML streams
     │
     ▼
Relevant JavaScript arrives
     │
     ▼
Interactive regions hydrate
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The line between “loading” and “loaded” becomes much less binary.&lt;/p&gt;





&lt;h2 id="react-server-components-change-the-question"&gt;React Server Components Change the Question&lt;/h2&gt;

&lt;p&gt;React Server Components take a more fundamental approach.&lt;/p&gt;

&lt;p&gt;Consider:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;function ProductDescription({ product }) {
    return (
        &amp;lt;section&amp;gt;
            &amp;lt;h1&amp;gt;{product.name}&amp;lt;/h1&amp;gt;
            &amp;lt;p&amp;gt;{product.description}&amp;lt;/p&amp;gt;
        &amp;lt;/section&amp;gt;
    );
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;If this component doesn’t need:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;State
Effects
Browser APIs
Event handlers
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;why does its component JavaScript need to run in the browser? With Server Components, it may not.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;SERVER

ProductDescription
Reviews
ProductDetails

        │
        ▼

Rendered representation

        │
        ▼

CLIENT

AddToCartButton
Search
Cart
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Only components that require client-side behavior need to become part of the browser’s interactive JavaScript application.&lt;/p&gt;

&lt;p&gt;This changes the conversation from:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;How do we hydrate the entire server-rendered application efficiently?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;toward:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;How much of this application needs hydration at all?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That’s a much more powerful question.&lt;/p&gt;





&lt;h2 id="server-components-vs-hydration"&gt;Server Components vs Hydration&lt;/h2&gt;

&lt;p&gt;It’s worth separating the concepts.&lt;/p&gt;

&lt;p&gt;A traditional server-rendered client component might go through:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Server Render
     │
     ▼
HTML
     │
     ▼
Browser
     │
     ▼
Hydration
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;A server-only component doesn’t need to become an interactive client component.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Server Component
     │
     ▼
Server
     │
     ▼
Rendered output
     │
     ▼
Browser

No equivalent component
JavaScript required
for hydration
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;But interactive client components still need browser JavaScript.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;"use client";

function AddToCart() {
    const [quantity, setQuantity] = useState(1);

    return (
        &amp;lt;button onClick={() =&amp;gt; add(quantity)}&amp;gt;
            Add to Cart
        &amp;lt;/button&amp;gt;
    );
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;So a modern page may contain both:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Server Components
       │
       └── No client hydration for their component logic

Client Components
       │
       └── Need client-side JavaScript / hydration
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Again, modern frontend architecture is increasingly about choosing boundaries.&lt;/p&gt;





&lt;h2 id="hydration-and-the-main-thread"&gt;Hydration and the Main Thread&lt;/h2&gt;

&lt;p&gt;Now let’s connect hydration to our previous article on the JavaScript event loop.&lt;/p&gt;

&lt;p&gt;Suppose the browser downloads a large JavaScript bundle. It needs to:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Download
   │
   ▼
Parse
   │
   ▼
Compile
   │
   ▼
Execute
   │
   ▼
Hydrate
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Much of that work interacts with the browser’s main thread.&lt;/p&gt;

&lt;p&gt;If hydration becomes expensive:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Main Thread

┌──────────────────────────────┐
│      JavaScript              │
├──────────────────────────────┤
│      Hydration               │
├──────────────────────────────┤
│      More Hydration          │
└──────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;user interactions may have to compete with that work.&lt;/p&gt;

&lt;p&gt;Remember our earlier lesson:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;A page can look ready while JavaScript is still occupying the main thread.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is why shipping less JavaScript isn’t merely about reducing network transfer.&lt;/p&gt;

&lt;p&gt;It can also mean:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Less parsing
Less compilation
Less execution
Less hydration
Less main-thread work
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The network is only part of the cost.&lt;/p&gt;





&lt;h2 id="hydration-and-the-rendering-pipeline"&gt;Hydration and the Rendering Pipeline&lt;/h2&gt;

&lt;p&gt;Hydration also connects directly to our first two articles. Hydration may update DOM state, DOM updates can invalidate styles, some updates may trigger layout, and layout changes may require painting. So the full picture can become:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Server HTML
     │
     ▼
Browser Parses HTML
     │
     ▼
Initial Render
     │
     ▼
JavaScript Loads
     │
     ▼
Hydration
     │
     ▼
DOM / State Updates
     │
     ▼
Style
     │
     ▼
Layout
     │
     ▼
Paint
     │
     ▼
Composite
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The concepts in this series aren’t isolated. They form one system.&lt;/p&gt;





&lt;h2 id="why-huge-javascript-bundles-hurt-ssr-too"&gt;Why Huge JavaScript Bundles Hurt SSR Too&lt;/h2&gt;

&lt;p&gt;Imagine two applications. Both server-render their HTML in:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;300ms
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Application A sends:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;80 KB JavaScript
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Application B sends:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;2 MB JavaScript
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Both might display meaningful content quickly. But the browser still has very different workloads.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Application A

HTML
 │
 ▼
Small JS
 │
 ▼
Hydrate
 │
 ▼
Interactive


Application B

HTML
 │
 ▼
Large JS
 │
 ▼
Download
 │
 ▼
Parse
 │
 ▼
Execute
 │
 ▼
Hydrate
 │
 ▼
Interactive
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This is why saying:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“We’re using SSR, so initial performance is solved.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;can be misleading. SSR addresses only part of the journey.&lt;/p&gt;





&lt;h2 id="what-about-event-handlers"&gt;What About Event Handlers?&lt;/h2&gt;

&lt;p&gt;A common simplified explanation of hydration says:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“Hydration attaches event listeners to server-rendered HTML.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That’s useful as an introduction, but hydration generally involves more than literally walking through every element and calling &lt;code&gt;addEventListener&lt;/code&gt;. Frameworks have different event systems and hydration strategies. For example, React uses event delegation for many events.&lt;/p&gt;

&lt;p&gt;The more accurate mental model is:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;Hydration connects server-rendered DOM with the client framework’s runtime representation so that state, events, and future updates can behave correctly.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Think:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Static Server DOM

        +

Client Application Runtime

        │
        ▼

Connected Interactive UI
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;rather than simply:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;HTML + click handlers
&lt;/code&gt;&lt;/pre&gt;





&lt;h2 id="hydration-errors-are-architecture-clues"&gt;Hydration Errors Are Architecture Clues&lt;/h2&gt;

&lt;p&gt;When developers encounter hydration warnings, the instinct is often to silence them. But a mismatch can reveal an architectural problem.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;function Theme() {
    const theme = localStorage.getItem("theme");

    return &amp;lt;div className={theme}&amp;gt;...&amp;lt;/div&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This code assumes browser storage exists. The server cannot make the same assumption. If the component participates in SSR, you need to think carefully about where that value should come from and when it should be read.&lt;/p&gt;

&lt;p&gt;Likewise:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;lt;p&amp;gt;{window.innerWidth}&amp;lt;/p&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;asks for browser-specific state during rendering.&lt;/p&gt;

&lt;p&gt;Hydration forces us to confront a fundamental reality:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Server Environment
       ≠
Browser Environment
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Code that crosses that boundary needs to be designed accordingly.&lt;/p&gt;





&lt;h2 id="a-practical-example-theme-preference"&gt;A Practical Example: Theme Preference&lt;/h2&gt;

&lt;p&gt;Suppose a user prefers dark mode. The browser has:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;localStorage.setItem("theme", "dark");
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;But the server doesn’t have access to that browser storage. It renders:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;lt;body class="light"&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Then the browser loads JavaScript and discovers:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Theme = dark
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The page switches:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Light
  │
  ▼
Dark
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The user may see a flash of the wrong theme. This isn’t necessarily a hydration problem alone, but it demonstrates how server/client differences can become visible.&lt;/p&gt;

&lt;p&gt;One solution might be storing the preference in a cookie the server can read.&lt;/p&gt;

&lt;p&gt;Then:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Cookie
  │
  ├────► Server renders dark
  │
  └────► Client initializes dark
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Both sides agree.&lt;/p&gt;

&lt;p&gt;This illustrates a broader principle:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;The closer server and client are to sharing the same initial truth, the smoother hydration becomes.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;





&lt;h2 id="a-practical-example-responsive-rendering"&gt;A Practical Example: Responsive Rendering&lt;/h2&gt;

&lt;p&gt;Suppose the server tries to render different markup for mobile and desktop.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;if (window.innerWidth &amp;lt; 768) {
    return &amp;lt;MobileNavigation /&amp;gt;;
}

return &amp;lt;DesktopNavigation /&amp;gt;;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;There’s an immediate problem. On the server, &lt;code&gt;window&lt;/code&gt; doesn’t exist. You could guess based on request information, but that guess may differ from the browser’s actual environment.&lt;/p&gt;

&lt;p&gt;A safer solution is often letting CSS handle purely visual responsive differences:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;.desktop-nav {
    display: block;
}

.mobile-nav {
    display: none;
}

@media (max-width: 768px) {
    .desktop-nav {
        display: none;
    }

    .mobile-nav {
        display: block;
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The server can produce stable markup while the browser’s CSS handles presentation.&lt;/p&gt;

&lt;p&gt;Hydration encourages developers to distinguish between:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Application state

and

Presentation state
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Not every browser difference needs to become JavaScript logic.&lt;/p&gt;





&lt;h2 id="when-hydration-is-worth-the-cost"&gt;When Hydration Is Worth the Cost&lt;/h2&gt;

&lt;p&gt;After discussing all these problems, hydration can sound like something we should avoid completely. That’s not the lesson.&lt;/p&gt;

&lt;p&gt;Hydration provides an extremely useful combination:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Fast server-rendered content

        +

Rich client-side interaction
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;For many applications, that’s exactly what we want. Imagine an e-commerce product page.&lt;/p&gt;

&lt;p&gt;Users benefit from seeing:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Product Name
Image
Price
Description
Reviews
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;as early as possible.&lt;/p&gt;

&lt;p&gt;But they also need:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Add to Cart
Choose Variant
Save Product
Update Quantity
Interactive Gallery
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Server rendering plus hydration can provide both.&lt;/p&gt;

&lt;p&gt;The question isn’t:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Is hydration bad?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The better question is:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;How much hydration does this page actually need?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;





&lt;h2 id="when-you-may-not-need-hydration"&gt;When You May Not Need Hydration&lt;/h2&gt;

&lt;p&gt;Suppose you’re building a documentation page.&lt;/p&gt;

&lt;p&gt;It contains:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Heading
Paragraphs
Code examples
Images
Links
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Perhaps the only interaction is copying code. Do you really need a full client-side framework runtime for the entire page? Possibly not.&lt;/p&gt;

&lt;p&gt;You might send static HTML and use a tiny amount of JavaScript for:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Copy Button
Search
Theme Toggle
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Likewise, a marketing page may need only:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Mobile menu
Newsletter form
Analytics
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The rest can remain HTML and CSS.&lt;/p&gt;

&lt;p&gt;A powerful frontend optimization is sometimes simply:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;Don’t make static content into a JavaScript application unless it needs to be one.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;





&lt;h2 id="hydration-strategies-compared"&gt;Hydration Strategies Compared&lt;/h2&gt;

&lt;p&gt;We can summarize the main ideas.&lt;/p&gt;

&lt;h4 id="full-hydration"&gt;Full Hydration&lt;/h4&gt;

&lt;pre&gt;&lt;code&gt;Entire Page
    │
    ▼
Hydrate
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Simple mental model, but potentially more client-side work.&lt;/p&gt;

&lt;h4 id="partial-hydration-1"&gt;Partial Hydration&lt;/h4&gt;

&lt;pre&gt;&lt;code&gt;Page
├── Static
├── Interactive ◄── Hydrate
├── Static
└── Interactive ◄── Hydrate
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Only interactive regions hydrate.&lt;/p&gt;

&lt;h4 id="lazy-hydration-1"&gt;Lazy Hydration&lt;/h4&gt;

&lt;pre&gt;&lt;code&gt;Interactive Component
        │
        ▼
Wait until needed
        │
        ▼
Hydrate
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Hydration is delayed.&lt;/p&gt;

&lt;h4 id="selective-hydration-1"&gt;Selective Hydration&lt;/h4&gt;

&lt;pre&gt;&lt;code&gt;Several regions waiting
        │
        ▼
User interacts
        │
        ▼
Prioritize relevant region
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Urgent parts can receive attention first.&lt;/p&gt;

&lt;h4 id="server-only-components"&gt;Server-Only Components&lt;/h4&gt;

&lt;pre&gt;&lt;code&gt;Component
    │
    ▼
Server
    │
    ▼
Rendered output

No client component
runtime required
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Instead of optimizing hydration, avoid needing it for that component. These strategies aren’t necessarily mutually exclusive. Modern frameworks can combine several of them.&lt;/p&gt;





&lt;h2 id="the-bigger-trend-ship-less-javascript"&gt;The Bigger Trend: Ship Less JavaScript&lt;/h2&gt;

&lt;p&gt;For years, frontend development often moved in one direction:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;More application logic
        │
        ▼
More JavaScript
        │
        ▼
More client rendering
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The industry is increasingly reconsidering that default.&lt;/p&gt;

&lt;p&gt;Modern architectures ask:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Does this code need to run
in the browser?

        │
   ┌────┴────┐
   │         │
  Yes        No
   │         │
   ▼         ▼
Client      Server
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;That doesn’t mean JavaScript is going away. Far from it. Rich interfaces still depend heavily on it. But we can become more intentional about &lt;strong&gt;which JavaScript users actually need to download and execute&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Hydration is central to that conversation.&lt;/p&gt;





&lt;h2 id="how-to-think-about-hydration-performance"&gt;How to Think About Hydration Performance&lt;/h2&gt;

&lt;p&gt;When profiling a server-rendered application, don’t look only at how quickly HTML arrives. Think about the whole lifecycle.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Request
   │
   ▼
Server Rendering
   │
   ▼
HTML Arrives
   │
   ▼
Content Visible
   │
   ▼
JavaScript Downloads
   │
   ▼
JavaScript Executes
   │
   ▼
Hydration
   │
   ▼
Interaction Ready
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Ask:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;How much JavaScript are we shipping?&lt;/li&gt;
  &lt;li&gt;How much of it is needed immediately?&lt;/li&gt;
  &lt;li&gt;Which components actually require client-side behavior?&lt;/li&gt;
  &lt;li&gt;Is hydration producing long tasks?&lt;/li&gt;
  &lt;li&gt;Can below-the-fold functionality wait?&lt;/li&gt;
  &lt;li&gt;Are server and client producing the same initial state?&lt;/li&gt;
  &lt;li&gt;Can static sections remain server-only?&lt;/li&gt;
  &lt;li&gt;Are we measuring interaction responsiveness, not just page visibility?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those questions are much more useful than simply asking whether SSR is enabled.&lt;/p&gt;





&lt;h2 id="connecting-everything-weve-learned"&gt;Connecting Everything We’ve Learned&lt;/h2&gt;

&lt;p&gt;We’re now four articles into &lt;strong&gt;Beyond the UI&lt;/strong&gt;, and the pieces are beginning to connect.&lt;/p&gt;

&lt;p&gt;First, we learned how browsers create pixels:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;HTML
 │
 ▼
DOM
 │
 ▼
Render Tree
 │
 ▼
Layout
 │
 ▼
Paint
 │
 ▼
Composite
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Then we learned how DOM and style changes can trigger expensive rendering work:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Reflow
Repaint
Compositing
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Then we explored how JavaScript competes for time on the main thread:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Tasks
Microtasks
Event Loop
Rendering
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Then we moved rendering to the server:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;CSR
SSR
SSG
Hybrid Rendering
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;And now hydration connects those worlds:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;SERVER

Components
    │
    ▼
HTML

    │
    ▼

BROWSER

HTML becomes visible
    │
    ▼
JavaScript arrives
    │
    ▼
Hydration
    │
    ▼
Interactive application
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Frontend performance isn’t one thing. It’s the result of all these systems interacting.&lt;/p&gt;





&lt;h2 id="bringing-it-all-together"&gt;Bringing It All Together&lt;/h2&gt;

&lt;p&gt;Hydration exists because server-rendered HTML and client-side applications solve different parts of the user experience.&lt;/p&gt;

&lt;p&gt;Server rendering can give us:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Content quickly
SEO-friendly HTML
Useful initial document
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Client JavaScript gives us:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;State
Events
Interactions
Dynamic updates
Rich application behavior
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Hydration connects them.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Server-Rendered HTML
          │
          │
          ├──── Client JavaScript
          │
          ▼
       Hydration
          │
          ▼
Interactive Application
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;But that connection has a cost. JavaScript must be transferred, parsed, and executed. The framework must connect itself to the server-rendered DOM, and the server and client must agree on what that initial DOM should represent.&lt;/p&gt;

&lt;p&gt;That’s why newer frontend architectures increasingly ask whether every part of a page needs hydration at all.&lt;/p&gt;

&lt;p&gt;Sometimes the best hydration optimization is:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Hydrate later.
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Sometimes it’s:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Hydrate only this component.
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;And sometimes it’s:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Don't hydrate this component.
&lt;/code&gt;&lt;/pre&gt;





&lt;h2 id="final-thoughts"&gt;Final Thoughts&lt;/h2&gt;

&lt;p&gt;Hydration is easy to miss because, when everything works properly, the user never sees it. A page arrives, content appears, buttons work, and the application feels alive. But between those moments, the browser may be doing substantial work.&lt;/p&gt;

&lt;p&gt;Understanding hydration helps explain why a server-rendered website can still ship too much JavaScript, why a visible page can still feel unresponsive, why server/client differences produce strange warnings, and why modern frameworks are becoming increasingly interested in server-only components, islands, streaming, and selective hydration.&lt;/p&gt;

&lt;p&gt;Most importantly, hydration teaches us another lesson that keeps appearing throughout this series:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;Frontend performance isn’t about making one stage fast. It’s about reducing unnecessary work across the entire journey from server to interaction.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The server can generate HTML in 50 milliseconds. The browser can paint it immediately. But if the user then has to wait while megabytes of JavaScript are downloaded, parsed, executed, and hydrated, we’ve optimized only part of the experience.&lt;/p&gt;

&lt;p&gt;The real goal is not simply:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;How quickly can we render the page?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It’s:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;How quickly can we give the user a page that is both useful and ready to respond?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;





&lt;h2 id="whats-next"&gt;What’s Next?&lt;/h2&gt;

&lt;p&gt;We’ve now seen how a server-rendered application becomes interactive. But once the application is alive, another problem quickly appears. Where should all of its data live?&lt;/p&gt;

&lt;p&gt;A simple component might have a boolean:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;const [open, setOpen] = useState(false);
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Then another component needs the same information, another page needs user data, the URL represents filters, API responses need caching, and forms have their own state.&lt;/p&gt;

&lt;p&gt;Before long, everything seems to be called “state” even though these values have completely different lifecycles and responsibilities.&lt;/p&gt;

&lt;p&gt;So next in &lt;strong&gt;Beyond the UI&lt;/strong&gt;:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;State Management Explained: Why Frontend State Gets Complicated&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;We’ll explore local state, shared state, server state, URL state, form state, and derived state, and why the solution to frontend state management isn’t simply putting everything into one global store.&lt;/p&gt;

</description>
      <category>frontend</category>
      <category>performance</category>
    </item>
    <item>
      <title>Client-Side Rendering vs Server-Side Rendering Explained: Where Should Your UI Be Built?</title>
      <dc:creator>Billy Okeyo</dc:creator>
      <pubDate>Fri, 28 Aug 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/billy_de_cartel/client-side-rendering-vs-server-side-rendering-explained-where-should-your-ui-be-built-220e</link>
      <guid>https://dev.to/billy_de_cartel/client-side-rendering-vs-server-side-rendering-explained-where-should-your-ui-be-built-220e</guid>
      <description>&lt;blockquote&gt;
  &lt;p&gt;&lt;em&gt;“Every web application eventually becomes HTML in the browser. The interesting question is where that HTML should be created.”&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Open two modern websites in your browser. They may look almost identical: both have navigation bars, product cards, forms, dashboards, buttons, and interactive components, and both may even be built using React. But underneath, the journey those interfaces took before appearing on your screen could be completely different.&lt;/p&gt;

&lt;p&gt;One application might send the browser a relatively small HTML document:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;lt;div id="root"&amp;gt;&amp;lt;/div&amp;gt;

&amp;lt;script src="https://billyokeyo.dev/app.js"&amp;gt;&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;JavaScript downloads, executes, fetches data, builds the interface, and inserts it into the page.&lt;/p&gt;

&lt;p&gt;Another application might send this immediately:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;lt;main&amp;gt;
    &amp;lt;h1&amp;gt;Products&amp;lt;/h1&amp;gt;

    &amp;lt;article&amp;gt;
        &amp;lt;h2&amp;gt;MacBook Pro&amp;lt;/h2&amp;gt;
        &amp;lt;p&amp;gt;KES 250,000&amp;lt;/p&amp;gt;
    &amp;lt;/article&amp;gt;
&amp;lt;/main&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The browser already has meaningful content before the application’s JavaScript finishes loading.&lt;/p&gt;

&lt;p&gt;The first approach is broadly known as &lt;strong&gt;Client-Side Rendering (CSR)&lt;/strong&gt;, and the second is &lt;strong&gt;Server-Side Rendering (SSR)&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;At first, the difference sounds simple:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;CSR

Server
  │
  ▼
JavaScript
  │
  ▼
Browser builds UI
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;versus:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;SSR

Server builds UI
  │
  ▼
HTML
  │
  ▼
Browser displays UI
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;But that small architectural decision affects much more than where some HTML is generated.&lt;/p&gt;

&lt;p&gt;It influences:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Initial page load&lt;/li&gt;
  &lt;li&gt;JavaScript requirements&lt;/li&gt;
  &lt;li&gt;SEO&lt;/li&gt;
  &lt;li&gt;Caching&lt;/li&gt;
  &lt;li&gt;Server infrastructure&lt;/li&gt;
  &lt;li&gt;Time to interactive&lt;/li&gt;
  &lt;li&gt;Data fetching&lt;/li&gt;
  &lt;li&gt;Navigation&lt;/li&gt;
  &lt;li&gt;Personalization&lt;/li&gt;
  &lt;li&gt;Failure modes&lt;/li&gt;
  &lt;li&gt;Application complexity&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And modern frameworks have made the distinction even more interesting. Next.js can render some things on the server and others in the browser, Nuxt does something similar for Vue, and Astro can ship almost no JavaScript for parts of a page while hydrating interactive islands. Modern applications increasingly aren’t purely client-rendered or purely server-rendered.&lt;/p&gt;

&lt;p&gt;To understand why, we first need to understand what actually happens in each model.&lt;/p&gt;





&lt;h2 id="what-is-client-side-rendering"&gt;What Is Client-Side Rendering?&lt;/h2&gt;

&lt;p&gt;In Client-Side Rendering, the browser receives enough HTML to bootstrap the application, but JavaScript performs much of the work required to construct the actual interface.&lt;/p&gt;

&lt;p&gt;A simplified React application might start with:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;title&amp;gt;Store&amp;lt;/title&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;

    &amp;lt;div id="root"&amp;gt;&amp;lt;/div&amp;gt;

    &amp;lt;script src="https://billyokeyo.dev/app.js"&amp;gt;&amp;lt;/script&amp;gt;

&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Notice what’s missing: there are no products, there is no navigation, and there may not even be a visible heading. Instead, the browser downloads &lt;code&gt;app.js&lt;/code&gt;, and React then mounts the application.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;const root = ReactDOM.createRoot(
    document.getElementById("root")
);

root.render(&amp;lt;App /&amp;gt;);
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The application may then fetch data.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;fetch("/api/products")
    .then(response =&amp;gt; response.json())
    .then(products =&amp;gt; {
        // Update application state
    });
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Eventually, React creates the DOM necessary to display the page.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Request Page
     │
     ▼
Server
     │
     ▼
Minimal HTML
     │
     ▼
Browser
     │
     ▼
Download JavaScript
     │
     ▼
Parse JavaScript
     │
     ▼
Execute Application
     │
     ▼
Fetch Data
     │
     ▼
Build DOM
     │
     ▼
Render UI
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The browser does a significant amount of work before the user sees the completed application. That’s Client-Side Rendering.&lt;/p&gt;





&lt;h2 id="why-client-side-rendering-became-popular"&gt;Why Client-Side Rendering Became Popular&lt;/h2&gt;

&lt;p&gt;Traditional websites worked differently. Clicking a link usually caused the browser to request another document from the server.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Page A
  │
  │ Click link
  ▼
Server Request
  │
  ▼
New HTML Document
  │
  ▼
Page B
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The browser navigated away from the current page and loaded another.&lt;/p&gt;

&lt;p&gt;This model worked extremely well, and it still does.&lt;/p&gt;

&lt;p&gt;But as web applications became increasingly interactive, developers wanted experiences that behaved more like desktop applications. Instead of requesting an entirely new page whenever something changed, JavaScript could update only the necessary parts of the interface.&lt;/p&gt;

&lt;p&gt;This led to the rise of &lt;strong&gt;Single-Page Applications&lt;/strong&gt;, or SPAs. Frameworks such as Angular, React, and Vue made these applications much easier to build, and navigation could happen without a full page reload.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Initial Page
     │
     ▼
JavaScript Application
     │
     ├────► /products
     │
     ├────► /orders
     │
     └────► /profile
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The application remained loaded while the UI changed around it.&lt;/p&gt;

&lt;p&gt;For dashboards, admin portals, project management tools, email clients, and other highly interactive applications, this was extremely attractive.&lt;/p&gt;





&lt;h2 id="the-client-side-rendering-experience"&gt;The Client-Side Rendering Experience&lt;/h2&gt;

&lt;p&gt;Imagine visiting an online store implemented entirely using CSR.&lt;/p&gt;

&lt;p&gt;The browser requests:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;GET /products
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The server might respond with:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html&amp;gt;
&amp;lt;body&amp;gt;

    &amp;lt;div id="root"&amp;gt;&amp;lt;/div&amp;gt;

    &amp;lt;script src="https://billyokeyo.dev/bundle.js"&amp;gt;&amp;lt;/script&amp;gt;

&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The browser can parse this almost immediately, but there still isn’t much useful content.&lt;/p&gt;

&lt;p&gt;Next:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Download bundle.js
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Perhaps that bundle is:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;300 KB
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;or:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;1 MB
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;or considerably larger.&lt;/p&gt;

&lt;p&gt;Downloading isn’t the end of the work.&lt;/p&gt;

&lt;p&gt;JavaScript must also be:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Downloaded
    │
    ▼
Parsed
    │
    ▼
Compiled
    │
    ▼
Executed
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Then the application starts and may immediately request:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;GET /api/products
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Only after that response arrives can the application render the product list.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;HTML
 │
 ▼
JavaScript
 │
 ▼
API Request
 │
 ▼
Data
 │
 ▼
UI
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This creates what is sometimes called a &lt;strong&gt;request waterfall&lt;/strong&gt;: the user requested the page long ago, but useful content depended on several sequential steps.&lt;/p&gt;





&lt;h2 id="the-empty-page-problem"&gt;The Empty Page Problem&lt;/h2&gt;

&lt;p&gt;This is one of the classic weaknesses of pure CSR.&lt;/p&gt;

&lt;p&gt;Suppose your initial HTML contains:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;lt;div id="root"&amp;gt;
    &amp;lt;div class="spinner"&amp;gt;&amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The browser can render something, but the actual content isn’t available yet.&lt;/p&gt;

&lt;p&gt;On a fast laptop with fiber internet, this might happen so quickly that nobody notices.&lt;/p&gt;

&lt;p&gt;Now imagine:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Budget Android phone
        +
Slow mobile network
        +
Large JavaScript bundle
        +
Slow API response
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The experience changes considerably.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;User opens page
      │
      ▼
Blank / Loading UI
      │
      │
      │
      ▼
JavaScript loads
      │
      ▼
Application starts
      │
      ▼
Data loads
      │
      ▼
Content appears
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This doesn’t mean CSR is inherently slow. A well-built client-rendered application can be extremely fast, but it does mean the browser may have more work to perform before meaningful content appears.&lt;/p&gt;





&lt;h2 id="what-is-server-side-rendering"&gt;What Is Server-Side Rendering?&lt;/h2&gt;

&lt;p&gt;Server-Side Rendering moves some of that work away from the browser. Instead of sending an almost empty application shell, the server generates HTML for the requested page.&lt;/p&gt;

&lt;p&gt;Suppose the user requests:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;GET /products
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The server loads the necessary data.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Server
  │
  ▼
Database / API
  │
  ▼
Products
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;It then renders the page. The browser receives:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;lt;main&amp;gt;
    &amp;lt;h1&amp;gt;Products&amp;lt;/h1&amp;gt;

    &amp;lt;article&amp;gt;
        &amp;lt;h2&amp;gt;Laptop&amp;lt;/h2&amp;gt;
        &amp;lt;p&amp;gt;KES 75,000&amp;lt;/p&amp;gt;
    &amp;lt;/article&amp;gt;

    &amp;lt;article&amp;gt;
        &amp;lt;h2&amp;gt;Monitor&amp;lt;/h2&amp;gt;
        &amp;lt;p&amp;gt;KES 35,000&amp;lt;/p&amp;gt;
    &amp;lt;/article&amp;gt;
&amp;lt;/main&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The journey becomes:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Request
   │
   ▼
Server
   │
   ├── Fetch data
   │
   └── Render HTML
   │
   ▼
HTML Response
   │
   ▼
Browser
   │
   ▼
Content
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The browser receives meaningful HTML from the beginning. That’s Server-Side Rendering.&lt;/p&gt;





&lt;h2 id="a-simple-server-side-example"&gt;A Simple Server-Side Example&lt;/h2&gt;

&lt;p&gt;Imagine an Express application using a template engine.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;app.get("/products", async (req, res) =&amp;gt; {
    const products = await productService.getAll();

    res.render("products", {
        products
    });
});
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The template might contain:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;lt;h1&amp;gt;Products&amp;lt;/h1&amp;gt;

&amp;lt;% products.forEach(product =&amp;gt; { %&amp;gt;

    &amp;lt;article&amp;gt;
        &amp;lt;h2&amp;gt;&amp;lt;%= product.name %&amp;gt;&amp;lt;/h2&amp;gt;
        &amp;lt;p&amp;gt;&amp;lt;%= product.price %&amp;gt;&amp;lt;/p&amp;gt;
    &amp;lt;/article&amp;gt;

&amp;lt;% }) %&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The browser doesn’t need JavaScript to create those product elements because the server already did it.&lt;/p&gt;

&lt;p&gt;This model isn’t new. PHP, Ruby on Rails, Django, Laravel, ASP.NET MVC, JSP, and countless other technologies have rendered HTML on servers for decades. What changed is that modern frontend frameworks started bringing server rendering back into JavaScript-heavy application architectures.&lt;/p&gt;





&lt;h2 id="csr-vs-ssr-the-fundamental-difference"&gt;CSR vs SSR: The Fundamental Difference&lt;/h2&gt;

&lt;p&gt;At its simplest:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;CLIENT-SIDE RENDERING

Server
  │
  ▼
Application Shell
  │
  ▼
Browser
  │
  ├── Load JavaScript
  ├── Execute Application
  ├── Fetch Data
  └── Build UI
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;While:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;SERVER-SIDE RENDERING

Browser
  │
  ▼
Server
  │
  ├── Fetch Data
  └── Build HTML
  │
  ▼
Browser receives UI
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The final browser DOM may look almost identical, but the difference is &lt;strong&gt;where the initial work happened&lt;/strong&gt;.&lt;/p&gt;





&lt;h2 id="but-server-rendered-html-isnt-necessarily-interactive"&gt;But Server-Rendered HTML Isn’t Necessarily Interactive&lt;/h2&gt;

&lt;p&gt;Suppose the server sends:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;lt;button id="cart"&amp;gt;
    Add to Cart
&amp;lt;/button&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The browser can display the button immediately, but what happens when the user clicks it?&lt;/p&gt;

&lt;p&gt;If the page is supposed to behave like an interactive React application, the browser still needs JavaScript. This introduces another important concept: &lt;strong&gt;Hydration&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A server-rendered React application might conceptually work like this:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Server
  │
  ▼
Render React Components
  │
  ▼
HTML
  │
  ▼
Browser Displays Page
  │
  ▼
JavaScript Downloads
  │
  ▼
React Hydrates HTML
  │
  ▼
Page Becomes Fully Interactive
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Before hydration:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Looks like application
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;After hydration:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Behaves like application
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This distinction creates an interesting performance problem. A user may be able to &lt;strong&gt;see&lt;/strong&gt; a button before the JavaScript necessary to handle that button has finished loading and executing. The page looks ready, but it isn’t necessarily ready.&lt;/p&gt;

&lt;p&gt;We’ll explore hydration deeply in the next article. For now, remember:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;SSR can make content visible earlier, but interactive applications may still require significant client-side JavaScript.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;





&lt;h2 id="initial-load-performance"&gt;Initial Load Performance&lt;/h2&gt;

&lt;p&gt;This is where CSR and SSR are often compared most aggressively. Imagine a CSR application:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Request
  │
  ▼
HTML
  │
  ▼
JavaScript
  │
  ▼
Execute
  │
  ▼
Fetch Data
  │
  ▼
Render
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Now SSR:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Request
  │
  ▼
Server Fetches Data
  │
  ▼
Server Renders
  │
  ▼
HTML
  │
  ▼
Browser Renders
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;SSR can often deliver meaningful content earlier because the browser doesn’t have to wait for the application to construct that content, but that doesn’t automatically mean SSR is faster in every situation. The server now has work to do before sending the response.&lt;/p&gt;

&lt;p&gt;If server rendering requires slow database queries:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Request
  │
  ▼
Database
  │
  │ 1.5 seconds
  ▼
Render
  │
  ▼
Response
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;the browser may wait longer for the initial HTML.&lt;/p&gt;

&lt;p&gt;Performance depends on the entire system.&lt;/p&gt;





&lt;h2 id="time-to-first-byte-vs-useful-content"&gt;Time to First Byte vs Useful Content&lt;/h2&gt;

&lt;p&gt;SSR can introduce an interesting trade-off. With CSR, the server can often return the initial shell quickly:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Request
  │
  ▼
HTML shell
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;That can produce a fast &lt;strong&gt;Time to First Byte&lt;/strong&gt;, but meaningful content may arrive later.&lt;/p&gt;

&lt;p&gt;SSR may take longer before sending the first HTML because the server needs to fetch data and render the page.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Request
  │
  ▼
Fetch Data
  │
  ▼
Render
  │
  ▼
HTML
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;But when the response arrives, it already contains useful content. So asking whether one approach returns HTML faster isn’t enough. A better question is:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;“When can the user actually see and use the content they came for?”&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;





&lt;h2 id="seo-and-crawlers"&gt;SEO and Crawlers&lt;/h2&gt;

&lt;p&gt;SEO is one of the most frequently cited reasons for SSR.&lt;/p&gt;

&lt;p&gt;Imagine a crawler receives:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;lt;div id="root"&amp;gt;&amp;lt;/div&amp;gt;

&amp;lt;script src="https://billyokeyo.dev/app.js"&amp;gt;&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The meaningful content depends on JavaScript execution. Modern search engines have become much better at processing JavaScript, but server-rendered HTML still provides a simpler and more predictable document for crawlers, link previews, social platforms, and other systems that consume webpage metadata.&lt;/p&gt;

&lt;p&gt;Compare:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;lt;div id="root"&amp;gt;&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;with:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;lt;article&amp;gt;
    &amp;lt;h1&amp;gt;
        The Browser Rendering Pipeline Explained
    &amp;lt;/h1&amp;gt;

    &amp;lt;p&amp;gt;
        Learn how browsers turn HTML into pixels...
    &amp;lt;/p&amp;gt;
&amp;lt;/article&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The second document already describes the content without requiring application execution.&lt;/p&gt;

&lt;p&gt;For:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Blogs&lt;/li&gt;
  &lt;li&gt;Documentation&lt;/li&gt;
  &lt;li&gt;News websites&lt;/li&gt;
  &lt;li&gt;E-commerce product pages&lt;/li&gt;
  &lt;li&gt;Marketing websites&lt;/li&gt;
  &lt;li&gt;Public landing pages&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;having content available in the initial HTML is often valuable.&lt;/p&gt;

&lt;p&gt;For:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Internal accounting dashboard
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;SEO probably doesn’t matter at all. Architecture should follow requirements.&lt;/p&gt;





&lt;h2 id="csr-can-be-excellent-for-application-like-interfaces"&gt;CSR Can Be Excellent for Application-Like Interfaces&lt;/h2&gt;

&lt;p&gt;Imagine you’re building an internal analytics dashboard. Users authenticate once, then spend hours navigating between:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Overview
Reports
Customers
Invoices
Settings
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;SEO is irrelevant, the application is highly interactive, and users frequently move between screens.&lt;/p&gt;

&lt;p&gt;A client-rendered SPA can work extremely well. After the initial JavaScript has loaded, navigation may require only data requests.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Application already loaded
        │
        ├── /api/reports
        ├── /api/customers
        └── /api/invoices
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The shell remains in the browser, and only data and necessary UI updates change.&lt;/p&gt;

&lt;p&gt;This can create a very responsive application experience. The point isn’t that CSR is outdated; it’s that &lt;strong&gt;different applications have different performance profiles&lt;/strong&gt;.&lt;/p&gt;





&lt;h2 id="ssr-has-a-server-cost"&gt;SSR Has a Server Cost&lt;/h2&gt;

&lt;p&gt;With CSR, your backend might primarily serve static assets and APIs, and a CDN can distribute:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;index.html
app.js
styles.css
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;very efficiently.&lt;/p&gt;

&lt;p&gt;With dynamic SSR, each page request may require server computation.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Request 1 ─────► Render Page

Request 2 ─────► Render Page

Request 3 ─────► Render Page

Request 4 ─────► Render Page
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;At scale:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Thousands of requests
        │
        ▼
Server Rendering
        │
        ▼
CPU + Memory + Data Access
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Now you need to think about:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Server capacity&lt;/li&gt;
  &lt;li&gt;Rendering latency&lt;/li&gt;
  &lt;li&gt;Caching&lt;/li&gt;
  &lt;li&gt;Database load&lt;/li&gt;
  &lt;li&gt;Failure handling&lt;/li&gt;
  &lt;li&gt;Geographic distribution&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;SSR moves work away from the client, but it doesn’t make the work disappear. It moves responsibility to another part of the system.&lt;/p&gt;





&lt;h2 id="static-site-generation-enters-the-picture"&gt;Static Site Generation Enters the Picture&lt;/h2&gt;

&lt;p&gt;What if the page doesn’t change on every request? Suppose you’re publishing a technical article. Rendering it on the server every single time someone visits may be unnecessary, so instead, the HTML can be generated during deployment. This is commonly known as &lt;strong&gt;Static Site Generation&lt;/strong&gt;, or SSG.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;BUILD TIME

Markdown
   │
   ▼
Framework
   │
   ▼
Generate HTML
   │
   ▼
Static File
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Then at request time:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;User
 │
 ▼
CDN
 │
 ▼
Pre-generated HTML
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;No application server needs to render the article for every visitor, which can be extremely fast and highly cacheable.&lt;/p&gt;





&lt;h2 id="csr-vs-ssr-vs-ssg"&gt;CSR vs SSR vs SSG&lt;/h2&gt;

&lt;p&gt;We now have three broad models.&lt;/p&gt;

&lt;h3 id="client-side-rendering"&gt;Client-Side Rendering&lt;/h3&gt;

&lt;pre&gt;&lt;code&gt;Request Time

Server
  │
  ▼
Application Shell
  │
  ▼
Browser builds UI
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id="server-side-rendering"&gt;Server-Side Rendering&lt;/h3&gt;

&lt;pre&gt;&lt;code&gt;Request Time

Request
  │
  ▼
Server builds UI
  │
  ▼
HTML
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id="static-site-generation"&gt;Static Site Generation&lt;/h3&gt;

&lt;pre&gt;&lt;code&gt;Build Time

Framework
  │
  ▼
HTML generated ahead of time

Request Time

CDN
  │
  ▼
HTML
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;A simplified comparison:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Characteristic&lt;/th&gt;
      &lt;th&gt;CSR&lt;/th&gt;
      &lt;th&gt;SSR&lt;/th&gt;
      &lt;th&gt;SSG&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;Initial HTML content&lt;/td&gt;
      &lt;td&gt;Limited in pure CSR&lt;/td&gt;
      &lt;td&gt;Rich&lt;/td&gt;
      &lt;td&gt;Rich&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Rendering happens&lt;/td&gt;
      &lt;td&gt;Browser&lt;/td&gt;
      &lt;td&gt;Server&lt;/td&gt;
      &lt;td&gt;Build time&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;SEO friendliness&lt;/td&gt;
      &lt;td&gt;Depends on implementation&lt;/td&gt;
      &lt;td&gt;Strong&lt;/td&gt;
      &lt;td&gt;Strong&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Server work per request&lt;/td&gt;
      &lt;td&gt;Low&lt;/td&gt;
      &lt;td&gt;Potentially higher&lt;/td&gt;
      &lt;td&gt;Very low&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Dynamic personalization&lt;/td&gt;
      &lt;td&gt;Strong&lt;/td&gt;
      &lt;td&gt;Strong&lt;/td&gt;
      &lt;td&gt;Limited without client/server additions&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;CDN caching&lt;/td&gt;
      &lt;td&gt;Excellent for shell/assets&lt;/td&gt;
      &lt;td&gt;Possible but more complex&lt;/td&gt;
      &lt;td&gt;Excellent&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Highly interactive apps&lt;/td&gt;
      &lt;td&gt;Excellent&lt;/td&gt;
      &lt;td&gt;Excellent after client JS&lt;/td&gt;
      &lt;td&gt;Requires client JS for interaction&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Content-heavy sites&lt;/td&gt;
      &lt;td&gt;Possible&lt;/td&gt;
      &lt;td&gt;Strong&lt;/td&gt;
      &lt;td&gt;Excellent when content changes infrequently&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;But modern frameworks don’t force you to choose one strategy for your entire application, and that’s where things get more interesting.&lt;/p&gt;





&lt;h2 id="modern-applications-are-hybrid"&gt;Modern Applications Are Hybrid&lt;/h2&gt;

&lt;p&gt;Imagine an e-commerce application. Its homepage contains marketing content that changes once per day, its product pages change when inventory or prices change, its shopping cart is specific to each user, and its account dashboard requires authentication and real-time information.&lt;/p&gt;

&lt;p&gt;Why should all four parts use exactly the same rendering strategy? They probably shouldn’t.&lt;/p&gt;

&lt;p&gt;A modern architecture might look like:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Homepage
   │
   └── Static Generation

Product Page
   │
   └── Server Rendering / Cached Rendering

Shopping Cart
   │
   └── Client Interaction

Account Dashboard
   │
   └── Server + Client
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Instead of asking:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;CSR or SSR?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;modern frontend architecture increasingly asks:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;Which parts should run where?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;





&lt;h2 id="nextjs-and-hybrid-rendering"&gt;Next.js and Hybrid Rendering&lt;/h2&gt;

&lt;p&gt;This is one reason frameworks such as Next.js became popular: a single application can combine multiple rendering strategies.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Next.js Application
        │
        ├── Static pages
        │
        ├── Server-rendered pages
        │
        ├── Server components
        │
        └── Client components
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;A mostly static marketing page doesn’t need the same architecture as an interactive dashboard. Modern frameworks allow developers to make those decisions at a much smaller granularity, and the boundary between “frontend” and “backend” becomes less rigid.&lt;/p&gt;





&lt;h2 id="server-components-add-another-dimension"&gt;Server Components Add Another Dimension&lt;/h2&gt;

&lt;p&gt;Traditional SSR generally works like:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Server
  │
  ▼
HTML
  │
  ▼
Browser
  │
  ▼
Hydrate JavaScript
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Server Components introduce a different idea: some components execute only on the server and don’t need their component JavaScript shipped to the browser.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Page
│
├── ProductDetails
│      Server
│
├── Reviews
│      Server
│
└── AddToCartButton
       Client
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The interactive button needs browser JavaScript, but the static product description may not.&lt;/p&gt;

&lt;p&gt;That means instead of sending JavaScript for everything:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Browser

ProductDetails JS
Reviews JS
Button JS
Navigation JS
Footer JS
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;you can potentially send client-side JavaScript only where interaction requires it.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Browser

Button JS
Navigation JS
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This is another example of the industry moving away from the idea that an entire application must belong exclusively to either the client or the server.&lt;/p&gt;





&lt;h2 id="the-javascript-cost-still-matters"&gt;The JavaScript Cost Still Matters&lt;/h2&gt;

&lt;p&gt;In the previous article, we explored the JavaScript event loop and why long-running JavaScript can freeze the UI, and that knowledge matters here.&lt;/p&gt;

&lt;p&gt;Suppose SSR delivers content almost instantly. Great. But then the browser downloads:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;2.5 MB JavaScript
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;and spends significant time parsing and executing it.&lt;/p&gt;

&lt;p&gt;The user may see the page quickly but still struggle to interact with it.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;HTML arrives
     │
     ▼
Content visible
     │
     ▼
Large JS bundle
     │
     ▼
Parse + Execute
     │
     ▼
Hydration
     │
     ▼
Interactive
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This is why SSR isn’t a magical performance switch. You need to think about both:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;How quickly can users SEE the page?

and

How quickly can users USE the page?
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Those are related but different questions.&lt;/p&gt;





&lt;h2 id="data-fetching-changes-too"&gt;Data Fetching Changes Too&lt;/h2&gt;

&lt;p&gt;CSR often produces a pattern like:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Browser
   │
   ▼
Load Application
   │
   ▼
Application Starts
   │
   ▼
Fetch /api/user
   │
   ▼
Fetch /api/orders
   │
   ▼
Render
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;With server rendering, data can often be fetched before HTML is sent.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Browser
   │
   ▼
Server
   │
   ├── Fetch User
   ├── Fetch Orders
   │
   ▼
Render HTML
   │
   ▼
Browser
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This can eliminate some client-side waterfalls and can also allow the server to access internal services or databases without exposing those credentials or endpoints directly to the browser.&lt;/p&gt;

&lt;p&gt;But now server rendering depends on those services, and if one is slow, the page may become slow. Again, architecture moves trade-offs around; it rarely eliminates them.&lt;/p&gt;





&lt;h2 id="caching-changes-the-equation"&gt;Caching Changes the Equation&lt;/h2&gt;

&lt;p&gt;Suppose 100,000 people request the same product page. If you dynamically server-render the page 100,000 times:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;100,000 Requests
       │
       ▼
100,000 Renders
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;that could be wasteful.&lt;/p&gt;

&lt;p&gt;If the response can safely be cached:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;First Request
     │
     ▼
Render
     │
     ▼
Cache
     │
     ├────► User
     ├────► User
     ├────► User
     └────► User
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;the economics change dramatically.&lt;/p&gt;

&lt;p&gt;Caching can make server-generated content behave much more like static content from an infrastructure perspective, which is why rendering strategy and caching strategy should often be designed together.&lt;/p&gt;





&lt;h2 id="personalized-pages-are-different"&gt;Personalized Pages Are Different&lt;/h2&gt;

&lt;p&gt;Now imagine:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;/dashboard
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Billy sees:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Welcome Billy

Account Balance: ...
Recent Orders: ...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Alice sees completely different data.&lt;/p&gt;

&lt;p&gt;Caching the entire HTML response globally is dangerous because responses are user-specific.&lt;/p&gt;

&lt;p&gt;You now need to consider:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Authentication
Personalization
Cache boundaries
Data privacy
Server rendering cost
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This doesn’t mean SSR is wrong. It means personalized SSR has a different operational profile from rendering a public blog article.&lt;/p&gt;





&lt;h2 id="failure-modes-are-different"&gt;Failure Modes Are Different&lt;/h2&gt;

&lt;p&gt;Suppose a pure CSR application loads successfully but its API fails. The browser may still have the application shell.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Application
    │
    ▼
API fails
    │
    ▼
Show error state
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;With SSR, if the server cannot obtain critical data, it may be unable to generate the page.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Request
   │
   ▼
Server
   │
   ▼
Data dependency fails
   │
   ▼
Page rendering fails
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;You therefore need to think about:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Timeouts&lt;/li&gt;
  &lt;li&gt;Partial rendering&lt;/li&gt;
  &lt;li&gt;Error boundaries&lt;/li&gt;
  &lt;li&gt;Fallback content&lt;/li&gt;
  &lt;li&gt;Retries&lt;/li&gt;
  &lt;li&gt;Caching stale data&lt;/li&gt;
  &lt;li&gt;Graceful degradation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Rendering architecture isn’t only a performance decision; it’s also a reliability decision.&lt;/p&gt;





&lt;h2 id="what-about-navigation-after-the-first-page"&gt;What About Navigation After the First Page?&lt;/h2&gt;

&lt;p&gt;Another common misconception is:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;SSR means every click reloads the entire page.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That doesn’t have to be true. Modern SSR frameworks frequently combine server-rendered initial requests with client-side navigation.&lt;/p&gt;

&lt;p&gt;The first request might look like:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Browser
   │
   ▼
Server
   │
   ▼
Rendered Page
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Then subsequent navigation can behave more like:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Current Application
      │
      ▼
Click /products/42
      │
      ▼
Fetch required data/content
      │
      ▼
Update interface
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This hybrid behavior allows applications to combine fast initial content with smooth navigation, and again, the boundary is becoming less binary.&lt;/p&gt;





&lt;h2 id="when-should-you-choose-csr"&gt;When Should You Choose CSR?&lt;/h2&gt;

&lt;p&gt;CSR remains an excellent choice when the application is primarily an interactive tool rather than publicly indexed content.&lt;/p&gt;

&lt;p&gt;Examples include:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Admin dashboards
Internal business tools
Analytics platforms
Complex editors
Authenticated SaaS applications
Project management tools
Email-like applications
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;CSR can be particularly attractive when users remain inside the application for long sessions, since the initial loading cost is paid once and navigation and interactions can then happen entirely within the application shell.&lt;/p&gt;

&lt;p&gt;You should still care about bundle size, rendering performance, caching, and code splitting, but SSR isn’t automatically necessary simply because it exists.&lt;/p&gt;





&lt;h2 id="when-should-you-choose-ssr"&gt;When Should You Choose SSR?&lt;/h2&gt;

&lt;p&gt;SSR becomes particularly attractive when initial content matters immediately.&lt;/p&gt;

&lt;p&gt;Examples include:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;E-commerce product pages
News websites
Public profiles
Search result pages
Content platforms
Pages requiring request-time personalization
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;It can help when:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Content needs to be available in the initial HTML.&lt;/li&gt;
  &lt;li&gt;SEO is important.&lt;/li&gt;
  &lt;li&gt;Social previews matter.&lt;/li&gt;
  &lt;li&gt;Initial rendering shouldn’t depend entirely on client JavaScript.&lt;/li&gt;
  &lt;li&gt;Data must be fresh at request time.&lt;/li&gt;
  &lt;li&gt;Server-side access to data simplifies the architecture.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But SSR comes with server complexity and shouldn’t be adopted merely because a framework makes it easy.&lt;/p&gt;





&lt;h2 id="when-should-you-choose-ssg"&gt;When Should You Choose SSG?&lt;/h2&gt;

&lt;p&gt;Static generation is extremely powerful when content doesn’t need to be regenerated for every request.&lt;/p&gt;

&lt;p&gt;Examples include:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Blogs
Documentation
Marketing pages
Portfolios
Product documentation
Company websites
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The generated HTML can be distributed through a CDN.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;                 ┌── London
                 │
Origin ─── CDN ──┼── Nairobi
                 │
                 ├── New York
                 │
                 └── Singapore
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Users receive files from infrastructure close to them without requiring dynamic rendering for every request. For content that changes relatively infrequently, it’s difficult to beat the simplicity and performance characteristics of static HTML.&lt;/p&gt;





&lt;h2 id="the-wrong-question-which-is-faster"&gt;The Wrong Question: “Which Is Faster?”&lt;/h2&gt;

&lt;p&gt;Developers often ask:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Is CSR or SSR faster?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That’s too broad. Consider two applications.&lt;/p&gt;

&lt;h4 id="application-a"&gt;Application A&lt;/h4&gt;

&lt;p&gt;CSR dashboard:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Small bundle
Fast API
Good caching
Code splitting
Long authenticated sessions
&lt;/code&gt;&lt;/pre&gt;

&lt;h4 id="application-b"&gt;Application B&lt;/h4&gt;

&lt;p&gt;SSR website:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Slow server
Slow database
No caching
Huge hydration bundle
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Application A may easily provide the better experience. Now reverse the conditions: a massive client-rendered marketing website might perform poorly compared with a mostly static or server-rendered equivalent. Rendering strategy doesn’t determine performance by itself; implementation matters.&lt;/p&gt;





&lt;h2 id="a-better-decision-framework"&gt;A Better Decision Framework&lt;/h2&gt;

&lt;p&gt;Instead of asking:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;CSR or SSR?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Ask a series of smaller questions.&lt;/p&gt;

&lt;h4 id="does-the-content-need-seo"&gt;Does the content need SEO?&lt;/h4&gt;

&lt;p&gt;If yes, delivering meaningful HTML directly is often useful.&lt;/p&gt;

&lt;h4 id="is-the-page-highly-personalized"&gt;Is the page highly personalized?&lt;/h4&gt;

&lt;p&gt;Dynamic server rendering or client-side fetching may make sense depending on the data.&lt;/p&gt;

&lt;h4 id="does-the-content-change-frequently"&gt;Does the content change frequently?&lt;/h4&gt;

&lt;p&gt;If not, static generation may be sufficient.&lt;/p&gt;

&lt;h4 id="is-the-application-highly-interactive"&gt;Is the application highly interactive?&lt;/h4&gt;

&lt;p&gt;Client-side JavaScript will likely play a significant role regardless of how the initial HTML is produced.&lt;/p&gt;

&lt;h4 id="is-the-page-mostly-content"&gt;Is the page mostly content?&lt;/h4&gt;

&lt;p&gt;Avoid shipping a large JavaScript application if simple HTML can solve the problem.&lt;/p&gt;

&lt;h4 id="can-the-output-be-cached"&gt;Can the output be cached?&lt;/h4&gt;

&lt;p&gt;If yes, SSR or static generation can become significantly cheaper.&lt;/p&gt;

&lt;h4 id="does-the-user-stay-in-the-application-for-a-long-time"&gt;Does the user stay in the application for a long time?&lt;/h4&gt;

&lt;p&gt;Paying an initial CSR cost may be perfectly reasonable for a long-lived application session.&lt;/p&gt;

&lt;p&gt;Architecture should emerge from these answers.&lt;/p&gt;





&lt;h2 id="common-mistake-ssr-everything"&gt;Common Mistake: SSR Everything&lt;/h2&gt;

&lt;p&gt;Once developers discover SSR, it’s tempting to move everything to the server, but that’s not always an improvement.&lt;/p&gt;

&lt;p&gt;Imagine an internal drag-and-drop project management application.&lt;/p&gt;

&lt;p&gt;It has:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Real-time updates&lt;/li&gt;
  &lt;li&gt;Rich client state&lt;/li&gt;
  &lt;li&gt;Dragging&lt;/li&gt;
  &lt;li&gt;Filtering&lt;/li&gt;
  &lt;li&gt;Modals&lt;/li&gt;
  &lt;li&gt;Optimistic updates&lt;/li&gt;
  &lt;li&gt;Keyboard shortcuts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Trying to make every interaction server-driven could introduce unnecessary network latency and complexity.&lt;/p&gt;

&lt;p&gt;Some things belong naturally in the browser. The browser isn’t merely a dumb HTML viewer anymore; it’s an extremely capable application runtime. Use it where it makes sense.&lt;/p&gt;





&lt;h2 id="common-mistake-csr-everything"&gt;Common Mistake: CSR Everything&lt;/h2&gt;

&lt;p&gt;The opposite extreme created many of the problems that caused SSR to regain popularity. A simple marketing page doesn’t necessarily need:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;React
+
Router
+
State Library
+
API Layer
+
500 KB JavaScript
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;just to display:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Company Name
Product Description
Pricing
Contact Form
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Sometimes HTML is enough. One of the signs of frontend engineering maturity is understanding that &lt;strong&gt;more JavaScript isn’t automatically more modern&lt;/strong&gt;.&lt;/p&gt;





&lt;h2 id="common-mistake-choosing-based-on-framework-hype"&gt;Common Mistake: Choosing Based on Framework Hype&lt;/h2&gt;

&lt;p&gt;A framework may strongly encourage a particular rendering model, but that doesn’t mean every application requires it.&lt;/p&gt;

&lt;p&gt;Technology discussions often become:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;"SSR is the future."

"SPAs are dead."

"Everything should be server components."

"Everything should be static."

&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Software architecture rarely works in absolutes, and every rendering strategy optimizes for different constraints.&lt;/p&gt;

&lt;p&gt;A better engineering question is:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;What does this particular page need?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Not even:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;What does this application need?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Different pages inside the same application may deserve different answers.&lt;/p&gt;





&lt;h2 id="a-practical-architecture"&gt;A Practical Architecture&lt;/h2&gt;

&lt;p&gt;Imagine we’re building an online learning platform. It contains:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Homepage
Courses
Course Details
Student Dashboard
Interactive Code Editor
Documentation
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;We don’t need one rendering strategy.&lt;/p&gt;

&lt;p&gt;We might choose:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Homepage
   │
   └── Static

Documentation
   │
   └── Static

Course Details
   │
   └── Server Rendered / Cached

Student Dashboard
   │
   └── Server + Client

Code Editor
   │
   └── Heavily Client-Side
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Now the architecture follows the product instead of forcing the product into one architectural philosophy, and that’s increasingly what modern frontend engineering looks like.&lt;/p&gt;





&lt;h2 id="csr-and-ssr-are-not-opponents"&gt;CSR and SSR Are Not Opponents&lt;/h2&gt;

&lt;p&gt;It’s easy to discuss these approaches as if they’re competing technologies.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;CSR

     VS.

SSR
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;But modern applications often look more like:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;                    Application
                         │
          ┌──────────────┼──────────────┐
          │              │              │
          ▼              ▼              ▼
       Static          Server         Client
       Content        Rendering     Interaction
          │              │              │
          └──────────────┼──────────────┘
                         │
                         ▼
                    User Experience
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The server can produce the initial document, the browser can take over interaction, and some components can remain server-only while others execute entirely on the client. Some pages can be generated days before anyone requests them, while others are generated for each request.&lt;/p&gt;

&lt;p&gt;The question is no longer:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;Where does my frontend run?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Increasingly, the answer is:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;Wherever each piece makes the most sense.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;





&lt;h2 id="bringing-it-all-together"&gt;Bringing It All Together&lt;/h2&gt;

&lt;p&gt;Client-Side Rendering and Server-Side Rendering solve the same fundamental problem differently. Both ultimately need to produce something the browser can display.&lt;/p&gt;

&lt;p&gt;CSR says:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Send the application to the browser
and let the browser construct the UI.
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;SSR says:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Construct the initial UI on the server
and send the browser meaningful HTML.
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;SSG goes one step further:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Construct the UI before the user
even requests it.
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;And modern hybrid frameworks say:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Why choose only one?
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The trade-offs can be summarized like this:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;                 WHERE IS THE UI CREATED?

Build Time             Server               Browser
    │                    │                     │
    ▼                    ▼                     ▼
   SSG                   SSR                   CSR

Fast static        Fresh request-time      Rich client-side
delivery           HTML                    applications
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;None is universally correct. The right choice depends on:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Content
Performance
Interactivity
SEO
Personalization
Caching
Infrastructure
User experience
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Understanding those trade-offs is far more valuable than memorizing which rendering strategy is currently fashionable.&lt;/p&gt;





&lt;h2 id="final-thoughts"&gt;Final Thoughts&lt;/h2&gt;

&lt;p&gt;Frontend development spent years moving more work into the browser, then the industry rediscovered the advantages of moving some of that work back to the server. That can make it look as though we’ve gone in a circle, but we haven’t.&lt;/p&gt;

&lt;p&gt;What changed is that we now have much finer control over &lt;strong&gt;where different parts of an application execute&lt;/strong&gt;. We can generate a blog post at build time, render a personalized account page on the server, run an interactive editor in the browser, cache a product page at the edge, and combine all of those approaches inside the same product.&lt;/p&gt;

&lt;p&gt;The most useful lesson isn’t that SSR is better than CSR, or that CSR is simpler than SSR. It’s this:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;Rendering is an architectural decision, not a framework preference.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Once you understand where the work happens, what it costs, and what the user needs, the choice becomes much easier. Server rendering also introduces one particularly interesting problem: the server can send a page that &lt;strong&gt;looks interactive before the browser has actually made it interactive&lt;/strong&gt;. Understanding what happens during that transition brings us to our next topic.&lt;/p&gt;





&lt;h2 id="whats-next"&gt;What’s Next?&lt;/h2&gt;

&lt;p&gt;In the next article in &lt;strong&gt;Beyond the UI&lt;/strong&gt;, we’ll explore:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;Hydration Explained: How Server-Rendered Pages Become Interactive&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;We’ll follow a server-rendered component from the server to the browser and see how frameworks attach JavaScript behavior to HTML that already exists. We’ll also explore hydration mismatches, why hydration can be expensive, partial and selective hydration, and why newer frontend architectures are trying to ship less JavaScript to the browser.&lt;/p&gt;

&lt;p&gt;Because after the server has created your HTML, there’s still one important question:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;How does that static HTML become a living application?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>frontend</category>
      <category>performance</category>
    </item>
    <item>
      <title>The JavaScript Event Loop Explained: Why Your UI Freezes</title>
      <dc:creator>Billy Okeyo</dc:creator>
      <pubDate>Mon, 24 Aug 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/billy_de_cartel/the-javascript-event-loop-explained-why-your-ui-freezes-2k4c</link>
      <guid>https://dev.to/billy_de_cartel/the-javascript-event-loop-explained-why-your-ui-freezes-2k4c</guid>
      <description>&lt;blockquote&gt;
  &lt;p&gt;&lt;em&gt;“Your browser isn’t frozen because JavaScript stopped working. Sometimes it’s frozen because JavaScript won’t stop working long enough for the browser to do anything else.”&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In the previous articles in &lt;strong&gt;Beyond the UI&lt;/strong&gt;, we followed the browser rendering pipeline from HTML to pixels and explored why reflow and repaint can make certain UI updates expensive. But there’s another reason an interface can feel slow even when rendering itself isn’t particularly complicated: JavaScript.&lt;/p&gt;

&lt;p&gt;Consider this button:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;lt;button id="generate"&amp;gt;
    Generate Report
&amp;lt;/button&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;When clicked, it performs some expensive work.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;document
    .querySelector("#generate")
    .addEventListener("click", () =&amp;gt; {
        performExpensiveCalculation();
    });
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The user clicks the button, and suddenly everything stops. The button doesn’t respond visually, animations freeze, scrolling becomes unresponsive, and other clicks don’t work. Even a loading spinner you tried to display may refuse to appear. Then, a few seconds later, everything suddenly comes back to life.&lt;/p&gt;

&lt;p&gt;What happened? The browser didn’t crash, the network wasn’t necessarily slow, and the rendering pipeline wasn’t necessarily doing too much work. The problem was that JavaScript occupied the browser’s &lt;strong&gt;main thread&lt;/strong&gt; for too long.&lt;/p&gt;

&lt;p&gt;To understand why that freezes the interface, we need to understand one of the most important concepts in JavaScript:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;The Event Loop&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;At a high level, JavaScript execution in the browser involves several moving parts:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;JavaScript Code
      │
      ▼
  Call Stack
      │
      ▼
Browser / Web APIs
      │
      ▼
 Task Queues
      │
      ▼
  Event Loop
      │
      └────────────► Call Stack
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;But that diagram hides some important details. There are tasks, microtasks, timers, promises, and rendering, and all of them need opportunities to run. Let’s unpack what actually happens.&lt;/p&gt;





&lt;h2 id="javascript-is-single-threaded"&gt;JavaScript Is Single-Threaded&lt;/h2&gt;

&lt;p&gt;One of the first things developers learn about JavaScript is that it is &lt;strong&gt;single-threaded&lt;/strong&gt;. At least from the perspective of normal JavaScript execution on the browser’s main thread, only one piece of JavaScript executes at a time.&lt;/p&gt;

&lt;p&gt;Consider:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;console.log("One");
console.log("Two");
console.log("Three");
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The result is predictable:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;One
Two
Three
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;JavaScript doesn’t normally execute all three statements simultaneously. Instead, execution happens one operation at a time. You can think of it like a single cashier serving customers.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Customer A
    │
    ▼
┌──────────┐
│ Cashier  │
└──────────┘
    ▲
    │
Customer B
    │
Customer C
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The cashier can serve only one customer at a time. If Customer A takes ten minutes, Customers B and C wait.&lt;/p&gt;

&lt;p&gt;The same basic problem exists with JavaScript. If one function takes five seconds to complete, other JavaScript can’t run on that thread during those five seconds. More importantly, much of the browser’s UI work also depends on the main thread getting time to operate. That is where freezing begins.&lt;/p&gt;





&lt;h2 id="the-call-stack"&gt;The Call Stack&lt;/h2&gt;

&lt;p&gt;JavaScript keeps track of currently executing functions using something called the &lt;strong&gt;call stack&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Consider:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;function greet() {
    console.log("Hello");
}

function start() {
    greet();
}

start();
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Execution begins with &lt;code&gt;start()&lt;/code&gt;. Conceptually:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Call Stack

┌──────────────┐
│   start()    │
└──────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;code&gt;start()&lt;/code&gt; calls &lt;code&gt;greet()&lt;/code&gt;. Now:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Call Stack

┌──────────────┐
│   greet()    │
├──────────────┤
│   start()    │
└──────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;code&gt;greet()&lt;/code&gt; runs and finishes, then leaves the stack.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;┌──────────────┐
│   start()    │
└──────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Then &lt;code&gt;start()&lt;/code&gt; finishes, and the stack becomes empty.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Call Stack

     empty
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This matters because queued asynchronous work cannot simply interrupt JavaScript that is already executing. The event loop generally waits for the current task to finish and the call stack to become available before scheduling more work.&lt;/p&gt;





&lt;h2 id="so-how-does-asynchronous-javascript-work"&gt;So How Does Asynchronous JavaScript Work?&lt;/h2&gt;

&lt;p&gt;This creates an obvious question: if JavaScript executes one thing at a time, how can this work?&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;setTimeout(() =&amp;gt; {
    console.log("Finished");
}, 2000);
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Surely JavaScript doesn’t sit on the call stack for two seconds doing nothing. It doesn’t. The browser provides capabilities outside the JavaScript engine that can handle operations such as timers, networking, and user events. We often refer to these capabilities collectively as &lt;strong&gt;Web APIs&lt;/strong&gt; or browser APIs.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;JavaScript
    │
    │ setTimeout(...)
    ▼
Browser Timer
    │
    │ waits independently
    ▼
Callback becomes eligible
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;JavaScript registers the timer and continues executing. The browser tracks the timer. Once the timer expires, its callback becomes eligible to run later.&lt;/p&gt;

&lt;p&gt;That distinction is important. The callback doesn’t necessarily run immediately when the timer expires. It has to wait until JavaScript is able to execute it.&lt;/p&gt;





&lt;h2 id="why-settimeout-0-doesnt-mean-immediately"&gt;Why &lt;code&gt;setTimeout(..., 0)&lt;/code&gt; Doesn’t Mean Immediately&lt;/h2&gt;

&lt;p&gt;Consider:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;console.log("A");

setTimeout(() =&amp;gt; {
    console.log("B");
}, 0);

console.log("C");
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Some developers initially expect:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;A
B
C
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;But the result is:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;A
C
B
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Why? Because &lt;code&gt;setTimeout(..., 0)&lt;/code&gt; doesn’t mean:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Run this function immediately.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It means something closer to:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;After at least the timer delay and once scheduling permits, make this callback available to run as a future task.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So execution looks roughly like:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;console.log("A")
        │
        ▼
Register timer
        │
        ▼
console.log("C")
        │
        ▼
Current task finishes
        │
        ▼
Timer callback can run
        │
        ▼
console.log("B")
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The timer delay controls when the callback becomes eligible. It doesn’t guarantee exactly when it executes. If the main thread is busy, it may execute much later.&lt;/p&gt;





&lt;h2 id="tasks-and-the-task-queue"&gt;Tasks and the Task Queue&lt;/h2&gt;

&lt;p&gt;When asynchronous work becomes ready, the browser needs somewhere to keep track of it until JavaScript can execute it. One important category of queued work is commonly called &lt;strong&gt;tasks&lt;/strong&gt;. You may also hear the older term &lt;strong&gt;macrotasks&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Examples include work associated with things such as:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Timer callbacks&lt;/li&gt;
  &lt;li&gt;User interactions&lt;/li&gt;
  &lt;li&gt;Message events&lt;/li&gt;
  &lt;li&gt;Certain browser events&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Conceptually:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Task Queue

┌─────────────────────────┐
│ click callback          │
├─────────────────────────┤
│ setTimeout callback     │
├─────────────────────────┤
│ message callback        │
└─────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The event loop coordinates when this queued work gets an opportunity to execute. A simplified model is:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Is JavaScript currently running?
          │
     ┌────┴────┐
     │         │
    Yes        No
     │         │
    Wait       ▼
          Take eligible task
               │
               ▼
          Execute JavaScript
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;But this is still incomplete, because JavaScript has another important queue.&lt;/p&gt;





&lt;h2 id="enter-microtasks"&gt;Enter Microtasks&lt;/h2&gt;

&lt;p&gt;Promises introduce another category of work called &lt;strong&gt;microtasks&lt;/strong&gt;. Consider:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;console.log("A");

Promise.resolve().then(() =&amp;gt; {
    console.log("B");
});

console.log("C");
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The result is:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;A
C
B
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;That looks similar to &lt;code&gt;setTimeout&lt;/code&gt;. But now watch what happens when we combine them.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;console.log("A");

setTimeout(() =&amp;gt; {
    console.log("B");
}, 0);

Promise.resolve().then(() =&amp;gt; {
    console.log("C");
});

console.log("D");
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The output is:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;A
D
C
B
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Why does the promise callback run before the timer? Because promise reactions are scheduled as &lt;strong&gt;microtasks&lt;/strong&gt;, while the timer callback is scheduled as a later task. After the current JavaScript task finishes, the browser drains the microtask queue before moving on to the next task.&lt;/p&gt;

&lt;p&gt;A simplified ordering looks like:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Current Task
     │
     ▼
JavaScript executes
     │
     ▼
Current stack finishes
     │
     ▼
Drain Microtasks
     │
     ▼
Rendering may get an opportunity
     │
     ▼
Next Task
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;So in our example:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;A
│
├── Timer scheduled ──────────────► Task Queue
│
├── Promise callback ─────────────► Microtask Queue
│
D
│
▼
Current task ends
│
▼
Microtasks
│
└── C
│
▼
Later task
│
└── B
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This distinction between tasks and microtasks explains a surprising amount of JavaScript behavior.&lt;/p&gt;





&lt;h2 id="what-creates-microtasks"&gt;What Creates Microtasks?&lt;/h2&gt;

&lt;p&gt;Common sources include:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Promise.resolve().then(...)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;and code after an awaited promise:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;async function load() {
    await fetchData();

    console.log("Finished");
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The continuation after &lt;code&gt;await&lt;/code&gt; eventually resumes through promise machinery and is scheduled as a microtask when the awaited promise settles.&lt;/p&gt;

&lt;p&gt;Another browser API is:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;queueMicrotask(() =&amp;gt; {
    console.log("Microtask");
});
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;These are useful tools, but microtasks have an important characteristic that can become dangerous:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;The browser drains the microtask queue before moving on.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;





&lt;h2 id="microtask-starvation"&gt;Microtask Starvation&lt;/h2&gt;

&lt;p&gt;Consider:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;function repeat() {
    queueMicrotask(repeat);
}

repeat();
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Each microtask creates another microtask.&lt;/p&gt;

&lt;p&gt;The browser finishes one:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Microtask 1
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;but another already exists:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Microtask 2
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;which creates:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Microtask 3
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;and so on.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Task finishes
     │
     ▼
Microtask
     │
     ▼
Creates Microtask
     │
     ▼
Microtask
     │
     ▼
Creates Microtask
     │
     ▼
...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;If this continues indefinitely, the browser may struggle to move on to other work. This is called &lt;strong&gt;starvation&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Promises are asynchronous, but that doesn’t automatically mean promise-heavy code can never block responsiveness. “Asynchronous” and “runs on another thread” are not the same thing. That’s an important distinction.&lt;/p&gt;





&lt;h2 id="why-your-ui-freezes"&gt;Why Your UI Freezes&lt;/h2&gt;

&lt;p&gt;Now we can finally return to our original problem. Suppose the user clicks a button and you run:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;button.addEventListener("click", () =&amp;gt; {
    let total = 0;

    for (let i = 0; i &amp;lt; 5_000_000_000; i++) {
        total += i;
    }

    result.textContent = total;
});
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Once that click handler begins executing, the main thread is occupied.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Main Thread

Click Handler
     │
     ▼
Huge Loop
     │
     │
     │  3 seconds
     │
     │
     ▼
Finished
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;During that time, the browser may have other work waiting.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;User scroll
      │
      ├─────────────┐
Button click        │
      │             │
Animation frame     │
      │             ▼
      └──────► WAITING

         Main Thread

      ┌───────────────┐
      │ Expensive JS  │
      │ Expensive JS  │
      │ Expensive JS  │
      │ Expensive JS  │
      └───────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The page appears frozen because JavaScript isn’t giving the browser enough opportunity to process interactions and produce frames. This is known as a &lt;strong&gt;long task&lt;/strong&gt;.&lt;/p&gt;





&lt;h2 id="the-loading-spinner-that-never-spins"&gt;The Loading Spinner That Never Spins&lt;/h2&gt;

&lt;p&gt;Here’s a classic example. You want to show a loading state before performing expensive work.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;button.addEventListener("click", () =&amp;gt; {
    spinner.style.display = "block";

    performExpensiveCalculation();

    spinner.style.display = "none";
});
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Logically, this seems correct: show the spinner, do the work, then hide the spinner. But the user may never see the spinner. Why?&lt;/p&gt;

&lt;p&gt;Changing:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;spinner.style.display = "block";
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;updates the DOM/style state, but the browser doesn’t necessarily paint the screen immediately after that line. Your JavaScript continues running.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Show spinner
     │
     ▼
Expensive JavaScript
     │
     ▼
Hide spinner
     │
     ▼
Task finishes
     │
     ▼
Browser finally gets chance to render
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;By the time rendering gets an opportunity, the spinner has already been hidden again. From the user’s perspective, it never appeared.&lt;/p&gt;

&lt;p&gt;This connects directly to what we learned in the previous articles:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;JavaScript execution and rendering must cooperate on the main thread.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Understanding the rendering pipeline alone isn’t enough. We also need to understand &lt;strong&gt;when the browser gets an opportunity to run it&lt;/strong&gt;.&lt;/p&gt;





&lt;h2 id="rendering-and-the-event-loop"&gt;Rendering and the Event Loop&lt;/h2&gt;

&lt;p&gt;The browser’s actual scheduling model is sophisticated, and different kinds of work are coordinated according to browser rules. But a useful mental model is:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;┌─────────────────────────────┐
│ Execute a Task              │
└──────────────┬──────────────┘
               │
               ▼
┌─────────────────────────────┐
│ Drain Microtasks            │
└──────────────┬──────────────┘
               │
               ▼
┌─────────────────────────────┐
│ Rendering opportunity       │
│ if needed / appropriate     │
└──────────────┬──────────────┘
               │
               ▼
          Next iteration
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The key idea is that the browser generally cannot just paint halfway through your long-running synchronous JavaScript function. Your code needs to yield control.&lt;/p&gt;





&lt;h2 id="breaking-expensive-work-into-smaller-pieces"&gt;Breaking Expensive Work Into Smaller Pieces&lt;/h2&gt;

&lt;p&gt;Suppose we need to process one million records. Instead of:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;function processRecords(records) {
    for (const record of records) {
        expensiveOperation(record);
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;we could process them in chunks.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;function processInChunks(records) {
    let index = 0;

    function processChunk() {
        const end = Math.min(index + 1000, records.length);

        while (index &amp;lt; end) {
            expensiveOperation(records[index]);
            index++;
        }

        if (index &amp;lt; records.length) {
            setTimeout(processChunk, 0);
        }
    }

    processChunk();
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Instead of one giant task:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;┌──────────────────────────────────────────────┐
│              3000ms JavaScript              │
└──────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;we create smaller pieces:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;JS     Browser     JS     Browser     JS
│         │         │         │        │
▼         ▼         ▼         ▼        ▼

20ms     work      20ms      work     20ms
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The total computation may not become dramatically smaller. But responsiveness can improve because the browser gets opportunities to process other work between chunks.&lt;/p&gt;

&lt;p&gt;This demonstrates a critical performance principle:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;Sometimes making an application feel faster isn’t about doing less work. It’s about scheduling the work more intelligently.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;





&lt;h2 id="what-about-async-and-await"&gt;What About &lt;code&gt;async&lt;/code&gt; and &lt;code&gt;await&lt;/code&gt;?&lt;/h2&gt;

&lt;p&gt;A common misconception is that adding &lt;code&gt;async&lt;/code&gt; automatically moves expensive work away from the main thread. It doesn’t.&lt;/p&gt;

&lt;p&gt;Consider:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;async function calculate() {
    let total = 0;

    for (let i = 0; i &amp;lt; 5_000_000_000; i++) {
        total += i;
    }

    return total;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Calling:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;await calculate();
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;doesn’t magically make that loop execute on another thread. The synchronous work inside &lt;code&gt;calculate()&lt;/code&gt; still runs on the JavaScript thread. &lt;code&gt;async&lt;/code&gt; changes how promises and continuations are handled. It doesn’t transform CPU-heavy JavaScript into background work.&lt;/p&gt;

&lt;p&gt;This:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;async function freezeUI() {
    while (true) {
        // expensive synchronous work
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;will still freeze your interface. &lt;code&gt;async&lt;/code&gt; is not a synonym for parallel.&lt;/p&gt;





&lt;h2 id="settimeout-isnt-a-background-thread-either"&gt;
&lt;code&gt;setTimeout&lt;/code&gt; Isn’t a Background Thread Either&lt;/h2&gt;

&lt;p&gt;The same misunderstanding often happens with:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;setTimeout(() =&amp;gt; {
    performExpensiveCalculation();
}, 0);
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This doesn’t move the expensive calculation to another thread. It simply schedules the callback to execute as a future task. When that callback eventually runs, the expensive calculation still occupies the main JavaScript thread.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Current Task
     │
     ▼
setTimeout scheduled
     │
     ▼
Current Task finishes
     │
     ▼
Timer Task starts
     │
     ▼
EXPENSIVE WORK
     │
     ▼
UI freezes
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;You’ve delayed the problem. You haven’t removed it. Chunking can help because each task is smaller, but truly CPU-intensive work may need a different solution.&lt;/p&gt;





&lt;h2 id="web-workers-actually-moving-work-off-the-main-thread"&gt;Web Workers: Actually Moving Work Off the Main Thread&lt;/h2&gt;

&lt;p&gt;Browsers provide &lt;strong&gt;Web Workers&lt;/strong&gt; for running JavaScript in a background thread separate from the main UI thread. Suppose you need to perform a large calculation. Instead of:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;const result = expensiveCalculation(data);
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;you can create a worker.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;const worker = new Worker("worker.js");

worker.postMessage(data);

worker.onmessage = event =&amp;gt; {
    console.log("Result:", event.data);
};
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Inside &lt;code&gt;worker.js&lt;/code&gt;:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;self.onmessage = event =&amp;gt; {
    const result = expensiveCalculation(event.data);

    self.postMessage(result);
};
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Now the architecture looks more like:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Main Thread                      Worker

UI
 │
 ├── User interactions
 │
 ├── Rendering
 │
 └── Send data ────────────────► Expensive calculation
                                      │
                                      │
                                      ▼
 Result received ◄──────────────── Result
 │
 ▼
Update UI
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The expensive computation no longer needs to monopolize the main thread. The interface can remain responsive while the worker performs the calculation.&lt;/p&gt;

&lt;p&gt;Workers aren’t appropriate for everything. There is communication overhead, data may need to be copied or transferred, and workers don’t directly manipulate the DOM. But for genuinely CPU-heavy operations, they can be extremely valuable.&lt;/p&gt;





&lt;h2 id="requestanimationframe"&gt;&lt;code&gt;requestAnimationFrame&lt;/code&gt;&lt;/h2&gt;

&lt;p&gt;Now suppose your JavaScript isn’t performing a large calculation. Instead, you’re animating something. You could write:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;setInterval(() =&amp;gt; {
    moveElement();
}, 16);
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;But the browser provides a better mechanism specifically for visual updates:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;requestAnimationFrame(() =&amp;gt; {
    moveElement();
});
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;code&gt;requestAnimationFrame&lt;/code&gt; tells the browser:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“I want to perform work before an upcoming repaint.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A typical animation looks like:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;let position = 0;

function animate() {
    position += 2;

    element.style.transform =
        `translateX(${position}px)`;

    requestAnimationFrame(animate);
}

requestAnimationFrame(animate);
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Conceptually:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Frame
 │
 ├── requestAnimationFrame callback
 │
 ├── Style / Layout
 │
 ├── Paint
 │
 └── Composite
 │
 ▼
Next Frame
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This allows your visual updates to align more naturally with the browser’s rendering cycle. It doesn’t mean you can perform unlimited work inside the callback. If your callback takes 100 milliseconds, you’ll still miss frames. &lt;code&gt;requestAnimationFrame&lt;/code&gt; gives you appropriate timing. It doesn’t give you unlimited processing power.&lt;/p&gt;





&lt;h2 id="tasks-vs-microtasks-vs-animation-frames"&gt;Tasks vs Microtasks vs Animation Frames&lt;/h2&gt;

&lt;p&gt;At this point, we have several scheduling mechanisms. A simplified comparison is useful.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Mechanism&lt;/th&gt;
      &lt;th&gt;Typical Use&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code&gt;setTimeout&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;Schedule future task&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Promise &lt;code&gt;.then()&lt;/code&gt;
&lt;/td&gt;
      &lt;td&gt;Promise continuation / microtask&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code&gt;queueMicrotask()&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;Explicit microtask&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code&gt;requestAnimationFrame()&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;Work tied to an upcoming visual frame&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Web Worker&lt;/td&gt;
      &lt;td&gt;CPU-heavy work away from main UI thread&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These mechanisms aren’t interchangeable. For example, using a chain of promises to split heavy work may not provide the rendering opportunities you expect because microtasks are drained before the browser moves on.&lt;/p&gt;

&lt;p&gt;Consider:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;function process() {
    Promise.resolve().then(process);
}

process();
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This can continually populate the microtask queue. If your goal is to yield so the browser can render, continuously scheduling microtasks may be exactly the wrong strategy. Understanding &lt;strong&gt;which queue your work enters&lt;/strong&gt; matters.&lt;/p&gt;





&lt;h2 id="a-practical-example-processing-a-large-dataset"&gt;A Practical Example: Processing a Large Dataset&lt;/h2&gt;

&lt;p&gt;Suppose a dashboard receives 100,000 records and needs to calculate statistics. The naive approach:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;button.addEventListener("click", () =&amp;gt; {
    const result = calculateStatistics(records);

    renderResults(result);
});
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;If the calculation takes two seconds:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Click
 │
 ▼
Calculate Statistics
 │
 │ 2 seconds
 │
 ▼
Render Results
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;the UI may become unresponsive. One option is chunking:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Chunk 1
   │
   ▼
Yield
   │
   ▼
Chunk 2
   │
   ▼
Yield
   │
   ▼
Chunk 3
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Another option, particularly for CPU-heavy computation, is a worker:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;                    ┌──────────────────┐
Records ───────────►│    Web Worker    │
                    │                  │
UI remains          │ Calculate stats  │
responsive          │                  │
                    └────────┬─────────┘
                             │
                             ▼
                           Result
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The right approach depends on the workload. But both are better than assuming &lt;code&gt;async&lt;/code&gt; will solve the problem automatically.&lt;/p&gt;





&lt;h2 id="long-tasks-and-the-167ms-frame-budget"&gt;Long Tasks and the 16.7ms Frame Budget&lt;/h2&gt;

&lt;p&gt;In the previous articles, we discussed the frame budget. On a 60 Hz display, the browser gets a new frame opportunity roughly every:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;1000ms / 60 ≈ 16.7ms
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Now imagine JavaScript runs for 200 milliseconds.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Frame budget

|----16.7ms----|

JavaScript

|----------------------------------------------------|
                       200ms
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Several potential frame opportunities pass while JavaScript is still executing. Animations stop updating smoothly, interactions feel delayed, and the page becomes janky.&lt;/p&gt;

&lt;p&gt;This is why long-running JavaScript matters even when the total amount of computation seems reasonable. Users experience responsiveness in small windows of time. A two-second calculation that completely blocks the interface feels much worse than work that can be performed without preventing interaction.&lt;/p&gt;





&lt;h2 id="the-event-loop-explains-delayed-clicks-too"&gt;The Event Loop Explains Delayed Clicks Too&lt;/h2&gt;

&lt;p&gt;Freezing isn’t always dramatic. Sometimes the interface simply feels slightly sluggish.&lt;/p&gt;

&lt;p&gt;Suppose the main thread is busy for 300 milliseconds. During that period, the user clicks a button. The click doesn’t disappear. It can wait until the browser is able to process the relevant event and run its handler.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;User clicks
     │
     ▼
Event waiting
     │
     │
     │ Main thread busy
     │
     ▼
JavaScript finishes
     │
     ▼
Click handler runs
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;From the user’s perspective:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“I clicked the button and nothing happened.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Then, a fraction of a second later, the interface responds. This is one reason JavaScript performance directly affects interaction responsiveness. It’s also why modern web performance metrics care about how quickly pages respond to user interactions, not merely how quickly they initially load.&lt;/p&gt;





&lt;h2 id="frameworks-still-depend-on-the-event-loop"&gt;Frameworks Still Depend on the Event Loop&lt;/h2&gt;

&lt;p&gt;Just as React cannot bypass the browser rendering pipeline, frameworks cannot bypass JavaScript scheduling.&lt;/p&gt;

&lt;p&gt;Consider:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;function App() {
    const handleClick = () =&amp;gt; {
        performHugeCalculation();
        setResult("Finished");
    };

    return (
        &amp;lt;button onClick={handleClick}&amp;gt;
            Calculate
        &amp;lt;/button&amp;gt;
    );
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;React can optimize how updates are reconciled. But if &lt;code&gt;performHugeCalculation()&lt;/code&gt; occupies the main thread for three seconds, React cannot magically make the browser responsive during that synchronous work. The same principle applies to Vue, Angular, Svelte, Solid, and every other browser-based framework.&lt;/p&gt;

&lt;p&gt;Eventually:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Framework Event Handler
          │
          ▼
      JavaScript
          │
          ▼
     Main Thread
          │
     ┌────┴─────┐
     │          │
Long work    Short work
     │          │
     ▼          ▼
UI blocked   Browser gets
             opportunities
             to continue
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Understanding the event loop is therefore framework-independent knowledge.&lt;/p&gt;





&lt;h2 id="common-event-loop-mistakes"&gt;Common Event Loop Mistakes&lt;/h2&gt;

&lt;p&gt;Several bugs and performance problems become easier to recognize once you understand scheduling. One is assuming this executes immediately:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;setTimeout(callback, 0);
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;It doesn’t. Another is assuming this moves CPU work off the main thread:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;async function work() {
    expensiveCalculation();
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;It doesn’t. Another is creating enormous promise or microtask chains and assuming that because they’re asynchronous, rendering will happen between each one. That isn’t necessarily true.&lt;/p&gt;

&lt;p&gt;And perhaps the most common mistake is simply doing too much synchronous work inside event handlers.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;button.addEventListener("click", () =&amp;gt; {
    parseHugeFile();
    calculateStatistics();
    transformRecords();
    generateReport();
    updateDashboard();
});
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Each individual function may look reasonable. Together, they can monopolize the main thread.&lt;/p&gt;





&lt;h2 id="how-to-keep-the-ui-responsive"&gt;How to Keep the UI Responsive&lt;/h2&gt;

&lt;p&gt;The goal isn’t to avoid JavaScript. It’s to cooperate with the browser.&lt;/p&gt;

&lt;p&gt;For large amounts of work, consider breaking processing into smaller chunks so the browser gets opportunities to handle other tasks. For CPU-heavy work that doesn’t need DOM access, consider Web Workers. For visual updates, use &lt;code&gt;requestAnimationFrame&lt;/code&gt; where appropriate. Avoid unnecessarily long synchronous event handlers. Be careful about creating endless microtask chains.&lt;/p&gt;

&lt;p&gt;And most importantly, measure. Browser developer tools can show long tasks, scripting time, rendering activity, and frame performance. A slow interface shouldn’t lead immediately to random optimization. First determine whether the bottleneck is:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;JavaScript execution?
        │
        ├── Long task?
        ├── Too many calculations?
        └── Excessive framework work?

Rendering?
        │
        ├── Layout?
        ├── Paint?
        └── Compositing?

Network?
        │
        └── Waiting for resources?
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Different problems require different solutions.&lt;/p&gt;





&lt;h2 id="putting-the-event-loop-together"&gt;Putting the Event Loop Together&lt;/h2&gt;

&lt;p&gt;We can now build a more complete mental model.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;                 Browser
                    │
       ┌────────────┴─────────────┐
       │                          │
       ▼                          ▼
  Browser APIs               User Events
       │                          │
       └────────────┬─────────────┘
                    │
                    ▼
                Task Queue
                    │
                    ▼
              ┌───────────┐
              │ Event Loop│
              └─────┬─────┘
                    │
                    ▼
               Call Stack
                    │
                    ▼
             JavaScript Runs
                    │
                    ▼
              Task Finishes
                    │
                    ▼
            Drain Microtasks
                    │
                    ▼
         Rendering Opportunity
                    │
                    ▼
               Next Work
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Again, the browser’s actual implementation is more sophisticated than this diagram. But as a mental model, it answers many practical questions.&lt;/p&gt;

&lt;p&gt;Why doesn’t &lt;code&gt;setTimeout(..., 0)&lt;/code&gt; run immediately? Because it has to wait for a future task opportunity.&lt;/p&gt;

&lt;p&gt;Why does a promise callback run before that timer? Because microtasks are processed before moving on to the next task.&lt;/p&gt;

&lt;p&gt;Why does a giant loop freeze the interface? Because JavaScript occupies the main thread.&lt;/p&gt;

&lt;p&gt;Why didn’t your spinner appear before the expensive calculation? Because the browser didn’t get a rendering opportunity.&lt;/p&gt;

&lt;p&gt;Why doesn’t &lt;code&gt;async&lt;/code&gt; solve CPU-heavy work? Because asynchronous syntax doesn’t automatically mean another thread.&lt;/p&gt;

&lt;p&gt;Why can Web Workers help? Because they allow computation to happen away from the main UI thread.&lt;/p&gt;





&lt;h2 id="bringing-it-all-together"&gt;Bringing It All Together&lt;/h2&gt;

&lt;p&gt;The JavaScript event loop isn’t merely an interview question. It’s the scheduling system behind much of the behavior users experience in web applications.&lt;/p&gt;

&lt;p&gt;When JavaScript performs small amounts of work and regularly gives control back to the browser, the interface remains responsive.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;JavaScript
    │
    ▼
Browser
    │
    ▼
JavaScript
    │
    ▼
Browser
    │
    ▼
JavaScript
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;But when one task monopolizes the main thread:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;JavaScript
    │
    │
    │
    │
    │
    │
    ▼
Finally finishes
    │
    ▼
Browser catches up
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;everything else has to wait. That’s the heart of the problem.&lt;/p&gt;

&lt;p&gt;A responsive frontend isn’t just about writing fast JavaScript. It’s about giving the browser enough opportunities to do everything &lt;strong&gt;other than JavaScript&lt;/strong&gt;.&lt;/p&gt;





&lt;h2 id="final-thoughts"&gt;Final Thoughts&lt;/h2&gt;

&lt;p&gt;The event loop can seem complicated because several concepts are usually introduced at once: the call stack, Web APIs, tasks, microtasks, promises, timers, rendering, and animation frames. But underneath all of them is a relatively simple idea.&lt;/p&gt;

&lt;p&gt;The browser has many responsibilities. It needs to run your JavaScript, respond to users, calculate layouts, paint pixels, animate interfaces, and process network results. And much of that work has to be coordinated around a main thread that can only do so much at once.&lt;/p&gt;

&lt;p&gt;If your JavaScript refuses to give that thread back, the browser cannot provide a smooth experience. That’s why the next time your interface freezes, the most useful question may not be:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;“Why is this function slow?”&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Instead, ask:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;“How long am I preventing the browser from doing anything else?”&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That question gets much closer to how users actually experience performance.&lt;/p&gt;





&lt;h2 id="whats-next"&gt;What’s Next?&lt;/h2&gt;

&lt;p&gt;So far in &lt;strong&gt;Beyond the UI&lt;/strong&gt;, we’ve looked underneath frontend frameworks at three fundamental browser concepts:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Browser Rendering Pipeline
          │
          ▼
Reflow and Repaint
          │
          ▼
JavaScript Event Loop
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;We now understand how the browser creates pixels, why some visual updates are expensive, and why JavaScript can prevent the UI from responding altogether.&lt;/p&gt;

&lt;p&gt;Next, we’ll move one level higher and look at a decision that shapes how modern frontend applications are delivered:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;Client-Side Rendering vs Server-Side Rendering Explained: Where Should Your UI Be Built?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;We’ll explore CSR, SSR, static generation, the trade-offs between them, what happens from the moment a user requests a page, and why frameworks such as Next.js, Nuxt, and modern meta-frameworks increasingly blur the line between client and server.&lt;/p&gt;

&lt;p&gt;Because once you understand &lt;strong&gt;how the browser renders&lt;/strong&gt;, the next question becomes:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;How much work should we make the browser do in the first place?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>frontend</category>
      <category>performance</category>
    </item>
    <item>
      <title>Reflow and Repaint Explained: Why Some UI Updates Are Expensive</title>
      <dc:creator>Billy Okeyo</dc:creator>
      <pubDate>Fri, 21 Aug 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/billy_de_cartel/reflow-and-repaint-explained-why-some-ui-updates-are-expensive-1g43</link>
      <guid>https://dev.to/billy_de_cartel/reflow-and-repaint-explained-why-some-ui-updates-are-expensive-1g43</guid>
      <description>&lt;blockquote&gt;
  &lt;p&gt;&lt;em&gt;“Changing one CSS property can be almost free. Changing another can force the browser to recalculate half the page. Understanding why is the difference between guessing at frontend performance and reasoning about it.”&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In the previous article in &lt;strong&gt;Beyond the UI&lt;/strong&gt;, we followed a webpage through the browser rendering pipeline. We started with HTML and CSS and eventually arrived at pixels:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;HTML ──────► DOM
               │
CSS ───────► CSSOM
               │
               ▼
          Render Tree
               │
               ▼
            Layout
               │
               ▼
             Paint
               │
               ▼
          Compositing
               │
               ▼
             Pixels
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;That pipeline explains how a page appears for the first time. But modern websites don’t remain still after they’re rendered. A user opens a menu, a notification appears, a modal slides onto the screen, an accordion expands, JavaScript adds another row to a table, an animation moves a card across the page, and a validation message appears underneath a form field. Every one of these interactions changes something the browser has already rendered.&lt;/p&gt;

&lt;p&gt;The interesting question is:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;How much work does the browser have to repeat when something changes?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The answer depends heavily on &lt;strong&gt;what changed&lt;/strong&gt;. Changing an element’s width may affect the position of everything around it. Changing its background color doesn’t affect its geometry, but the browser still needs to redraw it. Changing its &lt;code&gt;transform&lt;/code&gt; may, in favorable circumstances, avoid both layout and painting and require mostly compositing work.&lt;/p&gt;

&lt;p&gt;Conceptually, these updates can look very different:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Width Change

Style
  │
  ▼
Layout
  │
  ▼
Paint
  │
  ▼
Composite
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Compared with:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Background Change

Style
  │
  ▼
Paint
  │
  ▼
Composite
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;And sometimes:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Transform / Opacity

Style
  │
  ▼
Composite
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;These are simplified mental models rather than guarantees, but they reveal something fundamental about frontend performance:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;Not all UI updates cost the browser the same amount of work.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;To understand why, we need to look more closely at two terms that appear constantly in frontend performance discussions: &lt;strong&gt;Reflow&lt;/strong&gt; and &lt;strong&gt;Repaint&lt;/strong&gt;.&lt;/p&gt;





&lt;h2 id="what-is-reflow"&gt;What Is Reflow?&lt;/h2&gt;

&lt;p&gt;Imagine a simple page containing three cards.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;┌───────────────────────────┐
│ Card A                    │
│ height: 100px             │
└───────────────────────────┘

┌───────────────────────────┐
│ Card B                    │
│ height: 100px             │
└───────────────────────────┘

┌───────────────────────────┐
│ Card C                    │
│ height: 100px             │
└───────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The browser has already calculated where every card belongs. Then JavaScript changes the height of Card A.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;const card = document.querySelector(".card-a");

card.style.height = "300px";
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Card A can no longer occupy the same amount of space. That means Card B must move, and Card C must move as well. The browser needs to recalculate the geometry of the affected part of the page.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Before

Card A     y = 0
Card B     y = 120
Card C     y = 240


After Card A grows

Card A     y = 0
Card B     y = 320
Card C     y = 440
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;That recalculation is commonly called &lt;strong&gt;reflow&lt;/strong&gt;. In modern browser terminology, you’ll also frequently see it called &lt;strong&gt;layout&lt;/strong&gt;. During layout, the browser may need to recalculate things such as:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Width&lt;/li&gt;
  &lt;li&gt;Height&lt;/li&gt;
  &lt;li&gt;Position&lt;/li&gt;
  &lt;li&gt;Margins&lt;/li&gt;
  &lt;li&gt;Padding&lt;/li&gt;
  &lt;li&gt;Relationships between parents and children&lt;/li&gt;
  &lt;li&gt;Text wrapping&lt;/li&gt;
  &lt;li&gt;Available space&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The important part is that a layout change isn’t always isolated to the element you modified. One element can influence many others.&lt;/p&gt;





&lt;h2 id="why-reflow-can-become-expensive"&gt;Why Reflow Can Become Expensive&lt;/h2&gt;

&lt;p&gt;Suppose you change the width of a paragraph.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;.article {
    width: 600px;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Then later:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;article.style.width = "400px";
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The browser doesn’t simply make the rectangle narrower. Text may wrap differently, which changes the paragraph’s height, and everything below the paragraph may move. If the paragraph is inside another container whose height depends on its children, that container may also change, and now its siblings may need new positions. A single modification can ripple through the page.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Change width
     │
     ▼
Text wraps differently
     │
     ▼
Element height changes
     │
     ▼
Parent geometry changes
     │
     ▼
Sibling positions change
     │
     ▼
Layout recalculated
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;On a tiny page, this may take almost no noticeable time. On a large dashboard containing thousands of elements, complex grids, tables, charts, and nested components, repeated layout work can become expensive. This is particularly dangerous when it happens many times during a single animation or interaction.&lt;/p&gt;





&lt;h2 id="what-can-trigger-reflow"&gt;What Can Trigger Reflow?&lt;/h2&gt;

&lt;p&gt;Many operations can invalidate layout because they change geometry or influence how elements are positioned. Common examples include modifying:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;width
height
margin
padding
top
left
right
bottom
font-size
line-height
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Adding or removing DOM elements can also require layout.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;container.appendChild(newElement);
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;So can changing content.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;title.textContent =
    "This is a much longer title than before";
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The new text may wrap differently, changing the size of the element and potentially shifting everything around it. Even resizing the browser window can trigger significant layout work because responsive layouts may need to be recalculated. But writes aren’t the only thing developers need to think about. Sometimes &lt;strong&gt;reading&lt;/strong&gt; information from the DOM can cause performance problems too.&lt;/p&gt;





&lt;h2 id="the-surprising-cost-of-reading-layout"&gt;The Surprising Cost of Reading Layout&lt;/h2&gt;

&lt;p&gt;Consider this code:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;const width = element.offsetWidth;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;It looks harmless. We’re only asking the browser for a number. But imagine the browser already knows that something changed.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;element.style.width = "500px";

const width = element.offsetWidth;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The first line modifies layout. However, browsers often delay expensive rendering work until it is actually needed. Then the second line asks:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“What is the element’s width right now?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The browser cannot answer accurately using the old layout information. It may therefore need to calculate layout immediately before returning the value. Conceptually:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Change width
     │
     ▼
Layout becomes invalid
     │
     ▼
Read offsetWidth
     │
     ▼
Browser needs current geometry
     │
     ▼
Forced Layout
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This becomes especially problematic when reads and writes are repeatedly mixed together. And that leads us to one of the most notorious frontend performance problems.&lt;/p&gt;





&lt;h2 id="layout-thrashing-explained"&gt;Layout Thrashing Explained&lt;/h2&gt;

&lt;p&gt;Imagine you have 500 elements. You want to increase each one’s width slightly. You write:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;const items = document.querySelectorAll(".item");

items.forEach(item =&amp;gt; {
    const width = item.offsetWidth;

    item.style.width = `${width + 10}px`;
});
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;At first glance, this looks reasonable. Read the current width, add ten pixels, and move to the next element. But look at the pattern:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;READ
WRITE

READ
WRITE

READ
WRITE

READ
WRITE
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;After a write, layout may become invalid. The next read asks the browser for current geometry, so the browser may need to calculate layout. Then another write invalidates it again, and another read may force layout again. You can end up with something conceptually similar to:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Read
  │
Write
  │
Layout
  │
Read
  │
Write
  │
Layout
  │
Read
  │
Write
  │
Layout
  │
...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This repeated invalidation and recalculation is commonly known as &lt;strong&gt;layout thrashing&lt;/strong&gt;. Instead, you generally want to group reads together and then group writes together.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;const items = [...document.querySelectorAll(".item")];

const widths = items.map(item =&amp;gt; item.offsetWidth);

items.forEach((item, index) =&amp;gt; {
    item.style.width = `${widths[index] + 10}px`;
});
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Now the pattern becomes:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;READ
READ
READ
READ

     ↓

WRITE
WRITE
WRITE
WRITE
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This gives the browser much more opportunity to batch its work. The lesson is broader than this particular example:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;Avoid repeatedly switching between reading layout information and modifying layout inside tight loops.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;





&lt;h2 id="what-is-repaint"&gt;What Is Repaint?&lt;/h2&gt;

&lt;p&gt;Now imagine a different situation. You don’t change the size or position of an element. You simply change its background.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;card.style.backgroundColor = "blue";
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The card remains in exactly the same place. Its width doesn’t change, its height doesn’t change, and its siblings don’t move. The browser therefore doesn’t necessarily need to recalculate layout. But the card looks different, so the pixels representing it must be updated. This is where &lt;strong&gt;repaint&lt;/strong&gt; comes in. Conceptually:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Before

┌─────────────────┐
│                 │
│   Gray Card     │
│                 │
└─────────────────┘

       ↓

background-color changes

       ↓

┌─────────────────┐
│                 │
│   Blue Card     │
│                 │
└─────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The geometry hasn’t changed. The appearance has. The browser needs to paint the affected visual content again.&lt;/p&gt;





&lt;h2 id="reflow-vs-repaint"&gt;Reflow vs Repaint&lt;/h2&gt;

&lt;p&gt;This distinction is worth making clear. A &lt;strong&gt;reflow&lt;/strong&gt; deals primarily with geometry. A &lt;strong&gt;repaint&lt;/strong&gt; deals primarily with appearance. Consider these two updates.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;element.style.width = "500px";
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;and:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;element.style.backgroundColor = "red";
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The first can affect layout. The second generally doesn’t. A simplified comparison looks like this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Change&lt;/th&gt;
      &lt;th&gt;Layout&lt;/th&gt;
      &lt;th&gt;Paint&lt;/th&gt;
      &lt;th&gt;Composite&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code&gt;width&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;Usually&lt;/td&gt;
      &lt;td&gt;Usually&lt;/td&gt;
      &lt;td&gt;Usually&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code&gt;height&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;Usually&lt;/td&gt;
      &lt;td&gt;Usually&lt;/td&gt;
      &lt;td&gt;Usually&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code&gt;padding&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;Usually&lt;/td&gt;
      &lt;td&gt;Usually&lt;/td&gt;
      &lt;td&gt;Usually&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code&gt;font-size&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;Usually&lt;/td&gt;
      &lt;td&gt;Usually&lt;/td&gt;
      &lt;td&gt;Usually&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code&gt;background-color&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;No layout in typical cases&lt;/td&gt;
      &lt;td&gt;Usually&lt;/td&gt;
      &lt;td&gt;Usually&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code&gt;box-shadow&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;No layout in typical cases&lt;/td&gt;
      &lt;td&gt;Usually&lt;/td&gt;
      &lt;td&gt;Usually&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code&gt;transform&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;Often avoidable&lt;/td&gt;
      &lt;td&gt;Often avoidable&lt;/td&gt;
      &lt;td&gt;Often&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code&gt;opacity&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;Often avoidable&lt;/td&gt;
      &lt;td&gt;Often avoidable&lt;/td&gt;
      &lt;td&gt;Often&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This table is intentionally simplified. Browser engines are highly optimized, and exactly what gets recalculated depends on the page, browser, element, layer structure, and other factors. The useful mental model is the hierarchy:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Layout
  │
  ▼
Paint
  │
  ▼
Composite
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;If you invalidate layout, later stages may also need work. If you only invalidate paint, layout may be avoided. If an update can be handled during compositing, both layout and painting may sometimes be avoided. This is why avoiding unnecessary layout work can be so valuable.&lt;/p&gt;





&lt;h2 id="compositing-the-cheaper-path"&gt;Compositing: The Cheaper Path&lt;/h2&gt;

&lt;p&gt;Suppose you want to animate a card from left to right. One approach is changing &lt;code&gt;left&lt;/code&gt;.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;.card {
    position: absolute;
    left: 0;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Then:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;card.style.left = "300px";
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Because &lt;code&gt;left&lt;/code&gt; participates in positioning, changing it can require layout work. Now consider:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;card.style.transform = "translateX(300px)";
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;A transform changes how the already-rendered element is presented. In many situations, browsers can handle transforms efficiently during compositing, particularly when the element is on its own composited layer. The difference can look conceptually like this:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Animating left

JavaScript
    │
    ▼
Layout
    │
    ▼
Paint
    │
    ▼
Composite
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;versus:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Animating transform

JavaScript
    │
    ▼
Composite
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This is why &lt;code&gt;transform&lt;/code&gt; and &lt;code&gt;opacity&lt;/code&gt; are commonly recommended for animations. For example:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;.modal {
    opacity: 0;
    transform: translateY(20px);

    transition:
        opacity 200ms ease,
        transform 200ms ease;
}

.modal.open {
    opacity: 1;
    transform: translateY(0);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Rather than animating:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;top
left
width
height
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;you give the browser a better opportunity to perform the animation without repeatedly recalculating page geometry. But there’s an important warning here.&lt;/p&gt;





&lt;h2 id="dont-turn-use-transform-into-another-rule-to-memorize"&gt;Don’t Turn “Use Transform” Into Another Rule to Memorize&lt;/h2&gt;

&lt;p&gt;Frontend performance advice often becomes simplified into statements like:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Always animate &lt;code&gt;transform&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Or:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;code&gt;transform&lt;/code&gt; doesn’t cause repaint.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Those statements are useful shortcuts, but reality is more nuanced. Browsers make their own decisions about compositing layers. Effects such as filters, clipping, large painted areas, and complex descendants can influence how much work an animation requires. Hardware, browser versions, and the structure of the page matter too.&lt;/p&gt;

&lt;p&gt;So instead of memorizing:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;code&gt;transform = fast&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;remember the deeper idea:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;Prefer updates that allow the browser to avoid repeating earlier, more expensive stages of the rendering pipeline.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Then measure what actually happens.&lt;/p&gt;





&lt;h2 id="will-change-useful-but-easy-to-abuse"&gt;
&lt;code&gt;will-change&lt;/code&gt;: Useful but Easy to Abuse&lt;/h2&gt;

&lt;p&gt;CSS provides a property called &lt;code&gt;will-change&lt;/code&gt;. For example:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;.card {
    will-change: transform;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;You’re essentially giving the browser a hint:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“This element is likely to change in this way soon.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The browser may use that information to prepare optimizations ahead of time, potentially including promoting the element to its own compositing layer. That can be useful for animations that are known to be performance-sensitive. But this does &lt;strong&gt;not&lt;/strong&gt; mean you should do this:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;* {
    will-change: transform;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Compositing layers aren’t free. They consume memory and other resources, and creating unnecessary layers can make performance worse rather than better. &lt;code&gt;will-change&lt;/code&gt; should therefore be treated as a targeted optimization, not a default styling strategy.&lt;/p&gt;





&lt;h2 id="dom-updates-and-reflow"&gt;DOM Updates and Reflow&lt;/h2&gt;

&lt;p&gt;Another common source of rendering work is repeatedly modifying the DOM. Imagine adding 1,000 rows to a table. A naive implementation might look like:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;for (let i = 0; i &amp;lt; 1000; i++) {
    const row = document.createElement("tr");

    row.innerHTML = `
        &amp;lt;td&amp;gt;${i}&amp;lt;/td&amp;gt;
        &amp;lt;td&amp;gt;Customer ${i}&amp;lt;/td&amp;gt;
    `;

    table.appendChild(row);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Modern browsers are good at batching work, so this doesn’t automatically mean 1,000 complete reflows. Still, repeatedly touching the live DOM can create unnecessary work, especially when your code also performs layout reads or other operations between writes. One option is constructing the changes away from the live document first.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;const fragment = document.createDocumentFragment();

for (let i = 0; i &amp;lt; 1000; i++) {
    const row = document.createElement("tr");

    row.innerHTML = `
        &amp;lt;td&amp;gt;${i}&amp;lt;/td&amp;gt;
        &amp;lt;td&amp;gt;Customer ${i}&amp;lt;/td&amp;gt;
    `;

    fragment.appendChild(row);
}

table.appendChild(fragment);
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The browser receives the collection of new nodes together. The broader principle is more important than &lt;code&gt;DocumentFragment&lt;/code&gt; itself:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;Batch related DOM updates where practical instead of constantly alternating between DOM writes and layout-dependent reads.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Modern frameworks often help organize updates for you, but they cannot eliminate poor rendering patterns entirely.&lt;/p&gt;





&lt;h2 id="react-doesnt-make-reflow-disappear"&gt;React Doesn’t Make Reflow Disappear&lt;/h2&gt;

&lt;p&gt;Suppose you’re using React. You write:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;function Sidebar({ open }) {
    return (
        &amp;lt;aside
            className={open ? "sidebar open" : "sidebar"}
        &amp;gt;
            Menu
        &amp;lt;/aside&amp;gt;
    );
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;React determines what needs to change in the DOM. But after React updates the DOM, the browser still has to render the result. If your CSS says:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;.sidebar {
    width: 0;
    transition: width 300ms;
}

.sidebar.open {
    width: 300px;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;the browser may need to perform layout repeatedly while that width is being animated. React’s reconciliation algorithm doesn’t remove that cost. The same applies to Vue, Angular, Svelte, Solid, and other frameworks. Frameworks determine &lt;strong&gt;what DOM changes should happen&lt;/strong&gt;. The browser determines &lt;strong&gt;how those changes become pixels&lt;/strong&gt;.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Application State
       │
       ▼
Framework
       │
       ▼
DOM Update
       │
       ▼
Browser
       │
       ├── Layout?
       ├── Paint?
       └── Composite?
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This distinction is important. A highly optimized React component can still produce expensive browser rendering work.&lt;/p&gt;





&lt;h2 id="a-real-world-example-expanding-an-accordion"&gt;A Real-World Example: Expanding an Accordion&lt;/h2&gt;

&lt;p&gt;Imagine an FAQ section. When the user clicks a question, the answer expands. A common implementation animates height.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;.answer {
    height: 0;
    overflow: hidden;
    transition: height 300ms;
}

.answer.open {
    height: 200px;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The animation looks simple. But as the height changes:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;0px
20px
40px
60px
80px
...
200px
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;the elements underneath may need to move during each stage. Conceptually:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Answer grows
    │
    ▼
Layout changes
    │
    ▼
Content below moves
    │
    ▼
Paint
    │
    ▼
Composite
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This doesn’t automatically mean the animation is unacceptable. Sometimes layout animation is exactly what the design requires, and modern devices may handle it perfectly well. Performance engineering isn’t about eliminating every reflow. It’s about avoiding &lt;strong&gt;unnecessary&lt;/strong&gt; or &lt;strong&gt;excessive&lt;/strong&gt; work. That’s an important distinction.&lt;/p&gt;





&lt;h2 id="reflow-is-not-the-enemy"&gt;Reflow Is Not the Enemy&lt;/h2&gt;

&lt;p&gt;After learning about layout performance, it’s easy to become afraid of reflow. Don’t. Browsers are designed to perform layout, and changing layouts is a fundamental part of building interactive webpages. Adding a message to the page, opening a navigation menu, rendering search results, or resizing a responsive application may all require layout, and none of these are inherently bad. The problem arises when we force the browser to repeat expensive work unnecessarily.&lt;/p&gt;

&lt;p&gt;This:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;One user action
      │
      ▼
One coordinated DOM update
      │
      ▼
Layout
      │
      ▼
Paint
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;is perfectly normal.&lt;/p&gt;

&lt;p&gt;This is more concerning:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;One user action
      │
      ▼
Read
Write
Layout
Read
Write
Layout
Read
Write
Layout
Read
Write
Layout
...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Optimization is usually about reducing redundant work, not eliminating legitimate rendering work.&lt;/p&gt;





&lt;h2 id="measuring-reflow-and-repaint"&gt;Measuring Reflow and Repaint&lt;/h2&gt;

&lt;p&gt;You don’t have to guess whether your page is doing too much rendering work. Modern browsers provide performance profiling tools. In Chrome DevTools, for example, the &lt;strong&gt;Performance&lt;/strong&gt; panel can record what happens during an interaction. You may see work categorized around:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Scripting

Rendering / Layout

Painting

Compositing
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Suppose clicking a button causes a visible delay. Instead of immediately rewriting your React components, record the interaction. Perhaps JavaScript is the problem, layout takes too long, a huge section of the page is being repainted, or the main thread is busy doing unrelated work. The browser’s profiling tools help answer those questions.&lt;/p&gt;

&lt;p&gt;This leads to one of the most useful rules in frontend performance:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;Measure before you optimize.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A theoretical optimization that saves 0.2 milliseconds isn’t worth making your code significantly harder to maintain. Focus on bottlenecks users can actually experience.&lt;/p&gt;





&lt;h2 id="practical-ways-to-reduce-rendering-work"&gt;Practical Ways to Reduce Rendering Work&lt;/h2&gt;

&lt;p&gt;Once you understand the rendering pipeline, several optimization techniques start to make sense naturally. When animating movement, prefer &lt;code&gt;transform&lt;/code&gt; where it achieves the same visual result.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;transform: translateX(100px);
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;When fading elements, prefer &lt;code&gt;opacity&lt;/code&gt;.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;opacity: 0;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Avoid repeatedly alternating layout reads and writes. Instead of:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;READ → WRITE → READ → WRITE
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;prefer:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;READ → READ → READ
          │
          ▼
WRITE → WRITE → WRITE
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Batch related DOM updates where possible. Avoid unnecessary manipulation of large parts of the DOM. Be cautious with expensive visual effects on large or frequently changing areas. Use &lt;code&gt;will-change&lt;/code&gt; only when profiling suggests it is useful. And most importantly, use browser performance tools to confirm where your application is spending time. These aren’t arbitrary “frontend best practices.” Every one of them follows from the same idea:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;Give the browser less unnecessary work to do before the next frame must appear.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;





&lt;h2 id="the-167ms-problem"&gt;The 16.7ms Problem&lt;/h2&gt;

&lt;p&gt;In the previous article, we introduced the idea of the browser’s frame budget. On a 60 Hz display, a new frame is available roughly every:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;1000ms / 60 ≈ 16.7ms
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;That doesn’t mean your JavaScript gets the entire 16.7 milliseconds. The browser may also need to perform:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;JavaScript
     │
     ▼
Style Calculation
     │
     ▼
Layout
     │
     ▼
Paint
     │
     ▼
Composite
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;If your code triggers repeated layout calculations and expensive paints, the browser may not finish everything before the next frame is due. A frame gets missed, then another. The user sees:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Smooth

● ● ● ● ● ● ● ● ● ●


Janky

● ●   ●     ● ●    ●
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This is why rendering performance directly affects how an interface &lt;strong&gt;feels&lt;/strong&gt;. Users don’t know that your page suffered a forced synchronous layout. They simply know that dragging the panel felt sluggish.&lt;/p&gt;





&lt;h2 id="bringing-it-all-together"&gt;Bringing It All Together&lt;/h2&gt;

&lt;p&gt;Reflow and repaint aren’t mysterious browser behaviors. They’re consequences of how browsers turn changing documents into pixels. When geometry changes, the browser may need to recalculate layout.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Change width / height / position
              │
              ▼
            Layout
              │
              ▼
             Paint
              │
              ▼
          Composite
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;When only appearance changes, layout may be avoided.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Change visual property
          │
          ▼
         Paint
          │
          ▼
      Composite
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;And some changes may be handled primarily during compositing.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Transform / Opacity
          │
          ▼
      Composite
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Again, these are simplified mental models, not guarantees. But they give us a much better way to reason about frontend performance. Instead of asking:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“Which CSS properties are fast?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;we can ask:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;“Which parts of the rendering pipeline does this update require the browser to repeat?”&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That’s the question that scales beyond individual tricks.&lt;/p&gt;





&lt;h2 id="final-thoughts"&gt;Final Thoughts&lt;/h2&gt;

&lt;p&gt;Frontend performance is often taught as a collection of rules: use &lt;code&gt;transform&lt;/code&gt;, avoid changing width, don’t touch the DOM too much, use &lt;code&gt;requestAnimationFrame&lt;/code&gt;, and avoid forced layouts. Those recommendations can be useful, but memorizing them without understanding the browser makes them fragile. Once you understand reflow and repaint, the rules start explaining themselves. Changing geometry can require layout. Changing appearance can require painting. Some visual changes can be handled efficiently during compositing. Repeatedly forcing the browser backwards through that pipeline can consume the limited time available for each frame.&lt;/p&gt;

&lt;p&gt;The goal isn’t to build an application that never triggers reflow or repaint. That’s unrealistic. The goal is to make those operations &lt;strong&gt;intentional rather than accidental&lt;/strong&gt;. And perhaps the most dangerous accidental performance problem is one we’ve already encountered in this article: JavaScript asks the browser for information, changes something, asks for more information, changes something again, and continues doing this while the browser desperately tries to keep its layout up to date.&lt;/p&gt;

&lt;p&gt;To understand why that happens, we need to understand how JavaScript itself gets scheduled.&lt;/p&gt;





&lt;h2 id="whats-next"&gt;What’s Next?&lt;/h2&gt;

&lt;p&gt;In the next article in &lt;strong&gt;Beyond the UI&lt;/strong&gt;, we’ll move from rendering into JavaScript execution:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;The JavaScript Event Loop Explained: Why Your UI Freezes&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;We’ll explore the call stack, Web APIs, tasks, microtasks, promises, timers, and &lt;code&gt;requestAnimationFrame&lt;/code&gt;, and see why an innocent-looking piece of JavaScript can prevent an entire interface from responding. Because sometimes your UI isn’t slow because the browser is painting too much. Sometimes the browser simply &lt;strong&gt;doesn’t get a chance to paint at all.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>frontend</category>
      <category>performance</category>
    </item>
    <item>
      <title>The Browser Rendering Pipeline Explained: From HTML to Pixels</title>
      <dc:creator>Billy Okeyo</dc:creator>
      <pubDate>Mon, 17 Aug 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/billy_de_cartel/the-browser-rendering-pipeline-explained-from-html-to-pixels-1h0e</link>
      <guid>https://dev.to/billy_de_cartel/the-browser-rendering-pipeline-explained-from-html-to-pixels-1h0e</guid>
      <description>&lt;blockquote&gt;
  &lt;p&gt;&lt;em&gt;“You write HTML and CSS. The browser’s job is to somehow turn them into pixels. What happens in between is one of the most important things a frontend developer can understand.”&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id="introduction"&gt;Introduction&lt;/h2&gt;

&lt;p&gt;Open a browser and visit almost any website. Within milliseconds, text appears, buttons take shape, images load, colors fill the screen, and complex layouts arrange themselves into something you can interact with.&lt;/p&gt;

&lt;p&gt;As developers, we usually think about the code responsible for that interface. We write something like:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;lt;div class="card"&amp;gt;
    &amp;lt;h2&amp;gt;Welcome Back&amp;lt;/h2&amp;gt;
    &amp;lt;p&amp;gt;You have 3 new notifications.&amp;lt;/p&amp;gt;
    &amp;lt;button&amp;gt;View Notifications&amp;lt;/button&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Then we add some CSS:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;.card {
    padding: 24px;
    border-radius: 12px;
}

.card h2 {
    font-size: 24px;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;We refresh the browser and see a card. It feels almost instantaneous. But the browser cannot display HTML, and it cannot display CSS either. Your monitor ultimately understands pixels.&lt;/p&gt;

&lt;p&gt;Somewhere between receiving this:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;lt;h1&amp;gt;Hello World&amp;lt;/h1&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;and displaying &lt;strong&gt;Hello World&lt;/strong&gt; on the screen, the browser has to parse the document, understand its structure, determine which CSS rules apply, calculate the size and position of elements, determine what needs to be drawn, organize those drawings into layers, and finally send the result to the screen.&lt;/p&gt;

&lt;p&gt;That journey is known as the &lt;strong&gt;browser rendering pipeline&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;At a high level, it looks something like this:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;HTML
 │
 ▼
DOM

CSS
 │
 ▼
CSSOM

DOM + CSSOM
     │
     ▼
 Render Tree
     │
     ▼
   Layout
     │
     ▼
   Paint
     │
     ▼
Compositing
     │
     ▼
   Pixels
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Understanding this pipeline changes the way you think about frontend development. Suddenly, performance problems such as layout thrashing, expensive animations, unnecessary repaints, and large stylesheets stop feeling mysterious. You begin to understand &lt;strong&gt;why&lt;/strong&gt; certain operations are expensive rather than simply memorizing that they should be avoided.&lt;/p&gt;

&lt;p&gt;So let’s follow a webpage from the moment the browser receives its HTML to the moment pixels appear on your screen.&lt;/p&gt;





&lt;h2 id="step-1-the-browser-receives-html"&gt;Step 1: The Browser Receives HTML&lt;/h2&gt;

&lt;p&gt;Suppose you navigate to:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;https://example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;After the networking work required to obtain the document, the browser begins receiving HTML. Importantly, it doesn’t necessarily wait for the entire document before doing anything. HTML can be processed progressively as bytes arrive over the network.&lt;/p&gt;

&lt;p&gt;Imagine the server sends:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;title&amp;gt;My Store&amp;lt;/title&amp;gt;
&amp;lt;/head&amp;gt;

&amp;lt;body&amp;gt;
    &amp;lt;main&amp;gt;
        &amp;lt;h1&amp;gt;Products&amp;lt;/h1&amp;gt;

        &amp;lt;div class="product"&amp;gt;
            &amp;lt;h2&amp;gt;Laptop&amp;lt;/h2&amp;gt;
            &amp;lt;p&amp;gt;KES 75,000&amp;lt;/p&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/main&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;To us, this is a text document. To the browser, it’s a set of instructions describing the structure of a page. The first major job is converting that text into something the browser can work with. That something is the &lt;strong&gt;DOM&lt;/strong&gt;.&lt;/p&gt;





&lt;h2 id="step-2-building-the-dom"&gt;Step 2: Building the DOM&lt;/h2&gt;

&lt;p&gt;DOM stands for &lt;strong&gt;Document Object Model&lt;/strong&gt;. The browser parses the HTML and converts its elements into a tree of objects. Our previous HTML becomes conceptually similar to this:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Document
│
└── html
    │
    ├── head
    │   └── title
    │       └── "My Store"
    │
    └── body
        │
        └── main
            │
            ├── h1
            │   └── "Products"
            │
            └── div.product
                │
                ├── h2
                │   └── "Laptop"
                │
                └── p
                    └── "KES 75,000"
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This tree is the DOM. The DOM isn’t simply a copy of the HTML file. It’s the browser’s &lt;strong&gt;in-memory representation of the document&lt;/strong&gt;. That’s why JavaScript can do things like:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;const title = document.querySelector("h1");

title.textContent = "Featured Products";
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;JavaScript isn’t editing your original HTML file. It’s modifying the DOM that the browser constructed from it. Once that DOM changes, the browser may need to perform additional rendering work so the screen reflects the new state.&lt;/p&gt;

&lt;p&gt;We’ll come back to that shortly. For now, we have structure. But the browser still doesn’t know what the page should look like. For that, it needs CSS.&lt;/p&gt;





&lt;h2 id="step-3-building-the-cssom"&gt;Step 3: Building the CSSOM&lt;/h2&gt;

&lt;p&gt;Suppose our document references a stylesheet.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;lt;link rel="stylesheet" href="https://billyokeyo.dev/styles.css"&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;And that stylesheet contains:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;body {
    font-family: Arial, sans-serif;
}

.product {
    padding: 20px;
    border: 1px solid #ddd;
}

.product h2 {
    font-size: 24px;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Just as the browser doesn’t directly work with raw HTML when rendering the page, it doesn’t simply treat CSS as a collection of strings. It parses the CSS and constructs another representation called the &lt;strong&gt;CSS Object Model&lt;/strong&gt;, or &lt;strong&gt;CSSOM&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;CSSOM
│
├── body
│   └── font-family: Arial
│
└── .product
    │
    ├── padding: 20px
    ├── border: 1px solid #ddd
    │
    └── h2
        └── font-size: 24px
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The real CSSOM is considerably more sophisticated because CSS involves inheritance, specificity, cascading rules, media queries, browser defaults, and many other considerations. But the important idea is simple. The browser now has two important pieces of information.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;DOM&lt;/strong&gt; tells it:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;What exists?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The &lt;strong&gt;CSSOM&lt;/strong&gt; helps determine:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;What should it look like?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Now the browser can begin combining them.&lt;/p&gt;





&lt;h2 id="step-4-creating-the-render-tree"&gt;Step 4: Creating the Render Tree&lt;/h2&gt;

&lt;p&gt;The browser doesn’t simply draw every node in the DOM. Some elements shouldn’t appear on screen.&lt;/p&gt;

&lt;p&gt;Consider:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;lt;div class="message"&amp;gt;
    Payment successful
&amp;lt;/div&amp;gt;

&amp;lt;div class="debug"&amp;gt;
    Internal debugging information
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;With:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;.debug {
    display: none;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The &lt;code&gt;.debug&lt;/code&gt; element exists in the DOM. JavaScript can still find it.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;document.querySelector(".debug");
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;But it doesn’t need to be rendered. This is why the browser constructs another structure: the &lt;strong&gt;render tree&lt;/strong&gt;. The render tree combines relevant DOM nodes with their computed styles and contains the visual elements that need to participate in layout and painting.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;DOM                       CSSOM
 │                          │
 └────────────┬─────────────┘
              │
              ▼
         Render Tree
              │
      Visible elements
      + computed styles
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;An element with:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;display: none;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;doesn’t generate a box in the render tree.&lt;/p&gt;

&lt;p&gt;There are some important subtleties here. For example:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;visibility: hidden;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;is different. The element is invisible, but it still occupies space in the layout. Understanding these differences becomes important when optimizing interfaces.&lt;/p&gt;

&lt;p&gt;At this point, the browser knows &lt;strong&gt;what needs to be rendered&lt;/strong&gt; and &lt;strong&gt;how it should look&lt;/strong&gt;. But there’s still one major question. Where exactly should everything go?&lt;/p&gt;





&lt;h2 id="step-5-layout-calculating-geometry"&gt;Step 5: Layout: Calculating Geometry&lt;/h2&gt;

&lt;p&gt;Consider this CSS:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;.container {
    width: 80%;
}

.card {
    width: 50%;
    padding: 20px;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;What does &lt;code&gt;50%&lt;/code&gt; actually mean in pixels? That depends on the size of the parent, and the parent’s size might depend on its parent. The browser therefore needs to calculate the geometry of the page. This stage is commonly called &lt;strong&gt;layout&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;During layout, the browser determines things such as:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Element width&lt;/li&gt;
  &lt;li&gt;Element height&lt;/li&gt;
  &lt;li&gt;X position&lt;/li&gt;
  &lt;li&gt;Y position&lt;/li&gt;
  &lt;li&gt;Margins&lt;/li&gt;
  &lt;li&gt;Padding&lt;/li&gt;
  &lt;li&gt;Relationships between elements&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Imagine a viewport that is 1200 pixels wide.&lt;/p&gt;

&lt;p&gt;If:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;.container {
    width: 80%;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;then the container may become:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;960px
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;A child with:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;width: 50%;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;may therefore become:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;480px
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The browser performs these calculations throughout the relevant layout tree. The result might conceptually look like:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Viewport: 1200 × 800

┌───────────────────────────────────────┐
│ Header                                │
│ x: 0   y: 0                           │
│ width: 1200   height: 80              │
├───────────────────────────────────────┤
│                                       │
│   Product Card                        │
│   x: 120   y: 120                     │
│   width: 480   height: 220            │
│                                       │
└───────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The browser now knows exactly where elements belong. And this is where frontend performance starts becoming particularly interesting.&lt;/p&gt;





&lt;h2 id="why-layout-can-be-expensive"&gt;Why Layout Can Be Expensive&lt;/h2&gt;

&lt;p&gt;Suppose JavaScript changes the width of an element.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;element.style.width = "800px";
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;That change may affect much more than that one element. Its children may need to move. Its siblings may need to move. Its parent’s dimensions might change. Other parts of the page may need to be recalculated. The browser may therefore need to perform layout again. This is commonly referred to as &lt;strong&gt;reflow&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Consider a list:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;┌─────────────────────┐
│ Item 1              │
├─────────────────────┤
│ Item 2              │
├─────────────────────┤
│ Item 3              │
├─────────────────────┤
│ Item 4              │
└─────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;If Item 1 suddenly becomes three times taller, Items 2, 3, and 4 may all need new positions. One small change has affected several elements. This is why repeatedly modifying layout-related properties can hurt performance, especially on complex pages.&lt;/p&gt;

&lt;p&gt;But calculating positions still doesn’t put anything on the screen. The browser now needs to draw.&lt;/p&gt;





&lt;h2 id="step-6-paint-turning-elements-into-drawing-instructions"&gt;Step 6: Paint: Turning Elements Into Drawing Instructions&lt;/h2&gt;

&lt;p&gt;Once layout is complete, the browser knows what should appear and where it belongs. Now it needs to determine how to draw it. This is the &lt;strong&gt;paint&lt;/strong&gt; stage.&lt;/p&gt;

&lt;p&gt;Consider a button:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;button {
    background: blue;
    color: white;
    border-radius: 8px;
    box-shadow: 0 4px 10px rgba(0,0,0,.2);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Painting may involve drawing:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;The background&lt;/li&gt;
  &lt;li&gt;The border&lt;/li&gt;
  &lt;li&gt;The text&lt;/li&gt;
  &lt;li&gt;The rounded corners&lt;/li&gt;
  &lt;li&gt;The shadow&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The browser creates painting instructions representing these visual operations.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Layout

Button:
x = 100
y = 200
width = 160
height = 48

        │
        ▼

Paint

Draw background
Draw border
Draw shadow
Draw text
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Some visual effects are considerably more expensive to paint than others. Large shadows, complex gradients, filters, and large areas that change frequently can increase rendering work. This is why performance problems aren’t always caused by JavaScript. Sometimes the browser simply has too much visual work to perform.&lt;/p&gt;





&lt;h2 id="step-7-compositing"&gt;Step 7: Compositing&lt;/h2&gt;

&lt;p&gt;Modern webpages are often too complicated to paint as one giant flat image every time something changes. Browsers can divide parts of the page into separate compositing layers.&lt;/p&gt;

&lt;p&gt;Imagine a page containing:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Background

Content

Navigation

Modal

Animation
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Conceptually, the browser might treat these as layers:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;          ┌─────────────┐
          │    Modal    │
          └─────────────┘
                 ▲
          ┌─────────────┐
          │ Navigation  │
          └─────────────┘
                 ▲
          ┌─────────────┐
          │   Content   │
          └─────────────┘
                 ▲
          ┌─────────────┐
          │ Background  │
          └─────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;During &lt;strong&gt;compositing&lt;/strong&gt;, those layers are assembled in the correct order to produce the final image. This is especially important for animations.&lt;/p&gt;

&lt;p&gt;Suppose you animate an element using:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;transform: translateX(200px);
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;In favorable cases, the browser can move an already-painted composited layer rather than recalculating the layout and repainting large parts of the page.&lt;/p&gt;

&lt;p&gt;Compare that with repeatedly changing:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;left: 200px;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Depending on the page and positioning context, changing &lt;code&gt;left&lt;/code&gt; may trigger layout and subsequent rendering work. This is one reason frontend performance advice often recommends animating properties such as:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;transform
opacity
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;when appropriate. The recommendation isn’t arbitrary. It comes directly from understanding how browsers render pages.&lt;/p&gt;





&lt;h2 id="putting-the-entire-pipeline-together"&gt;Putting the Entire Pipeline Together&lt;/h2&gt;

&lt;p&gt;We can now see the full journey.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;                 HTML
                   │
                   ▼
              Parse HTML
                   │
                   ▼
                  DOM

                 CSS
                   │
                   ▼
               Parse CSS
                   │
                   ▼
                 CSSOM

           DOM + CSSOM
                   │
                   ▼
              Render Tree
                   │
                   ▼
                Layout
          Size + Position
                   │
                   ▼
                 Paint
         Drawing Instructions
                   │
                   ▼
              Compositing
            Combine Layers
                   │
                   ▼
                 Pixels
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;What started as text has become something visible on a screen. And the process happens incredibly quickly.&lt;/p&gt;





&lt;h2 id="where-javascript-enters-the-picture"&gt;Where JavaScript Enters the Picture&lt;/h2&gt;

&lt;p&gt;So far, we’ve mostly discussed HTML and CSS. But modern applications are highly dynamic. JavaScript constantly modifies the page.&lt;/p&gt;

&lt;p&gt;Consider:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;const card = document.querySelector(".card");

card.style.width = "600px";
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Changing the width affects geometry. The browser may need to perform:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;JavaScript
    │
    ▼
DOM / Style Change
    │
    ▼
Layout
    │
    ▼
Paint
    │
    ▼
Composite
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Now consider:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;card.style.backgroundColor = "red";
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The geometry hasn’t changed. The card remains exactly where it was. The browser may therefore avoid layout and perform only the later rendering stages.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;JavaScript
    │
    ▼
Style Change
    │
    ▼
Paint
    │
    ▼
Composite
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;And for some composited animations:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;card.style.transform = "translateX(100px)";
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;the browser may be able to perform primarily compositing work rather than recalculating the entire layout.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;JavaScript
    │
    ▼
Transform
    │
    ▼
Composite
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The exact behavior depends on the browser and page, so these diagrams should be treated as useful mental models rather than rigid guarantees. But they reveal an important principle:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;Not every visual change costs the browser the same amount of work.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;





&lt;h2 id="why-this-matters-for-frontend-developers"&gt;Why This Matters for Frontend Developers&lt;/h2&gt;

&lt;p&gt;It’s easy to treat browser performance as something only framework authors need to worry about. But your everyday decisions influence this pipeline. When you manipulate the DOM thousands of times, you’re interacting with it. When you animate dimensions, you’re interacting with it. When you ship enormous stylesheets, you’re interacting with it. When you build deeply nested layouts, you’re interacting with it. When you repeatedly read layout information and immediately modify styles, you’re interacting with it.&lt;/p&gt;

&lt;p&gt;Frameworks don’t eliminate the browser rendering pipeline. React still ends up modifying the DOM. Vue still ends up modifying the DOM. Angular still ends up modifying the DOM. Svelte still ends up modifying the DOM. Eventually, every web framework reaches the same destination:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Framework

   │

   ▼

DOM Changes

   │

   ▼

Browser Rendering Pipeline

   │

   ▼

Pixels
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Understanding the browser therefore gives you knowledge that survives whichever framework becomes popular next.&lt;/p&gt;





&lt;h2 id="a-practical-example"&gt;A Practical Example&lt;/h2&gt;

&lt;p&gt;Imagine you’re building an animation. You write:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;function move() {
    element.style.left =
        `${element.offsetLeft + 1}px`;

    requestAnimationFrame(move);
}

move();
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Every frame, the code reads &lt;code&gt;offsetLeft&lt;/code&gt; and then changes &lt;code&gt;left&lt;/code&gt;. The browser may need layout information to answer the read, and the subsequent write can invalidate layout again. On a simple page, you may never notice. On a large application with hundreds or thousands of elements, repeated layout work can become expensive.&lt;/p&gt;

&lt;p&gt;Now consider an animation using transforms:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;let position = 0;

function move() {
    position += 1;

    element.style.transform =
        `translateX(${position}px)`;

    requestAnimationFrame(move);
}

move();
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This gives the browser more opportunity to handle the animation efficiently through compositing.&lt;/p&gt;

&lt;p&gt;Again, the point isn’t:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“&lt;code&gt;transform&lt;/code&gt; is magically fast.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The important lesson is understanding &lt;strong&gt;why&lt;/strong&gt; some changes can require less work from the rendering pipeline than others. Once you understand that, frontend optimization becomes reasoning rather than memorization.&lt;/p&gt;





&lt;h2 id="the-browser-has-a-frame-budget"&gt;The Browser Has a Frame Budget&lt;/h2&gt;

&lt;p&gt;Smooth interfaces usually aim for approximately &lt;strong&gt;60 frames per second&lt;/strong&gt; on a 60 Hz display. That gives the browser roughly:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;1000ms / 60 ≈ 16.7ms
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;to produce each frame.&lt;/p&gt;

&lt;p&gt;Within that small window, the browser may need to:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;JavaScript

↓

Style Calculation

↓

Layout

↓

Paint

↓

Composite

↓

Display Frame
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;If the work takes significantly longer than the available frame time, the browser may miss a frame. Users experience that as:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Jank&lt;/li&gt;
  &lt;li&gt;Stuttering&lt;/li&gt;
  &lt;li&gt;Delayed interactions&lt;/li&gt;
  &lt;li&gt;Choppy animations&lt;/li&gt;
  &lt;li&gt;An interface that simply feels slow&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is why a page can load quickly and still feel terrible to use. Performance isn’t only about how quickly resources download. It’s also about how efficiently the browser can respond to changes after the page has loaded.&lt;/p&gt;





&lt;h2 id="common-rendering-performance-mistakes"&gt;Common Rendering Performance Mistakes&lt;/h2&gt;

&lt;p&gt;Understanding the pipeline makes several common frontend mistakes easier to recognize. One is changing layout properties continuously during animations. Another is performing large numbers of DOM operations individually when they could be grouped together. Developers can also accidentally force repeated layout calculations by alternating between reading geometry and modifying styles. Large and unnecessary visual effects can make painting more expensive. Creating too many compositing layers can consume additional memory rather than improving performance.&lt;/p&gt;

&lt;p&gt;And perhaps most importantly, developers sometimes optimize based on rules they’ve memorized instead of measuring what the browser is actually doing. Modern browser developer tools can show you layout work, painting, long tasks, frame timings, and other performance information. Use them.&lt;/p&gt;

&lt;p&gt;The rendering pipeline gives you the mental model. Profiling tells you what your particular application is actually doing.&lt;/p&gt;





&lt;h2 id="bringing-it-all-together"&gt;Bringing It All Together&lt;/h2&gt;

&lt;p&gt;When you write:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;lt;button&amp;gt;Buy Now&amp;lt;/button&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;the browser does far more work than the simplicity of that line suggests. It parses the HTML. It constructs the DOM. It processes CSS and builds the CSSOM. It determines which elements need to participate in rendering. It calculates their dimensions and positions. It generates painting instructions. It organizes visual content into layers where appropriate. Finally, it composites everything into the pixels you see on your screen.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;HTML ──────► DOM
               │
               │
CSS ───────► CSSOM
               │
               ▼
          Render Tree
               │
               ▼
            Layout
               │
               ▼
             Paint
               │
               ▼
          Compositing
               │
               ▼
             Pixels
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;And whenever your application changes the page, parts of that process may happen again. That’s why understanding browser rendering is so useful. It transforms frontend performance from a collection of mysterious rules into something you can reason about.&lt;/p&gt;





&lt;h2 id="final-thoughts"&gt;Final Thoughts&lt;/h2&gt;

&lt;p&gt;Frontend development is often taught from the framework downward. Learn React. Learn Vue. Learn Angular. Learn Next.js. Those tools are useful, but underneath every one of them sits the browser.&lt;/p&gt;

&lt;p&gt;The browser doesn’t know that your application uses React. It doesn’t care whether your state came from Redux, Zustand, Pinia, signals, or a server component. Eventually, something needs to become HTML, styles, layout, drawing instructions, and pixels.&lt;/p&gt;

&lt;p&gt;Understanding that journey gives you a foundation that isn’t tied to any particular framework. The next time an animation stutters, a page becomes sluggish after a DOM update, or a seemingly harmless CSS change causes unexpected performance problems, you’ll have a much better question to ask than:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;“Why is the browser slow?”&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You can ask:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;“Which part of the rendering pipeline am I forcing the browser to repeat?”&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And that’s a much more useful question.&lt;/p&gt;





&lt;h2 id="whats-next"&gt;What’s Next?&lt;/h2&gt;

&lt;p&gt;We’ve seen how the browser transforms HTML and CSS into pixels. But we also discovered something interesting along the way. Changing certain properties can force the browser to calculate layout again. Other changes may require repainting. Some animations can avoid much of that work and happen primarily during compositing.&lt;/p&gt;

&lt;p&gt;So what exactly makes one visual update more expensive than another?&lt;/p&gt;

&lt;p&gt;That’s where we’ll go next in &lt;strong&gt;Beyond the UI&lt;/strong&gt;:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;Reflow and Repaint Explained: Why Some UI Updates Are Expensive&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;We’ll explore what actually happens when the DOM changes, how layout thrashing occurs, why certain CSS properties are more expensive to animate, and how to build interfaces that remain smooth even as they become more complex.&lt;/p&gt;

</description>
      <category>frontend</category>
      <category>performance</category>
    </item>
    <item>
      <title>Event-Driven Architecture Explained: Building Systems That React to Events</title>
      <dc:creator>Billy Okeyo</dc:creator>
      <pubDate>Fri, 31 Jul 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/billy_de_cartel/event-driven-architecture-explained-building-systems-that-react-to-events-4i84</link>
      <guid>https://dev.to/billy_de_cartel/event-driven-architecture-explained-building-systems-that-react-to-events-4i84</guid>
      <description>&lt;blockquote&gt;
  &lt;p&gt;&lt;em&gt;“The most scalable systems don’t constantly ask what’s happening. They simply react when something happens.”&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Imagine you’re building an online marketplace. A customer places an order. At first, the workflow seems straightforward. The application creates the order, charges the customer’s card, reserves inventory, schedules shipment, sends a confirmation email, updates analytics, awards loyalty points, and notifies the warehouse. A traditional implementation might have the Order Service call each of these services directly.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Order Service

     │

     ├── Payment Service

     ├── Inventory Service

     ├── Shipping Service

     ├── Email Service

     ├── Analytics Service

     └── Loyalty Service
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;At first, this architecture feels perfectly reasonable. Each service performs its work and returns a response. As the business grows, however, the Order Service slowly becomes responsible for more and more integrations. Marketing introduces a Recommendation Service, finance adds an Accounting Service, customer success wants a CRM integration, and fraud detection joins the platform. Before long, every new feature requires modifying the Order Service. A service that originally knew only how to create orders now knows almost everything about the entire company. The result is tight coupling. Every new integration increases complexity, every downstream outage affects the original request, and every deployment becomes slightly more risky.&lt;/p&gt;

&lt;p&gt;Eventually, developers begin asking a different question. Instead of asking:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;“Which services should I call?”&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;They begin asking:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;“What happened?”&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That small change in thinking fundamentally changes the architecture. Instead of directly calling every interested service, the Order Service simply announces:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;“An order has been created.”&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It doesn’t care who listens and doesn’t even know who listens. It simply publishes an event, and every interested service reacts independently. The Shipping Service prepares delivery, the Email Service sends a confirmation, analytics records another sale, loyalty awards points, fraud detection evaluates the transaction, and recommendation engines update customer preferences. The Order Service never calls any of them directly.&lt;/p&gt;

&lt;p&gt;This architectural style is known as &lt;strong&gt;Event-Driven Architecture (EDA)&lt;/strong&gt;. Instead of services communicating through tightly coupled request-response interactions, they communicate through events describing things that have already happened. This shift may seem subtle, but in reality, it completely changes how distributed systems evolve, scale, and recover from failure.&lt;/p&gt;





&lt;h3 id="what-is-an-event"&gt;What Is an Event?&lt;/h3&gt;

&lt;p&gt;Before discussing Event-Driven Architecture, it’s important to understand what an event actually is. An event is simply a record that something meaningful has already happened. It is not something that might happen, and not something another service should do, but something that has already occurred. Examples include:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Order Created&lt;/li&gt;
  &lt;li&gt;Payment Completed&lt;/li&gt;
  &lt;li&gt;Customer Registered&lt;/li&gt;
  &lt;li&gt;Loan Approved&lt;/li&gt;
  &lt;li&gt;Inventory Reserved&lt;/li&gt;
  &lt;li&gt;Shipment Delivered&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Notice the wording: every event is written in the past tense because events describe facts. Once an event has occurred, it becomes part of the system’s history. Unlike commands, events don’t tell another service what to do. They simply communicate what has already happened. This distinction is incredibly important. Consider the difference between these two messages.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Command

Charge Customer
&lt;/code&gt;&lt;/pre&gt;

&lt;pre&gt;&lt;code&gt;Event

Customer Charged
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The first message expects another service to perform work; the second informs the rest of the system that the work has already been completed. Commands ask; events announce. That simple distinction lies at the heart of Event-Driven Architecture.&lt;/p&gt;





&lt;h3 id="how-event-driven-systems-work"&gt;How Event-Driven Systems Work&lt;/h3&gt;

&lt;p&gt;Imagine a borrower submits a loan application. The Loan Service validates the request, stores the application, and commits its transaction. Rather than directly calling every downstream service, it publishes a &lt;strong&gt;LoanApplicationSubmitted&lt;/strong&gt; event, and from there every interested service works independently.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Loan Service

      │

LoanApplicationSubmitted

      │

──────── Event Broker ────────

      │

      ├── Risk Service

      ├── Notification Service

      ├── Fraud Detection

      ├── CRM

      └── Analytics
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Notice what changed: the Loan Service no longer knows anything about these downstream systems. Adding another consumer doesn’t require modifying the Loan Service, and removing one doesn’t either. Every service simply subscribes to the events it cares about. That loose coupling is one of the defining advantages of Event-Driven Architecture.&lt;/p&gt;





&lt;h3 id="why-this-improves-scalability"&gt;Why This Improves Scalability&lt;/h3&gt;

&lt;p&gt;Suppose your application suddenly doubles in size. Marketing introduces three new services, operations introduces two more, and finance adds another. In a request-response architecture, every one of those integrations often requires modifying the originating service. In an event-driven architecture, nothing changes. The new services simply subscribe to existing events, and the publisher remains exactly the same. This dramatically reduces coupling and allows applications to evolve much more independently. Instead of building systems that know about one another, you’re building systems that share facts. That distinction becomes increasingly valuable as applications grow.&lt;/p&gt;

&lt;h3 id="event-brokers-the-backbone-of-event-driven-systems"&gt;Event Brokers: The Backbone of Event-Driven Systems&lt;/h3&gt;

&lt;p&gt;At this point, a natural question arises. If services no longer call one another directly, how do events actually travel through the system? The answer is an &lt;strong&gt;event broker&lt;/strong&gt;. An event broker acts as the central communication hub for your architecture. Instead of sending events directly to every interested service, a publisher sends the event to the broker, and the broker becomes responsible for delivering it to every subscriber. Conceptually, the architecture looks like this:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;                Order Service

                     │

      OrderCreated Event

                     │

                     ▼

             Event Broker

     ┌─────────┼─────────┐

     ▼         ▼         ▼

 Inventory   Shipping   Analytics

   Service    Service     Service
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Notice what has disappeared: the Order Service no longer knows how many consumers exist, doesn’t know whether the Analytics Service is online, and doesn’t know whether another team introduces a Recommendation Service next month. Its only responsibility is publishing the event. Everything else becomes someone else’s concern. Several technologies can act as event brokers. Apache Kafka is widely used for high-throughput event streaming. RabbitMQ is popular for traditional message queuing. Cloud platforms offer managed services such as Amazon EventBridge, Amazon SQS, Azure Service Bus, and Google Pub/Sub. Each has different strengths, but they all serve the same purpose: moving events from producers to consumers without tightly coupling the two.&lt;/p&gt;





&lt;h3 id="why-event-driven-architecture-matters"&gt;Why Event-Driven Architecture Matters&lt;/h3&gt;

&lt;p&gt;At first glance, publishing events instead of calling APIs might not seem like a revolutionary change. In practice, however, it fundamentally changes how software evolves. Consider our online marketplace again. The Order Service publishes an &lt;strong&gt;OrderCreated&lt;/strong&gt; event. Initially, only three services consume it.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Shipping&lt;/li&gt;
  &lt;li&gt;Email&lt;/li&gt;
  &lt;li&gt;Inventory&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A year later, the business introduces:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Fraud Detection&lt;/li&gt;
  &lt;li&gt;Recommendation Engine&lt;/li&gt;
  &lt;li&gt;Customer Rewards&lt;/li&gt;
  &lt;li&gt;CRM Integration&lt;/li&gt;
  &lt;li&gt;Business Intelligence&lt;/li&gt;
  &lt;li&gt;Machine Learning&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Order Service doesn’t change. It continues publishing exactly the same event, and the new services simply subscribe. This is one of the greatest strengths of Event-Driven Architecture: applications grow by adding consumers rather than modifying existing publishers, which greatly reduces the ripple effect of change.&lt;/p&gt;





&lt;h3 id="the-trade-offs"&gt;The Trade-Offs&lt;/h3&gt;

&lt;p&gt;Like every architectural style, Event-Driven Architecture solves some problems while introducing others. One important difference is that communication becomes asynchronous. When a customer places an order, the application may respond immediately even though several downstream services are still processing events. This improves responsiveness but introduces &lt;strong&gt;eventual consistency&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For a short period, different services may have slightly different views of the system. The order may already exist while the reporting dashboard hasn’t yet been updated, or the shipment may still be pending while the payment has already completed. This isn’t necessarily a problem; it’s simply a different consistency model, and applications must be designed with that reality in mind. Debugging also becomes more challenging. In a request-response architecture, tracing a workflow often means following a sequence of API calls. In an event-driven architecture, the workflow is distributed across many independent services reacting to events at different times, so good observability becomes essential. Correlation IDs, distributed tracing, structured logging, and monitoring tools become increasingly valuable as systems grow.&lt;/p&gt;





&lt;h3 id="common-mistakes"&gt;Common Mistakes&lt;/h3&gt;

&lt;p&gt;One of the biggest mistakes teams make is publishing events for everything. Not every database update deserves an event. Good events represent meaningful business occurrences. Examples include:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Customer Registered&lt;/li&gt;
  &lt;li&gt;Order Completed&lt;/li&gt;
  &lt;li&gt;Loan Approved&lt;/li&gt;
  &lt;li&gt;Payment Received&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Poor events often expose internal implementation details. Examples include:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;CustomerTableUpdated&lt;/li&gt;
  &lt;li&gt;RowModified&lt;/li&gt;
  &lt;li&gt;AddressFieldChanged&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Consumers should care about business facts, not database implementation. Another common mistake is assuming events are delivered exactly once. In reality, duplicates can occur, messages can be delayed, and consumers can retry. This is why patterns we’ve already explored, such as &lt;strong&gt;Idempotency&lt;/strong&gt;, remain critically important. Reliable event-driven systems assume events may be delivered more than once and design consumers accordingly. Finally, avoid replacing every API with events. Not every interaction should be asynchronous. Some operations naturally require an immediate response. For example:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;User authentication&lt;/li&gt;
  &lt;li&gt;Payment authorization&lt;/li&gt;
  &lt;li&gt;Real-time validation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A healthy architecture often combines synchronous APIs with asynchronous events, using each where it makes the most sense.&lt;/p&gt;





&lt;h3 id="request-response-vs-event-driven"&gt;Request-Response vs Event-Driven&lt;/h3&gt;

&lt;p&gt;A useful way to compare these architectures is to think about who controls the conversation.&lt;/p&gt;

&lt;p&gt;In a request-response system, one service explicitly asks another service to perform work, and the caller waits for an answer before continuing. In an event-driven system, a service simply announces what has already happened. Anyone interested may react, and anyone uninterested simply ignores the event. Neither architecture is universally better. Request-response communication is often simpler and easier to understand, while event-driven communication provides greater flexibility and scalability as systems become more complex. Most modern applications use both. User-facing requests frequently begin as synchronous API calls, and once the business transaction completes, the application publishes events that allow the rest of the system to react independently.&lt;/p&gt;





&lt;h3 id="how-everything-fits-together"&gt;How Everything Fits Together&lt;/h3&gt;

&lt;p&gt;If you’ve followed this series from the beginning, you’ve probably noticed a pattern. Each article answered a question created by the previous one.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Concept&lt;/th&gt;
      &lt;th&gt;Question It Answers&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Idempotency&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;What if the same request arrives twice?&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Race Conditions&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;What if multiple requests modify the same data?&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Database Transactions&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;How do I keep related database operations atomic?&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Isolation Levels&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;What should concurrent transactions be allowed to see?&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Distributed Locks&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;How do multiple application instances coordinate work?&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Outbox Pattern&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;How do I reliably publish events after committing data?&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Saga Pattern&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;How do multiple services complete one business process?&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;CQRS&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Should reads and writes use the same model?&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Event-Driven Architecture&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;How do independent services communicate and evolve together?&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Notice that none of these patterns exists in isolation. An event-driven application might use:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
&lt;strong&gt;Idempotency&lt;/strong&gt; to safely handle duplicate events.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Transactions&lt;/strong&gt; to protect local database operations.&lt;/li&gt;
  &lt;li&gt;The &lt;strong&gt;Outbox Pattern&lt;/strong&gt; to reliably publish domain events.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Saga Pattern&lt;/strong&gt; to coordinate long-running business workflows.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;CQRS&lt;/strong&gt; to optimize read and write workloads independently.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Distributed Locks&lt;/strong&gt; where multiple application instances must coordinate exclusive work.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The real power doesn’t come from mastering one pattern. It comes from understanding how they complement one another.&lt;/p&gt;





&lt;h3 id="final-thoughts"&gt;Final Thoughts&lt;/h3&gt;

&lt;p&gt;Software architecture isn’t about collecting design patterns. It’s about solving real problems with the right level of complexity. Event-Driven Architecture has become popular because it reflects how modern organizations grow. Teams become independent. Services evolve at different speeds. New features appear continuously. Direct dependencies become increasingly expensive to maintain. By allowing services to communicate through events rather than tightly coupled API calls, Event-Driven Architecture enables systems that are more flexible, more scalable, and more resilient to change. Like every pattern we’ve explored, however, it isn’t a silver bullet. Small applications may never need an event broker, and a well-designed monolith may outperform a poorly designed event-driven system. Architecture should always follow business needs, not trends. The goal isn’t to build the most sophisticated system possible. It’s to build the simplest system capable of solving today’s problem while leaving room for tomorrow’s growth.&lt;/p&gt;





&lt;h3 id="beyond-crud-chapter-one-complete"&gt;Beyond CRUD: Chapter One Complete&lt;/h3&gt;

&lt;p&gt;When we began this series, we started with a deceptively simple question:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;What happens if the same request arrives twice?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;From there, we explored race conditions, transactions, concurrency, distributed coordination, reliable messaging, long-running workflows, scalable read models, and event-driven communication. Each article introduced a new piece of the puzzle, and together they form a foundation for understanding how modern backend systems remain reliable under retries, failures, concurrency, and scale.&lt;/p&gt;

&lt;p&gt;If there’s one lesson to carry forward, it’s this:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;Reliable software isn’t built by avoiding failure. It’s built by expecting failure, understanding where it can occur, and designing systems that recover gracefully when it does.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That mindset, more than any single framework, language, or database, is what separates production-ready systems from code that only works under perfect conditions. The journey beyond CRUD doesn’t end here. It begins here.&lt;/p&gt;

</description>
      <category>distributedsystems</category>
      <category>concurrency</category>
    </item>
    <item>
      <title>CQRS Explained: Separating Reads and Writes for Scalable Systems</title>
      <dc:creator>Billy Okeyo</dc:creator>
      <pubDate>Mon, 27 Jul 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/billy_de_cartel/cqrs-explained-separating-reads-and-writes-for-scalable-systems-1f5h</link>
      <guid>https://dev.to/billy_de_cartel/cqrs-explained-separating-reads-and-writes-for-scalable-systems-1f5h</guid>
      <description>&lt;blockquote&gt;
  &lt;p&gt;&lt;em&gt;“The way you write data isn’t always the best way to read it.”&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Imagine you’re building an online marketplace. Every day, thousands of customers browse products, place orders, track deliveries, and review their purchase history. At the same time, administrators manage inventory, warehouse staff update shipments, finance teams generate reports, and recommendation engines continuously analyze customer behavior.&lt;/p&gt;

&lt;p&gt;From a user’s perspective, everything appears seamless. Behind the scenes, however, the application is performing two fundamentally different kinds of work. Some requests are &lt;strong&gt;changing&lt;/strong&gt; data: a customer places an order, an administrator updates inventory, a payment is processed, or a shipment is marked as delivered. Other requests simply &lt;strong&gt;read&lt;/strong&gt; data: customers search for products, managers open dashboards, support agents review order histories, and executives generate monthly reports.&lt;/p&gt;

&lt;p&gt;At first, it’s tempting to handle both using the same database tables and the same application models. After all, an order is an order. Whether you’re creating it or displaying it, why shouldn’t the same model work for both? For many applications, that’s exactly what happens. The same entity is used for inserting records, updating records, validating business rules, and serving data to the user interface.&lt;/p&gt;

&lt;p&gt;As applications grow, however, this approach begins to reveal its limitations. The information required to create an order is often very different from the information required to display one. Creating an order might require validating inventory, calculating taxes, applying discounts, verifying payment details, and enforcing business rules. Displaying an order, on the other hand, may require customer information, shipping updates, product images, payment status, warehouse progress, and delivery estimates, all combined into a single view optimized for the user.&lt;/p&gt;

&lt;p&gt;Trying to satisfy both responsibilities with one model often leads to increasingly complicated code. The write model becomes cluttered with fields needed only for reporting, the read model becomes constrained by rules that exist only for data modification, queries grow more complex, performance begins to suffer, and developers start adding workarounds that make the system harder to maintain.&lt;/p&gt;

&lt;p&gt;Eventually, an important realization emerges.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;The way we write data isn’t necessarily the best way to read it.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That simple observation led to a pattern known as &lt;strong&gt;Command Query Responsibility Segregation&lt;/strong&gt;, more commonly called &lt;strong&gt;CQRS&lt;/strong&gt;. Rather than forcing one model to satisfy two very different responsibilities, CQRS separates them completely. Commands become responsible for changing data, queries become responsible for reading data, and each side can then evolve independently, optimized for its own purpose. Before exploring how CQRS works, let’s first understand why combining reads and writes into the same model eventually becomes a problem.&lt;/p&gt;

&lt;h3 id="why-one-model-eventually-becomes-a-problem"&gt;Why One Model Eventually Becomes a Problem&lt;/h3&gt;

&lt;p&gt;When most applications begin, life is simple. Suppose you’re building a loan management platform. A borrower submits a loan application. The application validates the input, saves the record, and later retrieves the same information whenever a loan officer opens the application. The same model handles both writing and reading.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;LoanApplication
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Everything works perfectly. As the platform grows, however, different users begin asking for different views of the same information. A loan officer wants to see repayment history, guarantors, collateral, risk score, and supporting documents on one screen; finance wants reports showing outstanding balances grouped by branch; executives want dashboards displaying portfolio performance; and customers simply want to know whether their application has been approved.&lt;/p&gt;

&lt;p&gt;Notice what’s happening: everyone is looking at the same business entity, but nobody wants exactly the same data. To satisfy these different requirements, the application gradually starts joining more tables.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Loan

JOIN Customer

JOIN Branch

JOIN RiskAssessment

JOIN LoanOfficer

JOIN Repayments

JOIN Documents

JOIN Guarantors
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The queries become larger, response times increase, and the code becomes harder to maintain. Ironically, the information required to &lt;strong&gt;display&lt;/strong&gt; a loan has become far more complicated than the information required to &lt;strong&gt;create&lt;/strong&gt; one. This is where many systems begin struggling. The write model keeps accumulating fields that only reports need, the read model becomes constrained by business rules that only matter during updates, and eventually one model is trying to solve two completely different problems.&lt;/p&gt;





&lt;h3 id="commands-and-queries-are-different"&gt;Commands and Queries Are Different&lt;/h3&gt;

&lt;p&gt;One of the core ideas behind CQRS is recognizing that not every request has the same purpose. Some requests change data; others simply read it. These are fundamentally different operations. A &lt;strong&gt;command&lt;/strong&gt; tells the system to perform an action. For example:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Create Order&lt;/li&gt;
  &lt;li&gt;Approve Loan&lt;/li&gt;
  &lt;li&gt;Reserve Inventory&lt;/li&gt;
  &lt;li&gt;Charge Payment&lt;/li&gt;
  &lt;li&gt;Register Customer&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Commands represent intent. They usually contain business validation, enforce rules, and modify application state. A &lt;strong&gt;query&lt;/strong&gt;, on the other hand, doesn’t change anything. Its only responsibility is returning information. For example:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Get Customer Profile&lt;/li&gt;
  &lt;li&gt;List Outstanding Loans&lt;/li&gt;
  &lt;li&gt;View Order History&lt;/li&gt;
  &lt;li&gt;Search Products&lt;/li&gt;
  &lt;li&gt;Display Dashboard&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Queries don’t perform business logic. They answer questions. This distinction may seem small, but in reality, it changes how applications are designed.&lt;/p&gt;





&lt;h3 id="what-is-cqrs"&gt;What Is CQRS?&lt;/h3&gt;

&lt;p&gt;CQRS stands for &lt;strong&gt;Command Query Responsibility Segregation&lt;/strong&gt;. Despite the intimidating name, the underlying idea is surprisingly simple. Instead of using one model for everything, CQRS separates the write side from the read side. Commands become responsible for modifying data, and queries become responsible for retrieving it. Conceptually, the architecture looks like this.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;                Application

                     │

      ┌──────────────┴──────────────┐

      │                             │

   Commands                     Queries

      │                             │

Write Model                  Read Model

      │                             │

Database                 Optimized View
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The important thing to notice is that the read model no longer has to look like the write model. Each side is free to evolve independently.&lt;/p&gt;





&lt;h3 id="the-write-model"&gt;The Write Model&lt;/h3&gt;

&lt;p&gt;The write model exists to protect business rules. Suppose a customer places an order. The application needs to verify inventory, calculate discounts, validate payment, reserve stock, and create the order. All of those steps belong on the write side. The write model isn’t concerned with how information will later appear on a dashboard. Its only responsibility is ensuring the business operation is correct. Think of it as the gatekeeper for your data. Nothing enters the system without passing through the write model.&lt;/p&gt;





&lt;h3 id="the-read-model"&gt;The Read Model&lt;/h3&gt;

&lt;p&gt;The read model has a completely different job. Its responsibility isn’t enforcing business rules, it’s returning information as efficiently as possible. Imagine displaying an order summary. The customer expects to see:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Order number&lt;/li&gt;
  &lt;li&gt;Customer name&lt;/li&gt;
  &lt;li&gt;Product images&lt;/li&gt;
  &lt;li&gt;Shipping status&lt;/li&gt;
  &lt;li&gt;Payment status&lt;/li&gt;
  &lt;li&gt;Delivery estimate&lt;/li&gt;
  &lt;li&gt;Total price&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The write model probably stores all of this across multiple tables, but the read model doesn’t have to. Instead, it can store exactly the shape required by the user interface, so instead of executing six joins every time someone opens an order, the read model may already contain everything in one place.&lt;/p&gt;

&lt;p&gt;This dramatically simplifies queries while improving performance.&lt;/p&gt;





&lt;h3 id="why-this-improves-performance"&gt;Why This Improves Performance&lt;/h3&gt;

&lt;p&gt;Suppose an online store receives ten thousand requests every minute. Only five hundred of those requests create or update orders. The remaining nine thousand five hundred simply display information. Traditional CRUD applications often force both workloads through the same models and database structures. CQRS recognizes that reads and writes have completely different characteristics. Reads are usually far more frequent, while writes are usually more complicated. By separating them, each side can be optimized independently: the write model focuses on correctness, the read model focuses on speed, and neither compromises the other.&lt;/p&gt;





&lt;h3 id="a-real-world-example"&gt;A Real-World Example&lt;/h3&gt;

&lt;p&gt;Think about YouTube. Uploading a video and watching a video are two completely different operations. Uploading requires validation, virus scanning, metadata extraction, thumbnail generation, transcoding, and storage. Watching a video requires none of those things. The viewer simply wants the video to start playing immediately. Trying to optimize both operations using exactly the same model would make little sense.&lt;/p&gt;

&lt;p&gt;CQRS applies the same principle to business applications. The model responsible for creating data doesn’t have to be the same model responsible for presenting it. Recognizing that difference is the first step toward understanding why CQRS has become such a popular architectural pattern in modern backend systems.&lt;/p&gt;





&lt;p&gt;At this point, we’ve separated reads from writes conceptually. The next question naturally follows:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;If the read model and write model are separate, how do they stay synchronized?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3 id="keeping-the-read-model-up-to-date"&gt;Keeping the Read Model Up to Date&lt;/h3&gt;

&lt;p&gt;One of the first questions developers ask after learning about CQRS is:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;“If my read model is separate from my write model, how does it stay up to date?”&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The answer depends on the architecture, but in most modern systems, the read model is updated using &lt;strong&gt;events&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Suppose a customer places an order. The write model validates the request, checks inventory, processes payment, and commits the transaction. Once the transaction succeeds, an event such as &lt;strong&gt;OrderCreated&lt;/strong&gt; is published, and one or more components responsible for maintaining the read model receive that event and update their own optimized view of the data.&lt;/p&gt;

&lt;p&gt;Conceptually, the flow looks like this:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Customer

      │

      ▼

Command

(Create Order)

      │

      ▼

Write Model

      │

      ▼

Database

      │

      ▼

OrderCreated Event

      │

      ▼

Read Model Updated

      │

      ▼

User Queries Data
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Notice something important: the user’s query never touches the write model. Instead, it reads from a model specifically designed for displaying information.&lt;/p&gt;





&lt;h3 id="eventual-consistency"&gt;Eventual Consistency&lt;/h3&gt;

&lt;p&gt;Because the read model is updated after the write completes, there is usually a short delay before the latest information becomes visible. This is known as &lt;strong&gt;eventual consistency&lt;/strong&gt;. Imagine placing an order on a large e-commerce platform. The checkout page immediately confirms that your purchase was successful, but if you refresh your order history a fraction of a second later, the new order might not appear immediately. A moment later, it does. Nothing is wrong: the write completed instantly, and the read model simply needed a short amount of time to catch up. For many applications, this tiny delay is perfectly acceptable. Users rarely notice a difference measured in milliseconds or even a few seconds, and the benefit is that reads become dramatically faster and easier to scale.&lt;/p&gt;





&lt;h3 id="does-cqrs-require-microservices"&gt;Does CQRS Require Microservices?&lt;/h3&gt;

&lt;p&gt;One of the biggest misconceptions about CQRS is that it only works in microservice architectures. It doesn’t. CQRS is simply a design pattern. A single monolithic application can separate its command handlers from its query handlers just as effectively as a distributed system. Likewise, CQRS doesn’t require Kafka, RabbitMQ, Event Sourcing, or multiple databases. Many applications implement CQRS using a single database while maintaining separate command and query models inside the same application. As systems grow, those models may eventually evolve into separate databases or services, but that’s a scaling decision, not a requirement of the pattern itself.&lt;/p&gt;





&lt;h3 id="when-should-you-use-cqrs"&gt;When Should You Use CQRS?&lt;/h3&gt;

&lt;p&gt;CQRS isn’t a solution to every problem. Many applications work perfectly well using traditional CRUD architecture. If your application has simple business rules, relatively small datasets, and straightforward queries, introducing CQRS often adds unnecessary complexity. On the other hand, CQRS becomes increasingly valuable when reads and writes have very different characteristics. Common examples include:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;High-traffic e-commerce platforms.&lt;/li&gt;
  &lt;li&gt;Banking and financial systems.&lt;/li&gt;
  &lt;li&gt;Loan management platforms.&lt;/li&gt;
  &lt;li&gt;Logistics and supply chain applications.&lt;/li&gt;
  &lt;li&gt;Reporting and analytics dashboards.&lt;/li&gt;
  &lt;li&gt;SaaS products with complex administrative views.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In these systems, the information required to display data is often very different from the information required to modify it. Separating those responsibilities makes the application easier to optimize and easier to maintain.&lt;/p&gt;





&lt;h3 id="cqrs-vs-traditional-crud"&gt;CQRS vs Traditional CRUD&lt;/h3&gt;

&lt;p&gt;A useful way to understand CQRS is to compare it with the architecture most developers already know.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Traditional CRUD&lt;/th&gt;
      &lt;th&gt;CQRS&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;One model for reads and writes&lt;/td&gt;
      &lt;td&gt;Separate models for reads and writes&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Simpler to build&lt;/td&gt;
      &lt;td&gt;More flexible at scale&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Easier for small systems&lt;/td&gt;
      &lt;td&gt;Better suited for complex domains&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Queries often become increasingly complex&lt;/td&gt;
      &lt;td&gt;Read models are optimized for specific use cases&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Business rules and presentation concerns frequently mix together&lt;/td&gt;
      &lt;td&gt;Responsibilities remain clearly separated&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Neither approach is universally better. CRUD is an excellent choice for many applications. CQRS becomes valuable when the complexity of the domain begins to outweigh the simplicity of using a single model.&lt;/p&gt;





&lt;h3 id="common-mistakes"&gt;Common Mistakes&lt;/h3&gt;

&lt;p&gt;One mistake developers frequently make is adopting CQRS simply because they’ve heard it’s a “best practice.” Like every architectural pattern, CQRS introduces additional moving parts. You’ll often have separate models, additional event handling, and the possibility of eventual consistency. If those complexities don’t solve a real business problem, they’re simply unnecessary overhead. Another common mistake is trying to create one read model that satisfies every possible screen. The real strength of CQRS lies in allowing each query to have a model optimized for its own purpose. A customer dashboard, an administrative report, and a mobile application may each deserve different read models. Finally, don’t confuse CQRS with Event Sourcing. Although the two patterns are often used together, they solve different problems. CQRS separates reads from writes, while Event Sourcing stores state as a sequence of events. You can implement one without the other.&lt;/p&gt;





&lt;h3 id="bringing-it-all-together"&gt;Bringing It All Together&lt;/h3&gt;

&lt;p&gt;Throughout this series, we’ve gradually built a collection of patterns that solve different reliability and scalability challenges.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Concept&lt;/th&gt;
      &lt;th&gt;Question It Answers&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Idempotency&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;What if the same request arrives twice?&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Race Conditions&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;What if multiple requests modify the same data simultaneously?&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Database Transactions&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;How do I keep database operations atomic?&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Isolation Levels&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;What should concurrent transactions be allowed to see?&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Distributed Locks&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;How do multiple application instances coordinate work?&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Outbox Pattern&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;How do I reliably publish events after committing data?&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Saga Pattern&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;How do multiple services complete one business process reliably?&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;CQRS&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Should the same model be responsible for both reading and writing data?&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Notice how the series has gradually expanded in scope. We began by making individual API requests reliable, then learned how to coordinate transactions, communicate between services, and manage distributed business workflows. CQRS adds another important lesson: sometimes the best way to scale an application isn’t by making one model do everything, but by giving different responsibilities to different models.&lt;/p&gt;





&lt;h3 id="final-thoughts"&gt;Final Thoughts&lt;/h3&gt;

&lt;p&gt;One of the biggest lessons in software architecture is that different problems deserve different solutions. Reading data and writing data may involve the same business entity, but they rarely have the same requirements. Writes prioritize correctness, validation, and enforcing business rules; reads prioritize speed, simplicity, and delivering information in the shape users actually need. CQRS embraces this difference instead of trying to hide it. For small applications, a traditional CRUD architecture is often the right choice. As systems become larger and business requirements become more demanding, separating reads from writes can lead to simpler queries, clearer responsibilities, and applications that scale far more gracefully. Like every pattern we’ve explored in the &lt;strong&gt;Beyond CRUD&lt;/strong&gt; series, CQRS isn’t about making software more complicated. It’s about choosing the right level of complexity to solve the problem in front of you.&lt;/p&gt;





&lt;h3 id="whats-next"&gt;What’s Next?&lt;/h3&gt;

&lt;p&gt;So far, we’ve explored several patterns that improve reliability, scalability, and maintainability. One question still remains: how do all these patterns fit together to build applications where services communicate entirely through events instead of direct API calls?&lt;/p&gt;

&lt;p&gt;In the next article, we’ll explore &lt;strong&gt;Event-Driven Architecture Explained: Building Systems That React to Events&lt;/strong&gt;, where we’ll connect concepts like the Outbox Pattern, Saga Pattern, and CQRS into a cohesive architectural style used by many modern distributed systems.&lt;/p&gt;

</description>
      <category>distributedsystems</category>
      <category>concurrency</category>
    </item>
    <item>
      <title>Saga Pattern Explained: Managing Distributed Transactions Across Microservices</title>
      <dc:creator>Billy Okeyo</dc:creator>
      <pubDate>Fri, 24 Jul 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/billy_de_cartel/saga-pattern-explained-managing-distributed-transactions-across-microservices-3o1</link>
      <guid>https://dev.to/billy_de_cartel/saga-pattern-explained-managing-distributed-transactions-across-microservices-3o1</guid>
      <description>&lt;blockquote&gt;
  &lt;p&gt;&lt;em&gt;“A transaction can roll back one database. A Saga coordinates many databases that have never even met.”&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Imagine you’re building an online marketplace. A customer clicks &lt;strong&gt;Place Order&lt;/strong&gt;, expecting what feels like a single operation. Behind the scenes, however, that simple button sets off a chain of events involving several independent services. The Order Service creates a new order, the Inventory Service reserves the purchased items, the Payment Service charges the customer’s card, the Shipping Service schedules delivery, and the Notification Service sends a confirmation email.&lt;/p&gt;

&lt;p&gt;From the customer’s perspective, it’s one transaction. From your system’s perspective, it’s anything but. Each service owns its own database, each commits its own transaction independently, none of them has direct control over the others, and no single database transaction can span all of them.&lt;/p&gt;

&lt;p&gt;Now imagine the following sequence: the Order Service successfully creates the order, the Inventory Service reserves the last laptop in stock, and the Payment Service charges the customer’s credit card. Then, just as the Shipping Service begins preparing the shipment, it discovers that delivery isn’t available to the customer’s location. The order already exists, the customer’s card has already been charged, and inventory has already been reserved, but the shipment can never be created. Unlike a traditional database transaction, there is no single &lt;strong&gt;ROLLBACK&lt;/strong&gt; command capable of undoing work performed across multiple independent databases.&lt;/p&gt;

&lt;p&gt;This is one of the defining challenges of distributed systems. As applications evolve from monoliths into microservices, business processes increasingly span services that are independently deployed, independently scaled, and independently owned. While this architecture offers tremendous flexibility, it also introduces a difficult question:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;How do you maintain business consistency when a workflow spans multiple services and one of them fails halfway through?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The answer is not a larger transaction. It’s a different way of thinking. Instead of trying to make every service commit simultaneously, modern distributed systems break long-running business processes into a sequence of smaller local transactions. If every step succeeds, the workflow completes successfully. If a step fails, previously completed work is undone using carefully designed &lt;strong&gt;compensating actions&lt;/strong&gt; rather than database rollbacks.&lt;/p&gt;

&lt;p&gt;This approach is known as the &lt;strong&gt;Saga Pattern&lt;/strong&gt;. Like the Outbox Pattern, the Saga Pattern embraces the reality that failures are inevitable. Rather than pretending distributed transactions behave like local database transactions, it provides a practical way to recover when things don’t go according to plan. Before we explore how Sagas work, it’s important to understand why traditional transactions stop working once your application crosses service boundaries.&lt;/p&gt;





&lt;h3 id="why-database-transactions-dont-scale-across-microservices"&gt;Why Database Transactions Don’t Scale Across Microservices&lt;/h3&gt;

&lt;p&gt;Earlier in this series, we explored database transactions and learned how they guarantee that multiple operations either succeed together or fail together. If an online banking application deducts money from one account and credits another within the same database, a transaction ensures that both operations are treated as a single unit of work. If anything fails before the transaction commits, every change is rolled back automatically, leaving the database in a consistent state.&lt;/p&gt;

&lt;p&gt;That model works beautifully when everything happens inside one database, but microservices change the picture completely. Imagine an order workflow involving four independent services.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Customer

      │

      ▼

Order Service

      │

      ▼

Inventory Service

      │

      ▼

Payment Service

      │

      ▼

Shipping Service
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Each service owns its own database. The Order Service cannot directly roll back changes made by the Payment Service, the Payment Service cannot undo inventory reservations, and the Shipping Service has no authority over the Order database. Every service commits its own transaction independently. This independence is one of the greatest strengths of microservices, and it’s also one of their biggest challenges. Suppose the workflow progresses like this:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Create Order ✅

↓

Reserve Inventory ✅

↓

Charge Payment ✅

↓

Create Shipment ❌
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;At this point, three services have already committed their work, and nothing can simply “roll back.” Unlike a single database transaction, there is no global undo button. This challenge is often referred to as a &lt;strong&gt;distributed transaction&lt;/strong&gt;, and solving it using traditional database techniques quickly becomes impractical.&lt;/p&gt;

&lt;p&gt;Years ago, distributed systems experimented with approaches such as &lt;strong&gt;Two-Phase Commit (2PC)&lt;/strong&gt;, where every participating system agreed to either commit or roll back together. While theoretically elegant, 2PC introduced significant coordination overhead, reduced availability, increased latency, and created situations where entire systems could become blocked waiting for slow or unavailable participants. Modern cloud-native architectures generally avoid this approach. Instead of attempting to make every service commit simultaneously, they accept that each service commits independently and focus on coordinating the overall business process.&lt;/p&gt;

&lt;p&gt;That’s exactly what the Saga Pattern does. Instead of one large transaction, a Saga is a sequence of smaller transactions linked together by business logic. If every step succeeds, the Saga completes successfully; if one step fails, previously completed steps are compensated through additional business actions designed to reverse their effects. That distinction is subtle but incredibly important: a Saga doesn’t roll back database transactions, it performs new transactions whose purpose is to restore the system to a valid business state. Understanding that difference is the key to understanding everything else about the Saga Pattern.&lt;/p&gt;

&lt;h3 id="understanding-compensating-transactions"&gt;Understanding Compensating Transactions&lt;/h3&gt;

&lt;p&gt;One of the biggest misconceptions developers have when they first encounter the Saga Pattern is assuming it somehow provides a distributed version of &lt;code&gt;ROLLBACK&lt;/code&gt;. It doesn’t, and in fact, that’s one of the defining characteristics of a Saga. Once a service commits its local transaction, that transaction is permanent. The database has already saved the changes, and there is no mechanism for another service to rewind history. Instead of rolling back completed work, a Saga performs &lt;strong&gt;compensating transactions&lt;/strong&gt;. A compensating transaction is simply another business operation whose purpose is to undo the effects of a previous one.&lt;/p&gt;

&lt;p&gt;Suppose an order has already been created, inventory has been reserved, and payment has been successfully processed. If shipping later fails because the customer’s address falls outside the delivery area, the system cannot ask every database to roll back, because those transactions finished long ago. Instead, the application performs a series of new operations: the payment service issues a refund, the inventory service releases the reserved stock, and the order service changes the order status from &lt;strong&gt;Pending&lt;/strong&gt; to &lt;strong&gt;Cancelled&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Notice something important: nothing has been deleted and nothing has been rolled back. The system simply performs additional work that restores the business to a valid state.&lt;/p&gt;

&lt;p&gt;Conceptually, the workflow now looks like this:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Create Order ✅

↓

Reserve Inventory ✅

↓

Charge Payment ✅

↓

Create Shipment ❌

↓

Refund Payment

↓

Release Inventory

↓

Cancel Order
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This is the heart of the Saga Pattern. Rather than pretending failures never happened, the system accepts them and responds with carefully designed business actions. This approach is much more realistic because, in distributed systems, failures aren’t exceptional, they’re inevitable.&lt;/p&gt;





&lt;h3 id="a-banking-example"&gt;A Banking Example&lt;/h3&gt;

&lt;p&gt;Imagine a customer applies for a personal loan. Several independent services participate in the approval process: the Loan Service creates the application, the Credit Service performs a credit check, the Risk Service evaluates affordability, and the Notification Service informs the customer of the decision. Everything proceeds normally until the Risk Service determines that the customer’s debt-to-income ratio exceeds the organization’s lending policy. At this point, the application cannot continue. If this were a single database transaction, we would simply issue a rollback.&lt;/p&gt;

&lt;p&gt;In a microservice architecture, however, the Loan Service has already committed the new application and the Credit Service has already stored the completed credit assessment. Neither service can magically erase its work because another service encountered a problem. Instead, the Saga performs compensating actions: the Loan Service marks the application as withdrawn, the Credit Service archives its assessment, and the Notification Service informs the customer that the application could not proceed. Each action is itself a normal transaction, and collectively they restore the overall business process to a consistent state.&lt;/p&gt;





&lt;h3 id="rollback-vs-compensation"&gt;Rollback vs Compensation&lt;/h3&gt;

&lt;p&gt;Although these ideas sound similar, they’re fundamentally different. A database rollback behaves like this:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;BEGIN

↓

Update Balance

↓

Insert Payment

↓

Failure

↓

ROLLBACK
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;When the rollback occurs, the database behaves as though none of the changes ever happened. It’s as if the transaction never existed. A Saga works very differently.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Reserve Inventory

↓

Charge Payment

↓

Shipment Fails

↓

Refund Payment

↓

Release Inventory
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The original payment really happened, the refund also really happened, and both become part of the permanent history of the system. That’s an important distinction, and many business domains actually require this behavior. Consider financial systems. Deleting payment records would make auditing impossible, so recording both the payment and the subsequent refund creates a complete, traceable history of what occurred. Compensation isn’t about pretending mistakes never happened, it’s about correcting them transparently.&lt;/p&gt;





&lt;h3 id="designing-good-compensating-actions"&gt;Designing Good Compensating Actions&lt;/h3&gt;

&lt;p&gt;Writing a compensating transaction isn’t simply a matter of reversing database changes. You’re reversing business operations. For example, suppose a hotel booking system reserves a room.&lt;/p&gt;

&lt;p&gt;The compensating action isn’t:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Delete reservation row.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It’s:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Release the room back into available inventory.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Similarly, if an airline charges a customer’s credit card, the compensation isn’t:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Delete payment record.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It’s:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Create a refund transaction.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This distinction matters because business systems are usually audited. Historical events should remain visible, and what changes is the current business state. Whenever you design a Saga, a useful question to ask is:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;“If this step succeeds but a later step fails, what business action restores the system to a valid state?”&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Thinking in terms of business operations rather than database updates leads to much more reliable designs.&lt;/p&gt;





&lt;h3 id="every-step-is-independent"&gt;Every Step Is Independent&lt;/h3&gt;

&lt;p&gt;Another characteristic of Sagas is that every step represents a complete, independent transaction. Suppose an order workflow consists of four services.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Order Service

↓

Inventory Service

↓

Payment Service

↓

Shipping Service
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Each service commits its own database transaction before the next service begins, which means failures are isolated. If the Shipping Service experiences an outage, it doesn’t corrupt the Payment Service’s database; likewise, if the Payment Service fails, it doesn’t leave the Inventory Service with an unfinished SQL transaction.&lt;/p&gt;

&lt;p&gt;Each service remains responsible for its own data, and the Saga simply coordinates how those independent pieces fit together. This separation of responsibility is one of the reasons microservice architectures scale so well. Services remain loosely coupled while still participating in larger business workflows.&lt;/p&gt;





&lt;h3 id="thinking-in-business-processes"&gt;Thinking in Business Processes&lt;/h3&gt;

&lt;p&gt;One of the biggest mindset shifts when working with Sagas is realizing that you’re no longer designing database transactions. You’re designing business processes. Database transactions answer questions like:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“How do I keep these SQL statements consistent?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Sagas answer a much broader question:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“How does my business recover when part of this workflow succeeds and another part fails?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That’s why Sagas often involve business concepts rather than technical ones: refunds, reservation cancellations, order cancellations, inventory releases, and account reversals. These aren’t database operations; they’re business operations that happen to involve databases. Once you begin thinking at that level, the Saga Pattern becomes much easier to understand because it mirrors how real businesses operate. Companies don’t erase history when something goes wrong, they perform additional actions to correct it, and software simply follows the same principle.&lt;/p&gt;

&lt;h3 id="two-ways-to-coordinate-a-saga"&gt;Two Ways to Coordinate a Saga&lt;/h3&gt;

&lt;p&gt;Now that we understand what a Saga is, another important question emerges: &lt;strong&gt;Who is responsible for coordinating all these steps?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Imagine once again that a customer places an order. The Order Service creates the order, the Inventory Service reserves the stock, the Payment Service charges the customer, and the Shipping Service prepares the shipment. If everything succeeds, the Saga completes; if one step fails, compensating transactions begin. Someone, or something, must decide what happens next.&lt;/p&gt;

&lt;p&gt;There are two common ways to achieve this coordination:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Choreography&lt;/strong&gt;&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Orchestration&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both accomplish the same goal, but they do so in very different ways.&lt;/p&gt;





&lt;h3 id="choreography"&gt;Choreography&lt;/h3&gt;

&lt;p&gt;Think about a group of experienced dancers performing together. Nobody stands at the front giving instructions. Each dancer knows exactly when to move because they respond to the music and to one another. Saga choreography works in much the same way. Instead of a central coordinator directing every step, each service reacts to events published by other services. Consider our order workflow.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Customer Places Order

        │

        ▼

Order Service

Publishes

OrderCreated

        │

        ▼

Inventory Service

Publishes

InventoryReserved

        │

        ▼

Payment Service

Publishes

PaymentCompleted

        │

        ▼

Shipping Service

Publishes

ShipmentCreated
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Every service only knows two things:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;The events it listens for.&lt;/li&gt;
  &lt;li&gt;The events it publishes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Payment Service doesn’t know the Shipping Service exists, and the Shipping Service doesn’t know anything about the Inventory Service. Each service simply reacts whenever an event arrives. This loose coupling is one of choreography’s greatest strengths. Because services know very little about one another, adding or removing services often becomes much easier.&lt;/p&gt;

&lt;p&gt;Suppose your business introduces a Loyalty Service that awards reward points whenever an order is completed. Nothing else needs to change. The new service simply subscribes to the &lt;strong&gt;PaymentCompleted&lt;/strong&gt; event, and the rest of the system continues operating exactly as before. This flexibility makes choreography extremely attractive in event-driven architectures. However, it comes with a cost. As systems grow, understanding the overall business workflow becomes increasingly difficult. Instead of one visible process, the Saga becomes scattered across many services.&lt;/p&gt;

&lt;p&gt;To understand why a shipment wasn’t created, you may need to inspect logs from the Order Service, Inventory Service, Payment Service, Shipping Service, and Notification Service. The workflow still exists, it’s simply distributed across the entire system.&lt;/p&gt;





&lt;h3 id="orchestration"&gt;Orchestration&lt;/h3&gt;

&lt;p&gt;Now imagine the same dancers performing with a conductor standing at the front. Instead of reacting to one another, every performer follows instructions from a single leader. This is orchestration. Rather than allowing services to coordinate themselves, a dedicated component, often called the &lt;strong&gt;Saga Orchestrator&lt;/strong&gt;, controls the entire workflow. The orchestrator tells each service what to do next.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Saga Orchestrator

        │

        ▼

Create Order

        │

        ▼

Reserve Inventory

        │

        ▼

Charge Payment

        │

        ▼

Create Shipment
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;If every step succeeds, the orchestrator declares the Saga complete. If a failure occurs, it explicitly instructs previous services to execute their compensating transactions.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Shipment Failed

        │

        ▼

Refund Payment

        │

        ▼

Release Inventory

        │

        ▼

Cancel Order
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Unlike choreography, every decision is visible in one place. Need to understand the business process? Read the orchestrator. Need to change the workflow? Modify one component instead of updating several independent services. This centralized view makes orchestration particularly appealing for complex business processes involving many steps, conditional logic, or approval workflows. The trade-off, however, is tighter coupling. The orchestrator must understand every participating service, making it more aware of the overall system than any individual service would be in a choreographed Saga. Neither approach is universally better. The right choice depends on the complexity of the workflow and the level of control your application requires.&lt;/p&gt;





&lt;h3 id="choreography-vs-orchestration"&gt;Choreography vs Orchestration&lt;/h3&gt;

&lt;p&gt;A useful way to compare them is to think about where the business logic lives. With choreography, the workflow is distributed across many services, and every service contributes a small piece of the overall process by responding to events. With orchestration, the workflow lives inside a single coordinator that explicitly directs every participant.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Choreography&lt;/th&gt;
      &lt;th&gt;Orchestration&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;Event-driven&lt;/td&gt;
      &lt;td&gt;Command-driven&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Highly decoupled&lt;/td&gt;
      &lt;td&gt;Central coordinator&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Easy to extend&lt;/td&gt;
      &lt;td&gt;Easy to understand&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Workflow spread across services&lt;/td&gt;
      &lt;td&gt;Workflow visible in one place&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Can become difficult to trace&lt;/td&gt;
      &lt;td&gt;Coordinator becomes more complex&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Small event-driven systems often benefit from choreography because new consumers can easily subscribe to existing events. Larger enterprise workflows frequently choose orchestration because business rules remain easier to understand and maintain.&lt;/p&gt;





&lt;h3 id="real-world-examples"&gt;Real-World Examples&lt;/h3&gt;

&lt;p&gt;By now, you’ve probably encountered situations where the Saga Pattern would be useful without realizing it. An online retailer processing orders, a loan management platform approving applications, an airline booking system reserving flights, or a hotel reservation platform coordinating room availability all involve multiple independent services participating in a single business process. Consider a digital lending platform. When a borrower accepts a loan offer, several services may participate:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;The Loan Service creates the loan account.&lt;/li&gt;
  &lt;li&gt;The Disbursement Service sends funds.&lt;/li&gt;
  &lt;li&gt;The Accounting Service records journal entries.&lt;/li&gt;
  &lt;li&gt;The Notification Service sends an SMS.&lt;/li&gt;
  &lt;li&gt;The Credit Bureau Service updates the borrower’s status.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the disbursement fails because the customer’s bank account is invalid, the system shouldn’t leave behind a partially created loan. Instead, compensating actions mark the loan as cancelled, reverse accounting entries where necessary, and notify the customer that disbursement was unsuccessful. No database rollback spans all these services, and the Saga restores business consistency through coordinated business actions.&lt;/p&gt;





&lt;h3 id="common-mistakes"&gt;Common Mistakes&lt;/h3&gt;

&lt;p&gt;The Saga Pattern is powerful, but it’s not a silver bullet. One common mistake is trying to treat compensating transactions as database rollbacks. They aren’t. Compensation should reverse business effects, not erase history. Another mistake is making Saga steps too large. Every local transaction should remain focused and complete quickly. Long-running database transactions reduce scalability and increase the likelihood of contention. It’s also important to remember that messaging is rarely perfect. Duplicate events, delayed delivery, and retries are normal in distributed systems. Every participating service should therefore be designed with idempotency in mind. Finally, don’t introduce a Saga simply because your application uses microservices. If a workflow only involves one service and one database, a normal database transaction is usually the simpler and better solution. Sagas solve distributed coordination problems, not ordinary CRUD operations.&lt;/p&gt;





&lt;h3 id="saga-pattern-vs-traditional-transactions"&gt;Saga Pattern vs Traditional Transactions&lt;/h3&gt;

&lt;p&gt;At first glance, Sagas and database transactions appear to solve similar problems. In reality, they operate at completely different levels. A database transaction guarantees consistency within a single database; a Saga guarantees business consistency across multiple independent services. One relies on rollback, the other relies on compensation. One typically completes in milliseconds, while the other may run for several minutes, or even hours, depending on the business process. They’re not competing approaches. As you’ve seen throughout this series, they complement one another. In fact, a single Saga step usually contains its own local database transaction.&lt;/p&gt;





&lt;h3 id="bringing-it-all-together"&gt;Bringing It All Together&lt;/h3&gt;

&lt;p&gt;At this point in the &lt;strong&gt;Beyond CRUD&lt;/strong&gt; series, we’ve gradually built a toolkit for designing reliable backend systems. Each concept answers a different engineering question.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Concept&lt;/th&gt;
      &lt;th&gt;Question It Answers&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Idempotency&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;What if the same request arrives twice?&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Race Conditions&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;What if multiple requests modify the same data simultaneously?&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Database Transactions&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;How do I keep multiple database operations atomic?&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Isolation Levels&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;What should concurrent transactions be allowed to see?&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Distributed Locks&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;How do multiple application instances coordinate shared work?&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Outbox Pattern&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;How do I reliably publish events after committing data?&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Saga Pattern&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;How do multiple services complete one business process reliably?&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Notice how every concept builds upon the previous one. None replaces the others, and reliable distributed systems emerge when these patterns work together.&lt;/p&gt;





&lt;h3 id="final-thoughts"&gt;Final Thoughts&lt;/h3&gt;

&lt;p&gt;Building software inside a single database is relatively straightforward. Building software that spans dozens of independent services is something else entirely. The challenge isn’t simply writing correct code, it’s ensuring that business processes continue making sense even when networks fail, services restart, or one step succeeds while another doesn’t.&lt;/p&gt;

&lt;p&gt;The Saga Pattern embraces these realities rather than fighting them. Instead of relying on one enormous transaction that spans every service, it coordinates many smaller transactions while providing a structured way to recover when failures occur. That mindset has become one of the defining characteristics of modern cloud-native architecture. As your systems continue growing, you’ll discover that reliable software isn’t built by eliminating failures, it’s built by designing systems that expect failures and know exactly how to recover from them.&lt;/p&gt;





&lt;h3 id="whats-next"&gt;What’s Next?&lt;/h3&gt;

&lt;p&gt;So far, we’ve focused primarily on making writes reliable, but as applications grow, another challenge begins to emerge. Reading data efficiently often requires very different models from writing it. Should the same model be responsible for both, or should we optimize reads and writes independently?&lt;/p&gt;

&lt;p&gt;In the next article, we’ll explore &lt;strong&gt;CQRS Explained: Separating Reads and Writes for Scalable Systems&lt;/strong&gt;, a pattern that allows applications to scale, simplify complex queries, and build richer user experiences without overloading their write models.&lt;/p&gt;

</description>
      <category>distributedsystems</category>
      <category>concurrency</category>
    </item>
    <item>
      <title>The Outbox Pattern Explained: Publishing Events Without Losing Data</title>
      <dc:creator>Billy Okeyo</dc:creator>
      <pubDate>Mon, 20 Jul 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/billy_de_cartel/the-outbox-pattern-explained-publishing-events-without-losing-data-21o9</link>
      <guid>https://dev.to/billy_de_cartel/the-outbox-pattern-explained-publishing-events-without-losing-data-21o9</guid>
      <description>&lt;blockquote&gt;
  &lt;p&gt;&lt;em&gt;“A database transaction can guarantee your data is correct. It cannot guarantee the rest of your system knows about it.”&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Imagine you’re building an e-commerce platform. A customer places an order, and your application begins processing the request. Inside a database transaction, it creates the order, deducts the purchased items from inventory, records the payment, and commits the transaction successfully. From the perspective of the database, everything has gone exactly as planned. Every change has been saved, every business rule has been respected, and the transaction completes without error.&lt;/p&gt;

&lt;p&gt;If this were a monolithic application, the story might end there, but modern software rarely consists of a single application. The moment an order is created, several other systems need to react. A shipping service must prepare the package, an email service needs to send an order confirmation, an analytics platform wants to record another sale, a loyalty service may award reward points, and an accounting system might need to generate journal entries. Rather than constantly querying the orders table looking for changes, these services usually rely on events published through a message broker such as Kafka, RabbitMQ, Azure Service Bus, or Amazon SQS.&lt;/p&gt;

&lt;p&gt;A straightforward implementation seems almost obvious: once the transaction commits successfully, publish an &lt;strong&gt;OrderCreated&lt;/strong&gt; event.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;BEGIN TRANSACTION

↓

Create Order

↓

Reduce Inventory

↓

Record Payment

↓

COMMIT

↓

Publish OrderCreated Event
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;At first glance, there’s nothing wrong with this approach, until something fails. Suppose the database transaction commits successfully, permanently saving the customer’s order. A fraction of a second later, however, Kafka becomes temporarily unavailable. Perhaps RabbitMQ disconnects, or maybe the application crashes before it can publish the event.&lt;/p&gt;

&lt;p&gt;The order now exists in the database, the customer’s payment has been processed, and inventory has already been reduced, yet none of the downstream services know the purchase ever happened. The warehouse never receives instructions to prepare the shipment, the customer never receives a confirmation email, the analytics dashboard quietly reports incorrect sales figures, and the accounting system never records the transaction.&lt;/p&gt;

&lt;p&gt;Nothing inside the database is inconsistent. The inconsistency exists &lt;strong&gt;between systems&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This subtle failure is one of the most common reliability problems in distributed architectures. It’s known as the &lt;strong&gt;Dual-Write Problem&lt;/strong&gt;, and it’s surprisingly easy to introduce because writing to a database and publishing an event are two completely independent operations. One can succeed while the other fails, leaving different parts of the system with different versions of reality.&lt;/p&gt;

&lt;p&gt;In our previous articles, we’ve explored how transactions protect database operations, how isolation levels govern concurrent access to data, and how distributed locks coordinate work across multiple application servers. The Outbox Pattern builds on those concepts by solving a different challenge: ensuring that once your database commits a change, the rest of your system will eventually learn about it as well.&lt;/p&gt;

&lt;p&gt;Before we look at the solution, let’s first understand why this seemingly simple problem is much harder than it appears.&lt;/p&gt;





&lt;h4 id="the-dual-write-problem"&gt;The Dual-Write Problem&lt;/h4&gt;

&lt;p&gt;The Dual-Write Problem occurs whenever an application attempts to update two independent systems as part of a single business operation. One system is usually your database; the other is typically a message broker, search index, cache, analytics platform, or another external service. Although these updates feel like one logical operation, they are technically two completely separate actions.&lt;/p&gt;

&lt;p&gt;Consider an online banking application. When a customer transfers money, the application updates account balances inside the database. Afterward, it publishes a &lt;strong&gt;MoneyTransferred&lt;/strong&gt; event so other services can respond. A fraud detection system may inspect the transaction, a notification service may send an SMS, and an accounting platform may update its ledgers.&lt;/p&gt;

&lt;p&gt;Conceptually, all of this represents one business event, but technically, it looks more like this:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Update Database

↓

Publish Event
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The problem is that neither system knows anything about the other. Your database has no idea whether Kafka accepted the message, and Kafka has no knowledge of whether your SQL transaction committed successfully. They’re completely independent, and that independence creates four possible outcomes.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Database&lt;/th&gt;
      &lt;th&gt;Event&lt;/th&gt;
      &lt;th&gt;Result&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;✅ Success&lt;/td&gt;
      &lt;td&gt;✅ Success&lt;/td&gt;
      &lt;td&gt;Everything works correctly.&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;❌ Failure&lt;/td&gt;
      &lt;td&gt;❌ Failure&lt;/td&gt;
      &lt;td&gt;Nothing happens, which is acceptable.&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;❌ Failure&lt;/td&gt;
      &lt;td&gt;✅ Success&lt;/td&gt;
      &lt;td&gt;Downstream systems react to an event for data that doesn’t exist.&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;✅ Success&lt;/td&gt;
      &lt;td&gt;❌ Failure&lt;/td&gt;
      &lt;td&gt;The database is correct, but no other service knows the change occurred.&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The first two outcomes are easy to reason about, but it’s the final two that create serious problems. If an event is published for data that never commits, downstream services begin processing information that doesn’t actually exist. Equally dangerous is the opposite scenario, where the database commits successfully but the event is never published. From that moment onward, every service in the architecture has a different understanding of reality. This isn’t merely an inconvenience; it’s a consistency problem that becomes increasingly difficult to detect as systems grow.&lt;/p&gt;





&lt;h3 id="why-transactions-cant-solve-this"&gt;Why Transactions Can’t Solve This&lt;/h3&gt;

&lt;p&gt;A natural question follows: why not simply include the message broker inside the database transaction? Unfortunately, that’s not how database transactions work. Transactions provide atomicity because every operation is coordinated by the same database engine. The database controls when the transaction begins, when it commits, and when it rolls back. Every SQL statement participates in that process because they’re all executed by the same system.&lt;/p&gt;

&lt;p&gt;A message broker lives outside that boundary. Kafka doesn’t participate in your PostgreSQL transaction, RabbitMQ doesn’t know your SQL Server transaction exists, and PostgreSQL has no mechanism for asking Kafka whether a message was published successfully before committing the transaction. In other words, there is no shared transaction manager coordinating both systems.&lt;/p&gt;

&lt;p&gt;Years ago, technologies such as &lt;strong&gt;Two-Phase Commit (2PC)&lt;/strong&gt; attempted to solve this problem by coordinating transactions across multiple systems. Although theoretically appealing, they introduced significant complexity, increased latency, reduced availability, and often became bottlenecks in distributed environments.&lt;/p&gt;

&lt;p&gt;As microservices became more popular, the industry gradually moved toward simpler, more resilient approaches. Rather than trying to make two systems commit simultaneously, engineers began asking a different question:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;What if we only committed to one system first and made the second system eventually consistent?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That shift in thinking led to one of the most influential patterns in modern backend architecture: the &lt;strong&gt;Outbox Pattern&lt;/strong&gt;.&lt;/p&gt;

&lt;h3 id="what-is-the-outbox-pattern"&gt;What Is the Outbox Pattern?&lt;/h3&gt;

&lt;p&gt;The Outbox Pattern solves the Dual-Write Problem by changing the order in which work is performed. Instead of attempting to update the database and publish an event as part of the same operation, the application first commits everything it needs to the database, including the event itself. That last part is the key. Rather than sending the event directly to Kafka or RabbitMQ, the application writes the event into a dedicated database table commonly known as the &lt;strong&gt;outbox&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Because the business data and the outbox record are written inside the &lt;strong&gt;same database transaction&lt;/strong&gt;, they succeed or fail together. If the transaction commits, both the business data and the event are safely stored; if it rolls back, neither exists. Only after the transaction has completed does another process read events from the outbox table and publish them to the message broker.&lt;/p&gt;

&lt;p&gt;Conceptually, the workflow changes from this:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Update Database

↓

Publish Event
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;to this:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;BEGIN TRANSACTION

↓

Update Business Data

↓

Insert Event Into Outbox

↓

COMMIT

↓

Background Publisher Reads Outbox

↓

Publish Event

↓

Mark Event As Published
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;At first glance, this may seem like a small adjustment, but in reality, it completely changes the reliability characteristics of your application. The application is no longer trying to coordinate two independent systems within the same request. Instead, it commits to a single source of truth, the database, and lets another process handle communication with external systems afterward. If the message broker is temporarily unavailable, nothing is lost. The event is already safely stored inside the database and simply waits until the publisher can deliver it.&lt;/p&gt;





&lt;h3 id="understanding-the-outbox-table"&gt;Understanding the Outbox Table&lt;/h3&gt;

&lt;p&gt;The outbox itself is usually nothing more than a normal database table. A simplified version might look like this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;id&lt;/th&gt;
      &lt;th&gt;event_type&lt;/th&gt;
      &lt;th&gt;payload&lt;/th&gt;
      &lt;th&gt;created_at&lt;/th&gt;
      &lt;th&gt;published_at&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;1&lt;/td&gt;
      &lt;td&gt;OrderCreated&lt;/td&gt;
      &lt;td&gt;{…}&lt;/td&gt;
      &lt;td&gt;2026-07-01 10:15&lt;/td&gt;
      &lt;td&gt;NULL&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;2&lt;/td&gt;
      &lt;td&gt;PaymentReceived&lt;/td&gt;
      &lt;td&gt;{…}&lt;/td&gt;
      &lt;td&gt;2026-07-01 10:16&lt;/td&gt;
      &lt;td&gt;NULL&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;3&lt;/td&gt;
      &lt;td&gt;UserRegistered&lt;/td&gt;
      &lt;td&gt;{…}&lt;/td&gt;
      &lt;td&gt;2026-07-01 10:18&lt;/td&gt;
      &lt;td&gt;2026-07-01 10:18&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each row represents an event that should eventually be delivered. Notice the &lt;code&gt;published_at&lt;/code&gt; column: rows where this value is &lt;code&gt;NULL&lt;/code&gt; haven’t yet been published, while rows with a timestamp have already been successfully delivered.&lt;/p&gt;

&lt;p&gt;The outbox isn’t intended to become a permanent event store. Instead, it acts as a reliable staging area between your transactional database and your messaging infrastructure. Once an event has been published successfully, and your retention policy allows, it can be archived or deleted.&lt;/p&gt;





&lt;h3 id="a-complete-walkthrough"&gt;A Complete Walkthrough&lt;/h3&gt;

&lt;p&gt;Let’s revisit our e-commerce example. A customer purchases a laptop. Inside a single transaction, the application performs three operations: first, it creates the order; second, it deducts one item from inventory; and finally, instead of publishing an event immediately, it inserts a new row into the outbox table describing what happened.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;BEGIN TRANSACTION

↓

INSERT Order

↓

UPDATE Inventory

↓

INSERT Outbox Event

↓

COMMIT
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;At this point, the customer’s purchase has been committed successfully, and just as importantly, the event describing that purchase has also been committed. Suppose Kafka goes offline immediately afterward: the request still succeeds, nothing has been lost, and the application doesn’t need to roll back the order because the event hasn’t disappeared. It is sitting safely inside the outbox table waiting to be delivered.&lt;/p&gt;

&lt;p&gt;A separate publisher service periodically checks the outbox. When Kafka becomes available again, it publishes the event and updates the record to indicate that delivery succeeded.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Outbox Publisher

↓

Read Unpublished Events

↓

Publish To Kafka

↓

Success?

      │

   Yes │ No

      │

Mark Published

      │

Retry Later
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Notice something important: the user’s request is no longer responsible for talking to Kafka. Its only responsibility is ensuring that the event is safely recorded, and publishing becomes a separate concern. That separation dramatically improves reliability because temporary failures in external systems no longer affect the original business transaction.&lt;/p&gt;





&lt;h3 id="why-this-works"&gt;Why This Works&lt;/h3&gt;

&lt;p&gt;The elegance of the Outbox Pattern comes from the fact that it reduces a distributed systems problem to a database problem. Earlier in this series, we spent considerable time discussing transactions and learned that they guarantee a group of related database operations either all succeed or all fail together. The Outbox Pattern deliberately takes advantage of that guarantee: instead of asking a database and a message broker to commit simultaneously, a task they were never designed to perform, it asks only the database to commit. Since both the business data and the outbox record live inside the same database, the transaction naturally guarantees their consistency.&lt;/p&gt;

&lt;p&gt;Everything after that becomes a delivery problem rather than a consistency problem. Even if the publisher crashes, Kafka becomes unavailable, or the network experiences intermittent failures, the event remains safely stored. The publisher will eventually retry, and the event will eventually be delivered. The system moves from requiring &lt;strong&gt;immediate consistency&lt;/strong&gt; between the database and the message broker to achieving &lt;strong&gt;eventual consistency&lt;/strong&gt; in a reliable and predictable way. That single shift in mindset is what has made the Outbox Pattern one of the most widely adopted reliability patterns in modern distributed systems.&lt;/p&gt;





&lt;h3 id="why-not-publish-immediately-after-the-commit"&gt;Why Not Publish Immediately After the Commit?&lt;/h3&gt;

&lt;p&gt;Some developers look at the Outbox Pattern and ask a reasonable question: “If the transaction has already committed successfully, why not simply publish the event right afterward and retry if it fails?” The problem is that retries only work if the application survives long enough to perform them.&lt;/p&gt;

&lt;p&gt;Imagine the following sequence of events:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;COMMIT Transaction

↓

Application Crashes

↓

Restart
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The order exists, the event was never published, and the application has no memory that it still owes Kafka an event because that information was never persisted anywhere. With an outbox table, the event survives the crash because it was committed alongside the business data. When the publisher starts again, it simply resumes reading unpublished events from the database. The application doesn’t have to remember what happened because the database already does.&lt;/p&gt;

&lt;p&gt;This is one of the reasons the Outbox Pattern is so resilient. It relies on durable storage rather than application memory, making it naturally tolerant of crashes, restarts, and temporary infrastructure failures.&lt;/p&gt;

&lt;h3 id="how-events-leave-the-outbox"&gt;How Events Leave the Outbox&lt;/h3&gt;

&lt;p&gt;By now, we’ve established that the application’s responsibility ends once the business data and the corresponding event have been committed to the database. The next challenge is equally important: &lt;strong&gt;How do those events actually reach Kafka, RabbitMQ, or another messaging system?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Broadly speaking, there are two common approaches. The first is a &lt;strong&gt;Polling Publisher&lt;/strong&gt;, where a background worker periodically checks the outbox table for unpublished events. Every few seconds, or even every few milliseconds, it queries the database, publishes any pending events, and marks them as successfully delivered.&lt;/p&gt;

&lt;p&gt;Conceptually, the process looks like this:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Background Publisher

↓

Query Outbox

↓

Find Unpublished Events

↓

Publish Event

↓

Success?

     │

 Yes │ No

     │

Mark Published

     │

Retry Later
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This approach is simple to understand, easy to implement, and works well for many applications. Since the publisher operates independently of the original request, temporary failures in Kafka or RabbitMQ don’t affect users. If publishing fails, the worker simply retries later.&lt;/p&gt;

&lt;p&gt;The second approach is &lt;strong&gt;Change Data Capture (CDC)&lt;/strong&gt;. Instead of periodically querying the outbox table, a CDC tool monitors the database’s transaction log and automatically detects newly inserted outbox records. As soon as a new event appears, the tool publishes it to the message broker.&lt;/p&gt;

&lt;p&gt;One of the most popular CDC solutions is &lt;strong&gt;Debezium&lt;/strong&gt;, which integrates with databases such as PostgreSQL, MySQL, and SQL Server. Rather than polling the database repeatedly, Debezium continuously streams database changes, reducing unnecessary queries while providing near real-time event publication. Both approaches solve the same problem. Polling is generally simpler and perfectly adequate for many systems, while CDC becomes attractive when applications process very large numbers of events or require lower publishing latency.&lt;/p&gt;





&lt;h3 id="real-world-examples"&gt;Real-World Examples&lt;/h3&gt;

&lt;p&gt;The Outbox Pattern appears in many more places than developers often realize. Once you begin recognizing the Dual-Write Problem, you start seeing it almost everywhere.&lt;/p&gt;

&lt;h4 id="e-commerce"&gt;E-Commerce&lt;/h4&gt;

&lt;p&gt;A customer places an order. The transaction creates the order, updates inventory, and inserts an &lt;strong&gt;OrderCreated&lt;/strong&gt; event into the outbox. Later, the publisher delivers that event to Kafka, and other services respond independently:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Shipping prepares the package.&lt;/li&gt;
  &lt;li&gt;Email sends the confirmation.&lt;/li&gt;
  &lt;li&gt;Analytics records the sale.&lt;/li&gt;
  &lt;li&gt;Loyalty awards points.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every service receives exactly the same event without the original request needing to coordinate them.&lt;/p&gt;





&lt;h4 id="loan-management-systems"&gt;Loan Management Systems&lt;/h4&gt;

&lt;p&gt;Suppose a borrower makes a repayment. Inside a transaction, the application updates the loan balance, records the payment, recalculates outstanding interest, and inserts a &lt;strong&gt;LoanRepaymentReceived&lt;/strong&gt; event into the outbox. Once published, other systems can react independently. The accounting service posts journal entries, the notification service sends an SMS receipt, reporting dashboards update repayment statistics, and credit scoring services refresh customer profiles. The repayment itself remains fast because none of these downstream systems participate in the original transaction.&lt;/p&gt;





&lt;h4 id="user-registration"&gt;User Registration&lt;/h4&gt;

&lt;p&gt;A customer creates a new account. The application stores the user’s information and writes a &lt;strong&gt;UserRegistered&lt;/strong&gt; event into the outbox. Later, other services consume the event. An email service sends a welcome message, a CRM platform creates a customer profile, a marketing system subscribes the user to onboarding campaigns, and a recommendation engine begins generating personalized suggestions. The registration endpoint doesn’t need to know anything about those systems; its only responsibility is recording that the registration occurred.&lt;/p&gt;





&lt;h4 id="payment-processing"&gt;Payment Processing&lt;/h4&gt;

&lt;p&gt;Imagine a payment gateway confirms a successful transaction. The payment service updates account balances, records the payment, and inserts a &lt;strong&gt;PaymentCompleted&lt;/strong&gt; event. Even if Kafka becomes unavailable immediately afterward, the payment itself isn’t lost. Once messaging resumes, the event is delivered and downstream systems continue exactly where they left off.&lt;/p&gt;





&lt;h3 id="benefits-of-the-outbox-pattern"&gt;Benefits of the Outbox Pattern&lt;/h3&gt;

&lt;p&gt;One of the reasons the Outbox Pattern has become so widely adopted is that it solves several problems simultaneously. First, it provides &lt;strong&gt;reliability&lt;/strong&gt;. Once an event has been written into the outbox, it becomes durable. Temporary network failures, message broker outages, or application crashes no longer result in permanently lost events. Second, it simplifies application code. Rather than forcing every request to coordinate database updates and message publication, the request focuses exclusively on committing business data, and event publication becomes the responsibility of a dedicated background process. Third, it improves scalability. Because event publication happens asynchronously, user requests complete more quickly, and slow downstream systems no longer delay the original transaction. Finally, it naturally embraces &lt;strong&gt;eventual consistency&lt;/strong&gt;. Instead of requiring every service to update simultaneously, the system guarantees that every interested service will eventually receive the event once communication becomes available.&lt;/p&gt;





&lt;h3 id="common-mistakes"&gt;Common Mistakes&lt;/h3&gt;

&lt;p&gt;Like every architectural pattern, the Outbox Pattern can be implemented incorrectly.&lt;/p&gt;

&lt;p&gt;One common mistake is assuming that publishing an event means the work is finished. Publishing only guarantees that the message reached the broker. Consumers may still fail, messages may still be retried, and downstream services must therefore remain resilient and, where appropriate, idempotent.&lt;/p&gt;

&lt;p&gt;Another common mistake is forgetting to clean up the outbox table. If events remain forever, the table will continue growing until queries become unnecessarily expensive. Most production systems archive or remove published events after an appropriate retention period.&lt;/p&gt;

&lt;p&gt;Developers also sometimes attempt to perform expensive business logic inside the publisher. The publisher should remain intentionally simple. Its responsibility is publishing events, not recalculating business rules or modifying application state.&lt;/p&gt;

&lt;p&gt;Finally, don’t assume every database change requires an event. Publishing unnecessary events creates noise, increases infrastructure costs, and makes systems more difficult to understand. Good events represent meaningful business occurrences, not individual SQL statements.&lt;/p&gt;





&lt;h3 id="outbox-pattern-vs-distributed-transactions"&gt;Outbox Pattern vs Distributed Transactions&lt;/h3&gt;

&lt;p&gt;At first glance, the Outbox Pattern and distributed transactions appear to solve the same problem. Both attempt to coordinate work across multiple systems, but the difference lies in how they approach consistency. Distributed transactions attempt to make every participating system commit or roll back together. The Outbox Pattern accepts that this is often impractical in distributed architectures. Instead, it commits business data first and guarantees that events will eventually be delivered. This approach sacrifices immediate consistency in exchange for simplicity, resilience, and availability. For modern cloud-native applications, that trade-off is often the better engineering decision.&lt;/p&gt;





&lt;h3 id="outbox-pattern-vs-event-sourcing"&gt;Outbox Pattern vs Event Sourcing&lt;/h3&gt;

&lt;p&gt;The Outbox Pattern is also frequently confused with Event Sourcing. Although both involve events, they solve entirely different problems. With the Outbox Pattern, the database remains the primary source of truth, and events simply communicate that something has happened. With Event Sourcing, events &lt;strong&gt;are&lt;/strong&gt; the source of truth. Instead of storing the current state of an order or account, the system stores every event that led to its current state, and the application reconstructs state by replaying those events.&lt;/p&gt;

&lt;p&gt;An outbox event might say:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Order Created.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;An event-sourced system would permanently record every event throughout the order’s lifetime:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Order Created&lt;/li&gt;
  &lt;li&gt;Payment Authorized&lt;/li&gt;
  &lt;li&gt;Inventory Reserved&lt;/li&gt;
  &lt;li&gt;Shipment Prepared&lt;/li&gt;
  &lt;li&gt;Order Delivered&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both patterns involve events, but only Event Sourcing uses them to reconstruct application state.&lt;/p&gt;





&lt;h3 id="best-practices"&gt;Best Practices&lt;/h3&gt;

&lt;p&gt;If you’re considering adopting the Outbox Pattern, several practices consistently lead to reliable implementations.&lt;/p&gt;

&lt;p&gt;Keep the outbox in the same database as your business data so that both participate in the same transaction.&lt;/p&gt;

&lt;p&gt;Design your publisher to be idempotent wherever possible. Network failures and retries are inevitable, and duplicate publication attempts should never produce incorrect results.&lt;/p&gt;

&lt;p&gt;Monitor the outbox table. A growing backlog of unpublished events is often the earliest warning sign that something is wrong with your messaging infrastructure.&lt;/p&gt;

&lt;p&gt;Treat event schemas as part of your public contract. Once other services depend on them, changing them carelessly becomes just as risky as changing a public API.&lt;/p&gt;

&lt;p&gt;Finally, remember that publishing an event doesn’t guarantee it has been processed. Downstream services should always be designed to handle retries, duplicates, and temporary failures gracefully.&lt;/p&gt;





&lt;h3 id="bringing-it-all-together"&gt;Bringing It All Together&lt;/h3&gt;

&lt;p&gt;At this point in the series, we’ve explored several techniques for building reliable software systems. Each one addresses a different question.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Concept&lt;/th&gt;
      &lt;th&gt;Question It Answers&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Idempotency&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;What if the same request is sent twice?&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Race Conditions&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;What if multiple requests modify the same data simultaneously?&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Database Transactions&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;What if part of my database operation fails?&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Isolation Levels&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;What should concurrent transactions be allowed to see?&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Distributed Locks&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;How do multiple servers coordinate work?&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Outbox Pattern&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;How do I reliably notify other systems after committing data?&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Notice the progression: the concepts don’t replace one another, they build upon one another. Modern backend systems are reliable precisely because they combine multiple patterns, each solving a different class of failure.&lt;/p&gt;





&lt;h3 id="final-thoughts"&gt;Final Thoughts&lt;/h3&gt;

&lt;p&gt;One of the defining characteristics of distributed systems is that communication eventually fails. Networks become unreliable, applications restart, and message brokers experience outages. Trying to eliminate these failures entirely is unrealistic.&lt;/p&gt;

&lt;p&gt;The Outbox Pattern embraces this reality by changing the problem. Instead of attempting to guarantee that two independent systems succeed simultaneously, it guarantees that business data is safely committed first and that communication will eventually catch up. This seemingly small architectural decision dramatically improves reliability because it removes timing from the equation. Your application no longer depends on Kafka, RabbitMQ, or another messaging system being available at the exact moment a customer submits a request. Instead, it relies on something your database already does exceptionally well: storing data reliably.&lt;/p&gt;

&lt;p&gt;The next time you find yourself writing code that updates a database and immediately publishes an event, pause for a moment and ask yourself:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;“What happens if my database succeeds but my message broker doesn’t?”&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If the answer is “my systems become inconsistent,” you’ve probably found a place where the Outbox Pattern belongs.&lt;/p&gt;





&lt;h3 id="whats-next"&gt;What’s Next?&lt;/h3&gt;

&lt;p&gt;So far in this series, we’ve focused on making individual operations reliable. But what happens when a single business process spans &lt;strong&gt;multiple microservices&lt;/strong&gt;, each with its own database and transaction?&lt;/p&gt;

&lt;p&gt;Imagine placing an order that requires the payment service, inventory service, shipping service, and notification service to all complete successfully. What happens if one of those services fails halfway through?&lt;/p&gt;

&lt;p&gt;In the next article, we’ll explore the &lt;strong&gt;Saga Pattern&lt;/strong&gt;, one of the most widely used approaches for coordinating long-running business transactions across distributed systems.&lt;/p&gt;

</description>
      <category>distributedsystems</category>
      <category>concurrency</category>
    </item>
    <item>
      <title>Distributed Locks Explained: Technologies That Implement Distributed Locks</title>
      <dc:creator>Billy Okeyo</dc:creator>
      <pubDate>Fri, 17 Jul 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/billy_de_cartel/distributed-locks-explained-technologies-that-implement-distributed-locks-30l6</link>
      <guid>https://dev.to/billy_de_cartel/distributed-locks-explained-technologies-that-implement-distributed-locks-30l6</guid>
      <description>&lt;p&gt;Now that we understand how distributed locks work conceptually, the next question becomes:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;Where do these locks actually live?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Unlike database transactions, distributed locks cannot rely on the memory of a single application server. Remember, our application might be running on five, ten, or even hundreds of machines, and every server must consult the same source of truth before deciding whether it may perform a particular operation.&lt;/p&gt;

&lt;p&gt;Over the years, several technologies have emerged to solve this coordination problem. Although they all provide distributed locking capabilities, they were designed with slightly different goals in mind. Let’s look at the most common ones.&lt;/p&gt;





&lt;h2 id="redis"&gt;Redis&lt;/h2&gt;

&lt;p&gt;For most web applications, &lt;strong&gt;Redis&lt;/strong&gt; is by far the most popular choice. Originally designed as an in-memory data store, Redis is incredibly fast, making it an excellent candidate for lightweight coordination tasks.&lt;/p&gt;

&lt;p&gt;Acquiring a lock in Redis is surprisingly straightforward. A server attempts to create a key using an atomic command that says, in effect:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“Create this key only if it doesn’t already exist.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If Redis successfully creates the key, the server owns the lock; if the key already exists, another server is already performing the work. Because Redis executes this operation atomically, two servers can never successfully create the same lock at the same time.&lt;/p&gt;

&lt;p&gt;A simplified example looks like this:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;SET invoice-generation

Server-A

NX

EX 30
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The command says:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Create the key only if it doesn’t already exist (&lt;code&gt;NX&lt;/code&gt;).&lt;/li&gt;
  &lt;li&gt;Automatically expire it after thirty seconds (&lt;code&gt;EX 30&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In one atomic operation, Redis both acquires the lock and ensures it won’t remain forever if the server crashes. For many applications, this is all that’s needed.&lt;/p&gt;





&lt;h2 id="why-redis-is-so-popular"&gt;Why Redis Is So Popular&lt;/h2&gt;

&lt;p&gt;Redis has become the default choice for distributed locks because it satisfies three important requirements. First, it’s extremely fast: since Redis stores data in memory rather than on disk, lock acquisition usually takes only a few milliseconds. Second, many applications already use Redis for caching, sessions, queues, or rate limiting, so adding distributed locking often requires little additional infrastructure. Finally, Redis has mature client libraries for virtually every programming language, with Laravel, Django, Spring Boot, .NET, Node.js, and Go all providing excellent Redis support.&lt;/p&gt;

&lt;p&gt;For the vast majority of business applications, Redis offers an excellent balance between simplicity, performance, and reliability.&lt;/p&gt;





&lt;h2 id="the-challenge-with-a-single-redis-instance"&gt;The Challenge with a Single Redis Instance&lt;/h2&gt;

&lt;p&gt;Suppose your entire application depends on one Redis server. Everything works perfectly, then Redis crashes. Suddenly, no application server can acquire new locks, and even worse, if Redis loses its in-memory state during a restart, locks may disappear unexpectedly.&lt;/p&gt;

&lt;p&gt;This introduces a new challenge: the coordinator itself has become a single point of failure. For many applications, this risk is acceptable. For others, particularly financial systems or globally distributed services, it isn’t. This challenge led to one of the most discussed topics in distributed systems: the &lt;strong&gt;Redlock algorithm&lt;/strong&gt;.&lt;/p&gt;





&lt;h2 id="the-redlock-algorithm"&gt;The Redlock Algorithm&lt;/h2&gt;

&lt;p&gt;Redlock was proposed by Redis creator &lt;strong&gt;Salvatore Sanfilippo&lt;/strong&gt; as a way to make Redis-based distributed locks more resilient. Instead of relying on one Redis server, Redlock uses multiple independent Redis instances.&lt;/p&gt;

&lt;p&gt;Imagine five Redis servers.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Redis 1

Redis 2

Redis 3

Redis 4

Redis 5
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;When acquiring a lock, the application attempts to obtain it from all five servers, and the lock is considered successful only if a majority agree.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Acquire Lock

↓

Redis 1 ✅

Redis 2 ✅

Redis 3 ✅

Redis 4 ❌

Redis 5 ❌
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Three out of five succeeded, so the application proceeds. If only two servers grant the lock, the operation fails because a majority wasn’t reached. This approach significantly reduces the impact of individual Redis failures.&lt;/p&gt;

&lt;p&gt;However, Redlock is also one of the most debated algorithms in distributed systems. Some engineers argue that it’s sufficient for many practical systems, while others, including Martin Kleppmann, have published detailed critiques explaining situations where Redlock may not provide the guarantees developers expect. The important lesson isn’t that Redlock is good or bad; it’s that distributed systems involve trade-offs, and understanding those trade-offs matters more than memorizing algorithms.&lt;/p&gt;





&lt;h2 id="zookeeper"&gt;ZooKeeper&lt;/h2&gt;

&lt;p&gt;Long before Redis became popular for distributed locking, many large distributed systems relied on &lt;strong&gt;Apache ZooKeeper&lt;/strong&gt;. ZooKeeper was designed specifically for coordination. Rather than functioning as a cache, it provides services such as:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Distributed locks&lt;/li&gt;
  &lt;li&gt;Leader election&lt;/li&gt;
  &lt;li&gt;Configuration management&lt;/li&gt;
  &lt;li&gt;Service discovery&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Think of ZooKeeper as a highly reliable coordinator for distributed applications. Its primary goal isn’t speed, it’s correctness. Large systems such as Apache Kafka, Hadoop, and HBase have historically relied on ZooKeeper to coordinate clusters of machines. If your application requires complex distributed coordination rather than simple locking, ZooKeeper remains an excellent choice.&lt;/p&gt;





&lt;h2 id="etcd"&gt;etcd&lt;/h2&gt;

&lt;p&gt;If you’ve worked with Kubernetes, you’ve already encountered &lt;strong&gt;etcd&lt;/strong&gt;, even if you didn’t realize it. Every Kubernetes cluster stores its configuration inside etcd. Like ZooKeeper, etcd is a distributed key-value store designed for coordination rather than caching. It provides:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Distributed locks&lt;/li&gt;
  &lt;li&gt;Leader election&lt;/li&gt;
  &lt;li&gt;Configuration storage&lt;/li&gt;
  &lt;li&gt;Consensus&lt;/li&gt;
  &lt;li&gt;Service coordination&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Unlike Redis, etcd prioritizes consistency over raw performance. Its API is also designed around long-lived leases, making lock management particularly elegant. Modern cloud-native applications frequently choose etcd when they already operate within Kubernetes ecosystems.&lt;/p&gt;





&lt;h2 id="consul"&gt;Consul&lt;/h2&gt;

&lt;p&gt;HashiCorp &lt;strong&gt;Consul&lt;/strong&gt; occupies a similar space. Although many developers know Consul for service discovery, it also provides distributed locking capabilities through sessions. Organizations using Consul often rely on it for:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Service registration&lt;/li&gt;
  &lt;li&gt;Health checks&lt;/li&gt;
  &lt;li&gt;Distributed configuration&lt;/li&gt;
  &lt;li&gt;Leader election&lt;/li&gt;
  &lt;li&gt;Distributed locks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Like ZooKeeper and etcd, Consul focuses on reliable coordination across distributed infrastructure.&lt;/p&gt;





&lt;h2 id="which-technology-should-you-choose"&gt;Which Technology Should You Choose?&lt;/h2&gt;

&lt;p&gt;There isn’t a universal answer. Instead, the right choice depends on your application’s requirements. If you’re building a typical web application that already uses Redis, implementing distributed locks with Redis is usually the simplest and most practical solution. If you’re coordinating hundreds of services across a Kubernetes cluster, etcd may integrate more naturally with your infrastructure. If your organization already uses Consul or ZooKeeper for service coordination, leveraging those existing systems often makes more sense than introducing Redis solely for locking.&lt;/p&gt;

&lt;p&gt;Choosing a technology isn’t just about features. It’s also about operational complexity, existing infrastructure, and the guarantees your business requires. The important thing to remember is this: distributed locks are a concept, while Redis, ZooKeeper, etcd, and Consul are simply different tools for implementing that concept. Understanding the underlying idea is far more valuable than becoming attached to a specific technology.&lt;/p&gt;





&lt;h2 id="do-you-always-need-a-distributed-lock"&gt;Do You Always Need a Distributed Lock?&lt;/h2&gt;

&lt;p&gt;Reading this article, it might be tempting to conclude that distributed locks are the solution to every concurrency problem. They’re not. In fact, many applications never need them. If your application runs on a single server, ordinary in-memory locks are often sufficient, and if your database transaction already guarantees correctness, introducing a distributed lock may simply add unnecessary complexity.&lt;/p&gt;

&lt;p&gt;Distributed locks are powerful, but they should be introduced only when multiple independent application instances genuinely need to coordinate shared work. Like every distributed systems technique, they solve a very specific class of problems, and the best engineering decision is often knowing when &lt;strong&gt;not&lt;/strong&gt; to use them.&lt;/p&gt;

&lt;h2 id="common-mistakes-when-using-distributed-locks"&gt;Common Mistakes When Using Distributed Locks&lt;/h2&gt;

&lt;p&gt;Like many distributed systems concepts, distributed locks appear deceptively simple: acquire a lock, perform some work, release the lock. In practice, however, there are several subtle mistakes that can introduce bugs that are even harder to diagnose than the problem the lock was intended to solve. Understanding these pitfalls is just as important as understanding distributed locks themselves.&lt;/p&gt;





&lt;h3 id="assuming-a-lock-lasts-forever"&gt;Assuming a Lock Lasts Forever&lt;/h3&gt;

&lt;p&gt;One of the most common mistakes is forgetting that distributed locks usually have an expiration time. Suppose a server acquires a lock with a TTL of thirty seconds, and the developer assumes the operation will always finish within that window. Months later, a new feature makes the operation take forty-five seconds. The lock expires while the first server is still working, another server acquires the same lock and begins executing the exact same task, and suddenly, duplicate work appears again.&lt;/p&gt;

&lt;p&gt;Choosing an appropriate TTL, and renewing it for long-running tasks when necessary, is essential.&lt;/p&gt;





&lt;h3 id="forgetting-to-release-the-lock"&gt;Forgetting to Release the Lock&lt;/h3&gt;

&lt;p&gt;Although expiration protects against permanent deadlocks, applications should still release locks as soon as the protected work finishes. Holding a lock longer than necessary reduces concurrency and delays other servers waiting to perform legitimate work.&lt;/p&gt;

&lt;p&gt;A good rule is simple:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Hold the lock only for the work that genuinely requires exclusive access.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Everything else should happen outside the lock whenever possible.&lt;/p&gt;





&lt;h3 id="protecting-too-much-code"&gt;Protecting Too Much Code&lt;/h3&gt;

&lt;p&gt;Developers sometimes wrap entire workflows inside a distributed lock. For example:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Acquire Lock

↓

Call External Payment API

↓

Generate PDF

↓

Upload File

↓

Send Email

↓

Release Lock
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This means every other server waits while network requests, file generation, and email delivery are taking place. Often, only a small portion of the workflow actually requires exclusive access. A better approach is to keep the critical section as short as possible.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Acquire Lock

↓

Update Shared Resource

↓

Release Lock

↓

Generate PDF

↓

Send Email
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The shorter the lock, the better the system scales.&lt;/p&gt;





&lt;h2 id="distributed-locks-vs-database-locks"&gt;Distributed Locks vs Database Locks&lt;/h2&gt;

&lt;p&gt;At first glance, distributed locks and database locks appear very similar. Both prevent concurrent operations and both coordinate access to shared resources, but they operate at completely different levels.&lt;/p&gt;

&lt;p&gt;A database lock protects &lt;strong&gt;data inside the database&lt;/strong&gt;. For example, when two transactions attempt to update the same customer record, the database can lock that row until one transaction completes. Everything happens within the database engine itself.&lt;/p&gt;

&lt;p&gt;A distributed lock protects &lt;strong&gt;work performed by application servers&lt;/strong&gt;. Instead of preventing two transactions from updating the same row, it prevents two application instances from starting the same business process. Consider generating monthly invoices: before any invoice rows even exist in the database, every server must first decide whether it should begin the job. That decision happens outside the database, and a distributed lock coordinates that decision.&lt;/p&gt;

&lt;p&gt;An easy way to remember the difference is:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;Database locks protect data. Distributed locks protect business operations.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In many systems, you’ll use both together. A distributed lock ensures only one server begins generating invoices, and database transactions then ensure every invoice is written consistently.&lt;/p&gt;





&lt;h2 id="distributed-locks-vs-optimistic-concurrency"&gt;Distributed Locks vs Optimistic Concurrency&lt;/h2&gt;

&lt;p&gt;Another concept frequently confused with distributed locks is optimistic concurrency. Optimistic concurrency assumes conflicts are relatively rare. Instead of preventing multiple users from editing the same record, it detects whether someone else changed the data before saving.&lt;/p&gt;

&lt;p&gt;Suppose two employees open the same customer profile, and each begins editing. The system stores a version number alongside the record. When Employee A saves, the version changes from &lt;strong&gt;5&lt;/strong&gt; to &lt;strong&gt;6&lt;/strong&gt;. When Employee B later attempts to save, the application notices that the version has already changed. Rather than silently overwriting Employee A’s work, it rejects the update and asks Employee B to refresh the page. No locking was required; the conflict was simply detected before committing.&lt;/p&gt;

&lt;p&gt;Distributed locks take a different approach. Instead of detecting conflicts afterward, they prevent conflicting work from starting in the first place. Neither technique is universally better: optimistic concurrency works well when conflicts are uncommon, while distributed locks work best when duplicate execution would be expensive or dangerous.&lt;/p&gt;





&lt;h2 id="best-practices"&gt;Best Practices&lt;/h2&gt;

&lt;p&gt;As with most distributed systems techniques, simplicity is your friend. If you’re considering introducing distributed locks into your application, the following guidelines will help you avoid many common problems.&lt;/p&gt;

&lt;h4 id="keep-critical-sections-small"&gt;Keep Critical Sections Small&lt;/h4&gt;

&lt;p&gt;Acquire the lock immediately before modifying shared resources, and release it immediately afterward. The less work performed while holding the lock, the better your system scales.&lt;/p&gt;





&lt;h4 id="always-use-lock-expiration"&gt;Always Use Lock Expiration&lt;/h4&gt;

&lt;p&gt;Servers crash, containers restart, and networks fail. Never assume your application will always release its lock correctly. Expiration protects the rest of the system from waiting forever.&lt;/p&gt;





&lt;h4 id="verify-lock-ownership"&gt;Verify Lock Ownership&lt;/h4&gt;

&lt;p&gt;Before releasing a lock, ensure your application still owns it. Ownership checks prevent one server from accidentally deleting another server’s lock after an expiration or retry.&lt;/p&gt;





&lt;h4 id="design-for-failure"&gt;Design for Failure&lt;/h4&gt;

&lt;p&gt;Distributed systems should always assume that something will eventually fail. Ask yourself:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;What happens if Redis becomes unavailable?&lt;/li&gt;
  &lt;li&gt;What happens if the server crashes?&lt;/li&gt;
  &lt;li&gt;What happens if the network partitions?&lt;/li&gt;
  &lt;li&gt;What happens if the lock expires unexpectedly?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Thinking through failure scenarios early often prevents painful production incidents later.&lt;/p&gt;





&lt;h4 id="combine-reliability-patterns"&gt;Combine Reliability Patterns&lt;/h4&gt;

&lt;p&gt;Distributed locks are rarely used in isolation. Production systems often combine multiple reliability techniques. For example:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
&lt;strong&gt;Idempotency&lt;/strong&gt; prevents duplicate requests.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Transactions&lt;/strong&gt; guarantee atomic database updates.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Isolation Levels&lt;/strong&gt; provide predictable views of data.&lt;/li&gt;
  &lt;li&gt;
&lt;strong&gt;Distributed Locks&lt;/strong&gt; coordinate multiple application servers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each technique solves a different problem. Together, they create systems that continue behaving correctly even under heavy load and unexpected failures.&lt;/p&gt;





&lt;h2 id="bringing-it-all-together"&gt;Bringing It All Together&lt;/h2&gt;

&lt;p&gt;At this point in the series, we’ve explored several concepts that all contribute to building reliable backend systems. Each one addresses a different question.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Concept&lt;/th&gt;
      &lt;th&gt;Question It Answers&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Idempotency&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;What if the same request is sent twice?&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Race Conditions&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;What if multiple requests modify the same data simultaneously?&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Database Transactions&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;What if part of my operation fails?&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Isolation Levels&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;What should transactions be allowed to see while others are running?&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Distributed Locks&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;How do multiple servers agree who should perform a task?&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Notice how these concepts complement one another. None replaces the others; reliable systems are built by combining the right tools for the right problems.&lt;/p&gt;





&lt;h2 id="final-thoughts"&gt;Final Thoughts&lt;/h2&gt;

&lt;p&gt;As applications grow, the biggest challenges often aren’t writing business logic. They’re coordinating work across multiple users, multiple requests, multiple transactions, and eventually multiple servers.&lt;/p&gt;

&lt;p&gt;Distributed locks exist because modern applications are no longer confined to a single machine. They’re deployed across clusters, containers, cloud regions, and worker nodes that all need to cooperate without constantly stepping on each other’s toes.&lt;/p&gt;

&lt;p&gt;Like transactions and isolation levels, distributed locks aren’t something you’ll use for every feature. But when you do need them, they can be the difference between a system that behaves predictably and one that quietly creates duplicate invoices, repeated payments, or inconsistent business data.&lt;/p&gt;

&lt;p&gt;The next time you design a background job, scheduled task, or critical workflow, ask yourself one simple question:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;“What happens if two servers try to do this at exactly the same time?”&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If the answer is “something bad,” you’ve probably found a place where a distributed lock belongs.&lt;/p&gt;





&lt;h2 id="whats-next"&gt;What’s Next?&lt;/h2&gt;

&lt;p&gt;We’ve now covered:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Contract Testing&lt;/li&gt;
  &lt;li&gt;Idempotency&lt;/li&gt;
  &lt;li&gt;Race Conditions&lt;/li&gt;
  &lt;li&gt;Database Transactions&lt;/li&gt;
  &lt;li&gt;Database Concurrency&lt;/li&gt;
  &lt;li&gt;Database Isolation Levels&lt;/li&gt;
  &lt;li&gt;Distributed Locks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So far, every concept has focused on keeping operations consistent &lt;strong&gt;while they’re happening&lt;/strong&gt;. But there’s another challenge waiting. Imagine you’ve successfully updated your database inside a transaction and now need to publish an event to Kafka, RabbitMQ, or another message broker. What happens if the database commit succeeds, but publishing the event fails? Or worse, what if the event is published but the transaction rolls back?&lt;/p&gt;

&lt;p&gt;This problem has caused countless production incidents in distributed systems. In the next article, we’ll explore &lt;strong&gt;The Outbox Pattern Explained: Publishing Events Without Losing Data&lt;/strong&gt;, one of the most widely used patterns for ensuring your database and message broker stay in sync.&lt;/p&gt;

</description>
      <category>distributedsystems</category>
      <category>concurrency</category>
    </item>
  </channel>
</rss>
