<?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: Emmanuel Loveday</title>
    <description>The latest articles on DEV Community by Emmanuel Loveday (@freewayz-dev).</description>
    <link>https://dev.to/freewayz-dev</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%2F4122562%2Fae1b6ed2-9edc-4dcd-a719-e8d14853b6e9.png</url>
      <title>DEV Community: Emmanuel Loveday</title>
      <link>https://dev.to/freewayz-dev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/freewayz-dev"/>
    <language>en</language>
    <item>
      <title>What Actually Changes About Engineering Decisions When You're a Technical Co-Founder</title>
      <dc:creator>Emmanuel Loveday</dc:creator>
      <pubDate>Sun, 27 Sep 2026 12:04:00 +0000</pubDate>
      <link>https://dev.to/freewayz-dev/what-actually-changes-about-engineering-decisions-when-youre-a-technical-co-founder-c0m</link>
      <guid>https://dev.to/freewayz-dev/what-actually-changes-about-engineering-decisions-when-youre-a-technical-co-founder-c0m</guid>
      <description>&lt;p&gt;The first few months of co-founding Cygio, I kept making the same mistake: treating technical decisions like I still had a manager who'd catch it if I got one wrong. Nobody was going to catch it. That's the actual difference between being an employee and being a co-founder, and it's not about hours or title, it's about who absorbs the cost when a technical call turns out to be wrong.&lt;/p&gt;

&lt;p&gt;As an employee, a bad architecture decision is expensive for the company and mildly embarrassing for you. As a co-founder, a bad architecture decision is expensive for the thing you're personally betting years on. That reframes almost every choice, and most of it isn't about becoming more careful, it's about optimizing for a completely different variable than you used to.&lt;/p&gt;

&lt;h2&gt;
  
  
  Every technical decision is now a business decision
&lt;/h2&gt;

&lt;p&gt;There's no separate lane anymore for "engineering choices" that don't touch the business. Picking a database isn't just a database choice, it's a statement about how much runway you're willing to spend on infrastructure instead of talking to users. Picking to build a feature yourself instead of buying it off the shelf isn't a technical purity question, it's a bet that the weeks it costs are worth more than the money buying it would cost. Every "how should we build this" question is quietly also a "is this the best use of the only two people who can build anything right now" question.&lt;/p&gt;

&lt;h2&gt;
  
  
  Boring technology stops being a compromise
&lt;/h2&gt;

&lt;p&gt;I like new tools as much as the next engineer. As a co-founder, that curiosity gets expensive fast. The whole point of an early-stage product is finding out whether anyone wants it, and every hour spent debugging an unfamiliar framework instead of shipping the thing you're trying to validate is an hour that doesn't answer that question.&lt;/p&gt;

&lt;p&gt;So the calculus changes: pick the framework with the most Stack Overflow answers, not the most interesting one. Use a hosted Postgres instead of running your own. Don't write your own auth, use something that already handles password resets and session expiry correctly, because getting that subtly wrong is a security problem you won't notice until it's a real one. None of this is about being incurious. It's about spending your limited attention on the 10% of the stack that's actually your product, and treating the other 90% as a solved problem you shouldn't re-solve.&lt;/p&gt;

&lt;h2&gt;
  
  
  Technical debt becomes a tool instead of an accident
&lt;/h2&gt;

&lt;p&gt;As an employee, technical debt usually happens to you: a deadline gets moved up, a shortcut gets taken under pressure, and it sits there until someone with enough seniority finally gets time allocated to fix it. As a co-founder, you get to take on debt &lt;em&gt;on purpose&lt;/em&gt;, which is a genuinely different thing. Hardcoding a value that should eventually be configurable, skipping a proper admin panel in favor of running SQL by hand for your first ten customers, building a feature that only works for one specific workflow because that's the only workflow you have right now: all of that is fine, as long as you actually know you're doing it and you go back for it when the volume that made the shortcut cheap stops being true.&lt;/p&gt;

&lt;p&gt;The failure mode isn't taking on debt. It's taking on debt without noticing, and finding out six months later that the thing you thought was a temporary hack is now load-bearing for a part of the product you can't easily touch anymore.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one place where cutting corners actually bites
&lt;/h2&gt;

&lt;p&gt;Almost everything above argues for moving fast and not over-engineering. There's one category where that advice inverts: anything that's expensive to migrate away from later. Your core data model is the clearest example. A UI shortcut is a day of cleanup later. A data model that doesn't represent your domain correctly, baked into production data from real users, is a migration project, and migration projects at a two-person company are the kind of thing that quietly eats a month you didn't plan for.&lt;/p&gt;

&lt;p&gt;This doesn't mean over-designing your schema on day one before you know what the product actually is. It means spending real thought on the few things that are genuinely hard to change later, pricing model, core entities, how you represent the central object in your domain, and being much more relaxed everywhere else.&lt;/p&gt;

&lt;h2&gt;
  
  
  What co-founding actually taught me
&lt;/h2&gt;

&lt;p&gt;Not a new framework, not a new architecture pattern. The actual skill was learning to estimate a different question than the one I used to estimate. As an employee I was estimating "how long will this take." As a co-founder the question that actually matters is "what's the cost if I'm wrong about this," and those two questions point you toward completely different answers. Something that takes three days but is nearly free to undo if it's wrong is a much easier call than something that takes three hours but is expensive to unwind. Once that's the actual question you're asking, most of the "move fast vs. do it right" tension that used to feel like a philosophical debate turns out to have a pretty mechanical answer most of the time.&lt;/p&gt;

</description>
      <category>career</category>
      <category>startup</category>
      <category>leadership</category>
      <category>engineering</category>
    </item>
    <item>
      <title>"Server Components Aren't Just SSR Again: Rethinking State in the Next.js App Router"</title>
      <dc:creator>Emmanuel Loveday</dc:creator>
      <pubDate>Sun, 20 Sep 2026 21:30:00 +0000</pubDate>
      <link>https://dev.to/freewayz-dev/server-components-arent-just-ssr-again-rethinking-state-in-the-nextjs-app-router-2b31</link>
      <guid>https://dev.to/freewayz-dev/server-components-arent-just-ssr-again-rethinking-state-in-the-nextjs-app-router-2b31</guid>
      <description>&lt;p&gt;The most common mistake I see with the Next.js App Router isn't a bug, it's a mental model problem. Developers treat Server Components as a faster version of the old server-side rendering they already know: the server renders HTML, the client hydrates it, and everything works the way class-based SSR always worked. Then they add a &lt;code&gt;useState&lt;/code&gt; to a page and get a confusing error, slap &lt;code&gt;"use client"&lt;/code&gt; on the top of the file to make it go away, and the mental model never gets corrected. Eventually the whole app is client components again, just with extra steps.&lt;/p&gt;

&lt;h2&gt;
  
  
  The actual model: two component trees, not one
&lt;/h2&gt;

&lt;p&gt;Traditional SSR renders the same component tree on the server and the client; the server output is a head start on a render the client will redo. Server Components aren't a head start on anything. They run once, on the server, and never run again. They have no lifecycle, no re-renders, no access to &lt;code&gt;useState&lt;/code&gt;, &lt;code&gt;useEffect&lt;/code&gt;, or any browser API, because none of that concept exists for them. What reaches the browser isn't their code, it's their &lt;em&gt;output&lt;/em&gt;, serialized as part of the React Server Component payload.&lt;/p&gt;

&lt;p&gt;Client Components are the ones that behave the way React always has: they hydrate, they re-render, they hold state. The App Router isn't one tree with a rendering optimization, it's two trees that compose into each other, and knowing which one a given component belongs to changes what you can do inside it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The default should be server, and client should be a leaf
&lt;/h2&gt;

&lt;p&gt;The practical failure mode is marking an entire page &lt;code&gt;"use client"&lt;/code&gt; because one button on it needs an &lt;code&gt;onClick&lt;/code&gt;. That drags the whole subtree into the client bundle, along with every dependency it imports, even the parts that never needed to be interactive.&lt;/p&gt;

&lt;p&gt;The fix is almost always to push the client boundary down to the smallest possible leaf:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// page.js — stays a Server Component&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;LikeButton&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./like-button&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;ProductPage&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;product&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getProduct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// fetched on the server, no client waterfall&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;

     &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;   

     &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;description&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// like-button.js — the only part that actually needs the client&lt;/span&gt;
&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;use client&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;LikeButton&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;initialCount&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setCount&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;initialCount&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt;  &lt;span class="nf"&gt;setCount&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="nx"&gt;likes&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything above &lt;code&gt;LikeButton&lt;/code&gt; stays a Server Component: it fetches data directly, with no client-side loading state and no waterfall, and ships zero JavaScript to the browser for that part of the tree.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern almost nobody documents well: server components as children
&lt;/h2&gt;

&lt;p&gt;The part that trips people up next is assuming a Client Component "infects" everything nested inside its JSX. It doesn't, if you pass the server-rendered part in as &lt;code&gt;children&lt;/code&gt; (or any prop) rather than importing and instantiating it from inside the client file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// theme-panel.js — Client Component (needs open/close state)&lt;/span&gt;
&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;use client&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;ThemePanel&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;children&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;open&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setOpen&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;

       &lt;span class="nf"&gt;setOpen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;open&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;Toggle&lt;/span&gt;
       &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;open&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// page.js — Server Component&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;ThemePanel&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./theme-panel&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;ExpensiveServerRenderedContent&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./expensive-content&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// stays server-only&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Page&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;

  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ExpensiveServerRenderedContent&lt;/code&gt; is created in the Server Component tree and passed down as an already-rendered value. &lt;code&gt;ThemePanel&lt;/code&gt; just decides when to show it; it never imports it, so it never pulls it into the client bundle. This is the pattern that lets you keep interactive shells (modals, tabs, accordions) as small client components while everything they display stays server-rendered.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where state actually has to live
&lt;/h2&gt;

&lt;p&gt;Some things genuinely need the client: form inputs, anything with &lt;code&gt;useState&lt;/code&gt; or &lt;code&gt;useEffect&lt;/code&gt;, anything reading a browser API. The gotcha is Context: a Context Provider has to be a Client Component too, since Context is a runtime mechanism, so a global theme or auth provider will pull in a client boundary near the root by necessity. That's fine and expected, the goal was never zero client components, it's making the client boundary match where interactivity actually starts instead of defaulting to it everywhere.&lt;/p&gt;

&lt;p&gt;For mutations, Server Actions replace a lot of what used to be a client-side &lt;code&gt;fetch&lt;/code&gt; call to an API route, and they degrade gracefully: a form wired to a Server Action via &lt;code&gt;&lt;br&gt;
&lt;/code&gt; still submits and works even before the client JavaScript has loaded, because the browser's native form submission is the fallback, not an afterthought.&lt;/p&gt;

&lt;h2&gt;
  
  
  The checklist that replaces the confusion
&lt;/h2&gt;

&lt;p&gt;For any given component, the question isn't "server or client feels right," it's:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Does it use &lt;code&gt;useState&lt;/code&gt;, &lt;code&gt;useEffect&lt;/code&gt;, or another hook that needs the client runtime? Client.&lt;/li&gt;
&lt;li&gt;Does it attach an event handler, or read a browser-only API? Client.&lt;/li&gt;
&lt;li&gt;Otherwise: Server, by default, including anything doing &lt;code&gt;async&lt;/code&gt; data fetching.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once that's the actual decision rule, instead of "whatever makes the error go away," the App Router stops feeling like SSR with extra rules and starts feeling like what it is: a way to keep almost all of an app's weight on the server, and ship client JavaScript only for the parts of the page that are actually interactive.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>react</category>
      <category>webdev</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Designing Frontend Interfaces for Unreliable Networks and Low-End Devices</title>
      <dc:creator>Emmanuel Loveday</dc:creator>
      <pubDate>Sat, 12 Sep 2026 21:16:00 +0000</pubDate>
      <link>https://dev.to/freewayz-dev/designing-frontend-interfaces-for-unreliable-networks-and-low-end-devices-26fk</link>
      <guid>https://dev.to/freewayz-dev/designing-frontend-interfaces-for-unreliable-networks-and-low-end-devices-26fk</guid>
      <description>&lt;p&gt;Most performance guides are written by people testing on a MacBook over office wifi. The advice is usually correct and usually irrelevant to the users who need it most: someone on a three-year-old Android phone, on 3G, in a place where the connection drops mid-request. I spent a good part of my career building products in that reality, including a remittance product where a failed request wasn't an inconvenience, it was someone's money in an uncertain state. That constraint changes how you think about frontend engineering more than any framework choice does.&lt;/p&gt;

&lt;p&gt;This isn't a list of generic performance tips. It's the specific set of decisions that matter once you stop assuming a fast, stable connection and a capable device.&lt;/p&gt;

&lt;h2&gt;
  
  
  Perceived performance beats raw speed
&lt;/h2&gt;

&lt;p&gt;On a slow connection, the actual latency is out of your control. What's in your control is what the user perceives while they wait. A spinner tells the user "something is happening" but gives them nothing to look at and no sense of progress. A skeleton screen that mirrors the eventual layout does more work with the same wait time, because the user's brain starts parsing the page before the data arrives.&lt;/p&gt;

&lt;p&gt;The bigger lever is optimistic UI: update the interface as if the action already succeeded, then reconcile with the server response in the background.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;useOptimisticToggle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;initial&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;mutate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setState&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;initial&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;pending&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setPending&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;toggle&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;previous&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nf"&gt;setState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;      &lt;span class="c1"&gt;// update immediately&lt;/span&gt;
    &lt;span class="nf"&gt;setPending&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;mutate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;setState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;previous&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// roll back on failure&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;setPending&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;toggle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pending&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rollback path is the part people skip. Optimistic UI without a rollback is just a UI that lies to the user when the network fails, and on unreliable connections, the network fails often enough that this matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  Retries need a backoff, and mutations need idempotency
&lt;/h2&gt;

&lt;p&gt;On a flaky connection, a request that times out doesn't mean it failed. It might have reached the server and succeeded; the response just never made it back. Naively retrying a &lt;code&gt;POST&lt;/code&gt; in that situation can trigger the action twice. For a "like" button, that's harmless. For anything transactional, like moving money or submitting an application, it isn't.&lt;/p&gt;

&lt;p&gt;The fix is an idempotency key generated client-side and sent with the request, so the server can recognize a retried request as the same request and return the original result instead of repeating the action:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;fetchWithRetry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="nx"&gt;retries&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;idempotencyKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;idempotencyKey&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randomUUID&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;retries&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;attempt&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Idempotency-Key&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;idempotencyKey&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;retries&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;retryable&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;retries&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;delay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="nx"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// backoff + jitter&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The jitter matters as much as the backoff. If a batch of clients all lose connection to the same flaky cell tower at once, they'll all retry at the same intervals without it, turning a brief network blip into a thundering herd against your API the moment it recovers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Distinguish "slow" from "offline"
&lt;/h2&gt;

&lt;p&gt;These call for different UI, and conflating them is a common mistake. Offline means stop trying and tell the user clearly. Slow means keep trying, but tell the user it's taking longer than usual rather than leaving them staring at a spinner that looks identical at two seconds and twenty. The &lt;code&gt;navigator.onLine&lt;/code&gt; flag and the &lt;code&gt;online&lt;/code&gt;/&lt;code&gt;offline&lt;/code&gt; window events get you the first signal; a simple elapsed-time threshold on your loading state gets you the second.&lt;/p&gt;

&lt;p&gt;The Network Information API (&lt;code&gt;navigator.connection&lt;/code&gt;) is worth knowing about, even though support is inconsistent: where it exists, &lt;code&gt;effectiveType&lt;/code&gt; and &lt;code&gt;saveData&lt;/code&gt; let you make real decisions, like skipping a video autoplay or requesting a smaller image, based on the connection the user actually has instead of the one you tested on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bundle size is a UX decision, not a build metric
&lt;/h2&gt;

&lt;p&gt;On a low-end device, JavaScript isn't just slow to download over a bad connection, it's slow to &lt;em&gt;parse and execute&lt;/em&gt; once it arrives, because the CPU is also weaker. A bundle that feels instant on a modern laptop can add a full second or more of main-thread blocking on a budget Android device, and that cost doesn't show up in a wifi-and-MacBook test.&lt;/p&gt;

&lt;p&gt;Two things pay for themselves disproportionately here: route-based code splitting so users only download what the current screen needs, and being deliberate about dependencies, since a large date-formatting or animation library pulled in for one small feature is a tax paid by every user on every visit. Testing this requires actually turning on CPU throttling (4x slowdown is a reasonable baseline) and a slow-network profile in devtools, not just checking the numbers on your own machine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Images: decode cost matters as much as file size
&lt;/h2&gt;

&lt;p&gt;Responsive &lt;code&gt;srcset&lt;/code&gt;s and modern formats solve the download problem. They don't solve the decode problem: a large image still costs CPU and memory to decode and paint, and that cost is much higher relative to the total budget on a low-end device. A blur-up placeholder (a tiny, heavily compressed version shown immediately, swapped for the full image once decoded) keeps the layout stable and gives the user something meaningful during that gap, instead of a blank box that suddenly pops in.&lt;/p&gt;

&lt;h2&gt;
  
  
  The actual takeaway
&lt;/h2&gt;

&lt;p&gt;None of this is exotic. Optimistic updates, backoff with jitter, idempotency keys, and deliberate bundle size are all well-known techniques individually. What changes when you build for unreliable networks and low-end devices is the priority order: these stop being edge-case polish and become the baseline the product has to work on for a meaningful share of its users. Test on the conditions your actual users have, not the ones your laptop has, and a lot of these decisions make themselves.&lt;/p&gt;

</description>
      <category>performance</category>
      <category>react</category>
      <category>webdev</category>
      <category>fintech</category>
    </item>
  </channel>
</rss>
