<?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>Forem: CodeScoop.dev</title>
    <description>The latest articles on Forem by CodeScoop.dev (@codescoop).</description>
    <link>https://forem.com/codescoop</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.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F2578%2F79cddd4e-7cf7-4c07-b40c-fe749dbd00a6.png</url>
      <title>Forem: CodeScoop.dev</title>
      <link>https://forem.com/codescoop</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/codescoop"/>
    <language>en</language>
    <item>
      <title>Real-Time on the Frontend - SSE, WebSockets &amp; Polling</title>
      <dc:creator>Yogesh Yadav</dc:creator>
      <pubDate>Sun, 03 May 2026 13:58:37 +0000</pubDate>
      <link>https://forem.com/codescoop/real-time-on-the-frontend-sse-websockets-polling-i2f</link>
      <guid>https://forem.com/codescoop/real-time-on-the-frontend-sse-websockets-polling-i2f</guid>
      <description>&lt;p&gt;Real-time doesn't always mean WebSockets. Picking the right tool changes everything.&lt;/p&gt;

&lt;p&gt;In this article we'll cover when polling is actually the right answer, why SSE is the most underrated real-time technology on the web, when WebSockets are genuinely worth the complexity, how to handle reconnections properly, lessons from building real-time features at scale and a clear decision framework so you stop defaulting to WebSockets out of habit.&lt;/p&gt;




&lt;p&gt;Every time a developer hears "real-time", the next word out of their mouth is usually "WebSockets."&lt;/p&gt;

&lt;p&gt;I get it. WebSockets feel like the serious, production-grade choice. Polling feels naive. SSE feels like something you read about once and forgot. So the default becomes WebSockets and teams end up maintaining infrastructure complexity they didn't need for a problem that SSE would have solved in 30 lines.&lt;/p&gt;

&lt;p&gt;I've built real-time features on platforms handling more than 10 million active users. Subscription verification flows, live event score updates, notification systems. Different problems, different tools. And the most important lesson I've taken from all of it is this: pick the simplest technology that actually solves the problem.&lt;/p&gt;

&lt;p&gt;This article will help you do that.&lt;/p&gt;




&lt;h2&gt;
  
  
  Polling - Not Always the Wrong Answer
&lt;/h2&gt;

&lt;p&gt;Polling gets dismissed quickly. "Just use WebSockets" is the usual response. But polling has a place and understanding when it's appropriate saves you from over-engineering.&lt;/p&gt;

&lt;h3&gt;
  
  
  Short Polling
&lt;/h3&gt;

&lt;p&gt;The simplest form. Make a request every N seconds, get a response, repeat.&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;startPolling&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;interval&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5000&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="nf"&gt;setInterval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/status&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="nf"&gt;updateUI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;interval&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;This works fine when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The data changes infrequently and slight delays are acceptable.&lt;/li&gt;
&lt;li&gt;You have a small number of concurrent users.&lt;/li&gt;
&lt;li&gt;The implementation complexity budget is low and you need something working fast.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Where it falls apart is scale.&lt;/strong&gt; If 100,000 users are polling every 5 seconds, that's 20,000 requests per second hitting your server for data that probably hasn't changed. At that point polling isn't simple anymore - it's expensive.&lt;/p&gt;

&lt;h3&gt;
  
  
  Smarter Polling
&lt;/h3&gt;

&lt;p&gt;If you're going to poll, at least do it intelligently. Back off when the tab is hidden, increase the interval when responses show no change, stop entirely when the user is inactive.&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;smartPoll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fetchFn&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="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;baseInterval&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;maxInterval&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;60000&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;currentInterval&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;baseInterval&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;timeoutId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&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;poll&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="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hidden&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;timeoutId&lt;/span&gt; &lt;span class="o"&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;poll&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;maxInterval&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&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;fetchFn&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nx"&gt;currentInterval&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;changed&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;baseInterval&lt;/span&gt; &lt;span class="p"&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;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;currentInterval&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;1.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;maxInterval&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nx"&gt;timeoutId&lt;/span&gt; &lt;span class="o"&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;poll&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;currentInterval&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;poll&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;clearTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;timeoutId&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;This alone can cut unnecessary requests significantly without changing the fundamental approach.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When to use polling:&lt;/strong&gt; low-frequency updates, small user base, dashboard data that refreshes every minute or so, situations where you need something working today and can revisit later.&lt;/p&gt;




&lt;h2&gt;
  
  
  SSE - The Underrated Middle Ground
&lt;/h2&gt;

&lt;p&gt;Server-Sent Events is the technology most developers skip over on their way to WebSockets. That's a mistake.&lt;/p&gt;

&lt;p&gt;SSE is a one-directional, HTTP-native protocol. The server pushes data to the client over a persistent connection. The client listens. That's it.&lt;/p&gt;

&lt;p&gt;What makes SSE genuinely compelling:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It runs over regular HTTP.&lt;/strong&gt; No protocol upgrade, no special infrastructure, no load balancer configuration changes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The browser handles reconnection automatically.&lt;/strong&gt; If the connection drops, the browser retries with the last event ID so you don't miss events.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It works through HTTP/2 multiplexing&lt;/strong&gt;, meaning multiple SSE connections share a single TCP connection.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's dramatically simpler to implement than WebSockets&lt;/strong&gt; on both the client and server side.
&lt;/li&gt;
&lt;/ul&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;connectToEventStream&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;handlers&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;eventSource&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;EventSource&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="na"&gt;withCredentials&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="nx"&gt;eventSource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onopen&lt;/span&gt; &lt;span class="o"&gt;=&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;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SSE connection established&lt;/span&gt;&lt;span class="dl"&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;eventSource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onmessage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nx"&gt;handlers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;eventSource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onerror&lt;/span&gt; &lt;span class="o"&gt;=&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="o"&gt;=&amp;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;eventSource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;readyState&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;EventSource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;CLOSED&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;handlers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onClose&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;// Listen to named events&lt;/span&gt;
  &lt;span class="nx"&gt;eventSource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;subscription-update&lt;/span&gt;&lt;span class="dl"&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;event&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;handlers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onSubscriptionUpdate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;eventSource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&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 server side is equally straightforward. Any server that can keep an HTTP connection alive and stream data to it can serve SSE. No WebSocket library needed.&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="c1"&gt;// Express example&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/events&lt;/span&gt;&lt;span class="dl"&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;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&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;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text/event-stream&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Cache-Control&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;no-cache&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Connection&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;keep-alive&lt;/span&gt;&lt;span class="dl"&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;sendEvent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;eventName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&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;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`event: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;eventName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;\n`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`data: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;\n\n`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Send initial state&lt;/span&gt;
  &lt;span class="nf"&gt;sendEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;connected&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="c1"&gt;// Clean up on disconnect&lt;/span&gt;
  &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;close&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&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="c1"&gt;// Remove this client from your subscriber list&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;h3&gt;
  
  
  Where SSE Shines at Scale
&lt;/h3&gt;

&lt;p&gt;I've used SSE for subscription verification flows where the client needs to know the moment a payment has been confirmed on the backend. The user completes a payment, the frontend opens an SSE connection and the moment the backend confirms the transaction, it pushes an event. The client reacts immediately.&lt;/p&gt;

&lt;p&gt;No polling loop hammering the endpoint. No WebSocket infrastructure. Just an HTTP connection that the server writes to when something happens.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SSE is the right tool for:&lt;/strong&gt; notifications, live feed updates, subscription and payment status, progress tracking for long-running operations, any scenario where data flows server-to-client and you don't need the client to send messages back in real time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The one real limitation:&lt;/strong&gt; SSE is one-directional. The client cannot send data back over the same connection. If you need bidirectional communication, you need WebSockets.&lt;/p&gt;




&lt;h2&gt;
  
  
  WebSockets - When You Actually Need Them
&lt;/h2&gt;

&lt;p&gt;WebSockets give you a full-duplex connection. Both the client and server can send messages at any time over a single persistent connection. That's genuinely powerful and genuinely more complex to operate.&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;class&lt;/span&gt; &lt;span class="nc"&gt;WebSocketClient&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;socket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reconnectAttempts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;maxReconnectAttempts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;listeners&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;socket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;WebSocket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onopen&lt;/span&gt; &lt;span class="o"&gt;=&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;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;WebSocket connected&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reconnectAttempts&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onmessage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&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;handler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;listeners&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&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;handler&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onclose&lt;/span&gt; &lt;span class="o"&gt;=&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reconnect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onerror&lt;/span&gt; &lt;span class="o"&gt;=&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;WebSocket error&lt;/span&gt;&lt;span class="dl"&gt;'&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="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;payload&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;readyState&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;WebSocket&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="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;payload&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="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;listeners&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;reconnect&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reconnectAttempts&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;maxReconnectAttempts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Max reconnect attempts reached&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="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="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1000&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reconnectAttempts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;30000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reconnectAttempts&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;

    &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&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="nf"&gt;disconnect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;close&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;WebSockets are the right tool for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Chat and messaging&lt;/strong&gt; - messages flow in both directions in real time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Collaborative editing&lt;/strong&gt; - multiple users editing the same document simultaneously.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Live gaming&lt;/strong&gt; - state needs to sync between players with minimal latency.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Live auctions or trading&lt;/strong&gt; - both the server and client are pushing updates constantly.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What most tutorials don't tell you about WebSockets:
&lt;/h3&gt;

&lt;p&gt;Load balancers need sticky sessions or a pub/sub layer. A WebSocket connection is stateful - it lives on one server instance. If you're running multiple server instances behind a load balancer, a message published on instance A won't reach clients connected to instance B unless you have a message broker like Redis pub/sub in between.&lt;/p&gt;

&lt;p&gt;At scale, this infrastructure cost is real. It's manageable, but it's not free, and it's worth knowing about before you commit to WebSockets for a use case that SSE would have handled.&lt;/p&gt;




&lt;h2&gt;
  
  
  Reconnection -  The Part Everyone Gets Wrong
&lt;/h2&gt;

&lt;p&gt;Every real-time implementation needs to handle disconnections. Networks drop. Servers restart. Mobile users switch between WiFi and cellular. If your reconnection logic is wrong, users silently stop receiving updates with no indication that anything is broken.&lt;/p&gt;

&lt;p&gt;A few things that matter:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Exponential backoff with jitter.&lt;/strong&gt; Don't retry on a fixed interval. If your server goes down and 100,000 clients all retry every 5 seconds in perfect synchrony, they'll create a thundering herd the moment the server comes back up.&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;getReconnectDelay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;attempt&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;base&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;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1000&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="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;30000&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;jitter&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;1000&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;base&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;jitter&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 spreads reconnection attempts across time so your server isn't hit by a simultaneous spike.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Track the last received event.&lt;/strong&gt; For SSE, the browser does this for you with the &lt;code&gt;Last-Event-ID&lt;/code&gt; header. For WebSockets, you need to implement this yourself. When you reconnect, tell the server where you left off so it can replay missed events.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Distinguish between clean closes and unexpected drops.&lt;/strong&gt; A user navigating away is a clean close - don't reconnect. A network error is unexpected - do reconnect. Handle them differently.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Show the user when the connection is lost.&lt;/strong&gt; A silent failure is worse than a visible one. If the real-time connection drops and doesn't recover quickly, tell the user. A small "reconnecting…" indicator is far better than them wondering why the live scores stopped updating.&lt;/p&gt;




&lt;h2&gt;
  
  
  Lessons From Building Real-Time at Scale
&lt;/h2&gt;

&lt;p&gt;A few things you learn when real-time features are running for millions of users that don't show up in tutorials:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Connection count is a resource.&lt;/strong&gt; Every open SSE or WebSocket connection holds state on the server. At scale, you need to think about connection limits, memory per connection and what happens when a deployment causes all connections to drop and reconnect simultaneously.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Health checks matter.&lt;/strong&gt; Long-lived connections can appear alive to both sides while being silently broken in the middle - a dropped connection that neither end detected. Implement a heartbeat: the server sends a ping event every 30 seconds, the client expects it. If it doesn't arrive, reconnect.&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="c1"&gt;// Server heartbeat&lt;/span&gt;
&lt;span class="nf"&gt;setInterval&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;clients&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;client&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;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;event: ping&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;data: {}&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="dl"&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;span class="mi"&gt;30000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// Client - restart connection if no ping received&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;lastPing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="nx"&gt;eventSource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ping&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&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;lastPing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="nf"&gt;setInterval&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;lastPing&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;45000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;eventSource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;reconnect&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="mi"&gt;10000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Authentication on long-lived connections needs thought.&lt;/strong&gt; Tokens expire. If a user's access token expires while they have an open WebSocket connection, how do you handle that? You need a mechanism to refresh the token and either send it over the existing connection or re-establish with a new one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test with realistic concurrency.&lt;/strong&gt; A real-time feature that works perfectly with 10 concurrent connections can behave completely differently with 10,000. Load test before you ship.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Else Is Out There
&lt;/h2&gt;

&lt;h3&gt;
  
  
  WebRTC
&lt;/h3&gt;

&lt;p&gt;WebRTC is a different category entirely. Where SSE and WebSockets are server-client protocols, WebRTC enables peer-to-peer communication directly between browsers. Audio, video and data can flow between clients without going through your server.&lt;/p&gt;

&lt;p&gt;If you're building video calling, live audio or peer-to-peer file sharing, WebRTC is the right tool. It's not a replacement for SSE or WebSockets for server-push use cases - it solves a fundamentally different problem.&lt;/p&gt;

&lt;h3&gt;
  
  
  WebTransport
&lt;/h3&gt;

&lt;p&gt;WebTransport is a newer protocol built on HTTP/3 and QUIC. It offers lower latency than WebSockets, supports both reliable and unreliable message delivery and handles connection migration better on mobile networks. It's promising, but browser support is still maturing. Worth keeping an eye on for the next few years.&lt;/p&gt;

&lt;h3&gt;
  
  
  GraphQL Subscriptions
&lt;/h3&gt;

&lt;p&gt;If your stack uses GraphQL, subscriptions give you real-time updates through a familiar query interface. Under the hood they typically run over WebSockets. Useful if you're already invested in GraphQL and want real-time without adding a separate protocol layer.&lt;/p&gt;




&lt;h2&gt;
  
  
  How to Pick the Right Approach
&lt;/h2&gt;

&lt;p&gt;Here's the decision framework I use:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do you need the client to send messages to the server in real time?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No - SSE is probably enough. Start there.&lt;/li&gt;
&lt;li&gt;Yes - WebSockets.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;How frequently does data change?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every few minutes or less - polling with a sensible interval.&lt;/li&gt;
&lt;li&gt;Continuously or on server events - SSE or WebSockets.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What's your infrastructure complexity budget?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Low - polling or SSE. Both work over standard HTTP.&lt;/li&gt;
&lt;li&gt;Higher - WebSockets, with the understanding that you'll need to handle load balancing and state carefully.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Do you need peer-to-peer communication?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Yes -  WebRTC.&lt;/li&gt;
&lt;li&gt;No - SSE or WebSockets.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Start simple. Polling is not embarrassing. SSE handles more use cases than most developers realize. WebSockets are powerful but come with operational overhead that you should only take on when you genuinely need what they offer.&lt;/p&gt;

&lt;p&gt;The real-time feature that works reliably in production is always better than the one built with the most impressive technology.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Real-time on the frontend is not a single technology decision. It's a series of decisions, made per feature, based on what that feature actually needs.&lt;/p&gt;

&lt;p&gt;SSE is HTTP-native, automatically reconnecting and dramatically simpler than WebSockets for one-directional data flows. Most real-time use cases are one-directional. Start there.&lt;/p&gt;

&lt;p&gt;Use WebSockets when you genuinely need bidirectional communication. Understand the infrastructure implications before you commit.&lt;br&gt;
And never underestimate polling for the right use case. At low frequency and low scale, it's often the most maintainable solution you can ship.&lt;/p&gt;

&lt;p&gt;Pick the simplest tool that solves the problem. Your future self - and the engineer who maintains this after you -  will thank you.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Have thoughts or questions on real-time frontend? Drop them in the comments, always happy to discuss.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This article is part of the &lt;a href="https://dev.to/yogeshyadav/series/38132"&gt;&lt;strong&gt;Frontend at Scale&lt;/strong&gt;&lt;/a&gt; series.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>frontend</category>
      <category>webdev</category>
      <category>react</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Frontend Security - What Your Browser Is Quietly Protecting You From</title>
      <dc:creator>Yogesh Yadav</dc:creator>
      <pubDate>Sun, 26 Apr 2026 11:45:57 +0000</pubDate>
      <link>https://forem.com/codescoop/frontend-security-what-your-browser-is-quietly-protecting-you-from-nih</link>
      <guid>https://forem.com/codescoop/frontend-security-what-your-browser-is-quietly-protecting-you-from-nih</guid>
      <description>&lt;p&gt;The browser is doing more security work than you realize. Here's what happens when you accidentally get in its way.&lt;/p&gt;

&lt;p&gt;In this article we'll cover how XSS actually happens in real frontend codebases, how CSRF works and where the browser's default protections break down, how clickjacking works and how one header stops it, what Content Security Policy actually does and how to implement it without breaking your app, and the CORS misconfigurations that create vulnerabilities while feeling like security.&lt;/p&gt;




&lt;p&gt;Frontend security has a reputation for being someone else's problem. The backend handles auth, the infrastructure team handles firewalls and the frontend just renders what it's given.&lt;/p&gt;

&lt;p&gt;That mindset is expensive.&lt;/p&gt;

&lt;p&gt;I've worked on platforms handling more than 10 million active users. At that scale, a single security misconfiguration isn't a bug report - it's an incident. And the uncomfortable truth is that most of the vulnerabilities I've seen on the frontend weren't sophisticated attacks. They were gaps left behind by developers who didn't know the browser was already protecting them and accidentally turned that protection off.&lt;/p&gt;

&lt;p&gt;This article is about understanding what the browser does for you by default, so you never accidentally undo it.&lt;/p&gt;




&lt;h2&gt;
  
  
  XSS - The Attack That Lives in Your Own Code
&lt;/h2&gt;

&lt;p&gt;Cross-Site Scripting is the most common frontend vulnerability. The idea is simple: an attacker finds a way to get malicious JavaScript to run in your page, in your users' browsers, with access to everything your page has access to - cookies, localStorage, DOM, API tokens.&lt;/p&gt;

&lt;p&gt;The most important thing to understand about XSS is that it doesn't require a sophisticated attacker. It requires a place in your codebase where user-controlled content is rendered without sanitization.&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="c1"&gt;// This is all it takes&lt;/span&gt;
&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;username&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;innerHTML&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;userInput&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;userInput&lt;/code&gt; contains &lt;code&gt;&amp;lt;script&amp;gt;alert(document.cookie)&amp;lt;/script&amp;gt;&lt;/code&gt; that script runs. Game over.&lt;/p&gt;

&lt;p&gt;Modern frameworks help significantly here. React, Vue, and Angular all escape output by default. When you write &lt;code&gt;{userInput}&lt;/code&gt; in JSX, React treats it as text, not HTML. That's the browser's XSS protection working through the framework.&lt;/p&gt;

&lt;p&gt;The danger is when you deliberately bypass it:&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;// This bypasses React's built-in escaping&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;dangerouslySetInnerHTML&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;__html&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;userContent&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;dangerouslySetInnerHTML&lt;/code&gt; exists for legitimate reasons - rendering rich text, markdown output, CMS content. But every time you use it, you're taking on the responsibility of sanitizing that content yourself.&lt;/p&gt;

&lt;p&gt;If you need to render HTML from an untrusted source, sanitize it first:&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;import&lt;/span&gt; &lt;span class="nx"&gt;DOMPurify&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dompurify&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;clean&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;DOMPurify&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sanitize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userContent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;// Now safe to render&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;DOMPurify strips anything that could execute JavaScript while preserving legitimate HTML structure. At scale, this belongs in a shared utility, not copied across components.&lt;/p&gt;

&lt;h3&gt;
  
  
  The other XSS surface most teams miss: third-party scripts.
&lt;/h3&gt;

&lt;p&gt;Every analytics library, chat widget, and ad script you load is running JavaScript in your page with the same privileges as your own code. If any of those scripts are compromised - through a supply chain attack on the npm package or a hijacked CDN -  your users are exposed.&lt;/p&gt;

&lt;p&gt;This is where Content Security Policy becomes critical, which we'll cover shortly.&lt;/p&gt;




&lt;h2&gt;
  
  
  CSRF - The Attack the Browser Already Mostly Fixed
&lt;/h2&gt;

&lt;p&gt;Cross-Site Request Forgery works like this: a user is logged into your platform. They visit a malicious site. That site silently makes a request to your platform's API - a request that carries the user's cookies automatically, because that's how cookies work.&lt;/p&gt;

&lt;p&gt;If your API trusts cookie-authenticated requests without verifying their origin, the attacker can perform actions on behalf of the user without them knowing.&lt;/p&gt;

&lt;p&gt;The browser has quietly been fixing this for years through the SameSite cookie attribute.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;Set-Cookie: session=abc123; SameSite=Lax; Secure; HttpOnly
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;SameSite=Lax&lt;/code&gt; tells the browser: only send this cookie on requests that originate from the same site or on top-level navigation. Cross-site requests from a malicious page won't carry the cookie.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;SameSite=Strict&lt;/code&gt; is even more restrictive - the cookie won't be sent on any cross-site request, including navigations. Good for sensitive session cookies.&lt;/p&gt;

&lt;p&gt;Most modern browsers now default to &lt;code&gt;SameSite=Lax&lt;/code&gt; even if you don't set it explicitly. That's the browser protecting you without you asking.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where this breaks down:
&lt;/h3&gt;

&lt;p&gt;If your frontend and API are on different domains - &lt;code&gt;app.yourplatform.com&lt;/code&gt; and &lt;code&gt;api.yourplatform.com&lt;/code&gt; - cookie handling gets more complex. Subdomains are generally considered same-site, but cross-domain setups are not.&lt;/p&gt;

&lt;p&gt;If you're using token-based auth with the Authorization header instead of cookies, CSRF is largely not your problem. Browsers don't automatically attach Authorization headers to cross-site requests the way they do with cookies.&lt;/p&gt;

&lt;p&gt;But if you're mixing both - cookies for session management and tokens for API calls - understand exactly what each request is sending and why.&lt;/p&gt;




&lt;h2&gt;
  
  
  Clickjacking - The Invisible Iframe Attack
&lt;/h2&gt;

&lt;p&gt;Clickjacking is straightforward in concept. An attacker embeds your platform in an invisible iframe on their site, positioned perfectly over a button the attacker wants you to click. You think you're clicking something on the attacker's page. You're actually clicking something on yours.&lt;/p&gt;

&lt;p&gt;On a platform with payment flows, subscription management or any destructive actions, this is a real risk.&lt;/p&gt;

&lt;p&gt;One HTTP header stops it entirely:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;X-Frame-Options: DENY
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or the more modern equivalent via Content Security Policy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;Content-Security-Policy: frame-ancestors 'none'
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;DENY&lt;/code&gt; means your page cannot be embedded in any iframe, on any site. If you need to allow embedding on specific trusted domains:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;Content-Security-Policy: frame-ancestors 'self' https://trusted-partner.com
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is one of those protections that takes 30 seconds to implement and eliminates an entire category of attack. There is almost no reason not to have it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Content Security Policy - The Most Powerful Tool Nobody Uses
&lt;/h2&gt;

&lt;p&gt;CSP is the most underutilized browser security feature in frontend development. It's also the most powerful.&lt;/p&gt;

&lt;p&gt;A Content Security Policy is a HTTP header that tells the browser exactly what it's allowed to load and execute on your page. Scripts, styles, images, fonts, iframes - you define the allowlist. Anything not on the list gets blocked.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;Content-Security-Policy: 
  default-src 'self';
  script-src 'self' https://cdn.trusted.com;
  style-src 'self' 'unsafe-inline';
  img-src 'self' data: https:;
  font-src 'self' https://fonts.gstatic.com;
  frame-ancestors 'none'
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What this does:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;default-src 'self'&lt;/code&gt; - by default, only load resources from your own domain.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;script-src 'self' https://cdn.trusted.com&lt;/code&gt; - scripts can only come from your domain or this specific CDN. An injected script from anywhere else is blocked by the browser before it executes.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;frame-ancestors 'none'&lt;/code&gt; - replaces X-Frame-Options, prevents clickjacking.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;This is your XSS backstop.&lt;/strong&gt; Even if an attacker injects a script tag into your page, if the script source isn't on your allowlist, the browser won't run it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why most teams don't implement it:
&lt;/h3&gt;

&lt;p&gt;CSP is hard to get right, especially on established codebases. Inline scripts, eval usage and dynamically injected styles all conflict with a strict policy. The error messages aren't always helpful.&lt;/p&gt;

&lt;p&gt;The right approach is to start in report-only mode:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-violations
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This enforces nothing but logs every violation. You can see exactly what your existing codebase would break before you flip the switch. Run it for a few weeks, fix the violations, then move to enforcement.&lt;/p&gt;

&lt;p&gt;At scale, CSP is not optional. It's the difference between a contained incident and a platform-wide compromise.&lt;/p&gt;




&lt;h2&gt;
  
  
  CORS - The Misconfigurations That Feel Like Security
&lt;/h2&gt;

&lt;p&gt;CORS - Cross-Origin Resource Sharing - is widely misunderstood. Most developers encounter it as an error to fix, not a security mechanism to understand.&lt;/p&gt;

&lt;p&gt;Here's the core idea: browsers block JavaScript from reading responses from a different origin unless that origin explicitly allows it. CORS headers on the server tell the browser what's permitted.&lt;/p&gt;

&lt;p&gt;The misconfig that creates vulnerabilities:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;Access-Control-Allow-Origin: *
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This tells the browser any site can read responses from your API. For truly public, unauthenticated APIs this is fine. For an API that returns user data or accepts authenticated requests, this is a problem.&lt;/p&gt;

&lt;p&gt;An even worse pattern:&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="c1"&gt;// On the server - don't do this&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;origin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&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="nx"&gt;origin&lt;/span&gt;
&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Access-Control-Allow-Origin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;origin&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Access-Control-Allow-Credentials&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;true&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This dynamically reflects whatever origin made the request and allows credentials. Effectively any site can make credentialed requests to your API and read the responses. This defeats the entire purpose of CORS.&lt;/p&gt;

&lt;p&gt;The correct approach is an explicit allowlist:&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;const&lt;/span&gt; &lt;span class="nx"&gt;allowedOrigins&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://yourplatform.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://app.yourplatform.com&lt;/span&gt;&lt;span class="dl"&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;origin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&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="nx"&gt;origin&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;allowedOrigins&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;origin&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Access-Control-Allow-Origin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;origin&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;strong&gt;CORS is not an authentication mechanism.&lt;/strong&gt; It controls what the browser allows JavaScript to read. It does not stop direct requests made with curl, Postman or server-to-server calls. Your API still needs proper authentication and authorization regardless of CORS configuration.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Developers Accidentally Break
&lt;/h2&gt;

&lt;p&gt;The browser's security model is opt-out, not opt-in. Most of the protections are on by default. The problems happen when developers don't realize they're turning them off.&lt;/p&gt;

&lt;h3&gt;
  
  
  Using &lt;code&gt;target="_blank"&lt;/code&gt; without &lt;code&gt;rel="noopener"&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Opening a link in a new tab with target="_blank" gives the new page a reference back to your page through window.opener. A malicious site opened this way can redirect your original tab.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- Vulnerable --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://external.com"&lt;/span&gt; &lt;span class="na"&gt;target=&lt;/span&gt;&lt;span class="s"&gt;"_blank"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Link&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;

&lt;span class="c"&gt;&amp;lt;!-- Safe --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://external.com"&lt;/span&gt; &lt;span class="na"&gt;target=&lt;/span&gt;&lt;span class="s"&gt;"_blank"&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"noopener noreferrer"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Link&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Modern browsers now default to &lt;code&gt;noopener&lt;/code&gt; behavior for &lt;code&gt;target="_blank"&lt;/code&gt;, but it's still worth being explicit.&lt;/p&gt;

&lt;h3&gt;
  
  
  Disabling certificate warnings in development
&lt;/h3&gt;

&lt;p&gt;Running your local dev server with a self-signed cert and clicking through browser security warnings trains developers to ignore those warnings. More importantly, configurations that disable certificate validation sometimes make their way into staging or production environments.&lt;/p&gt;

&lt;h3&gt;
  
  
  Putting secrets in frontend code
&lt;/h3&gt;

&lt;p&gt;API keys, client secrets, internal endpoint URLs - these don't belong in frontend code. They end up in your JavaScript bundle, which is downloaded by every user and readable by anyone who opens DevTools. If a key needs to be kept secret, it belongs on the server.&lt;/p&gt;

&lt;h3&gt;
  
  
  Using &lt;code&gt;eval()&lt;/code&gt; or &lt;code&gt;new Function()&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Beyond being a performance issue, eval executes arbitrary strings as JavaScript. If any user-controlled content reaches an eval call, it's an XSS vulnerability. CSP's &lt;code&gt;script-src&lt;/code&gt; directive blocks eval by default for exactly this reason.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;The browser is not your enemy. It's doing a significant amount of security work on your behalf, quietly on every page load. SameSite cookies, CORS restrictions, XSS escaping in frameworks, referrer policies - most of this just works if you don't interfere with it.&lt;/p&gt;

&lt;p&gt;The job of a security-conscious frontend engineer is not to implement security from scratch. It's to understand what the browser already does, build on top of it and avoid accidentally disabling it.&lt;/p&gt;

&lt;p&gt;At scale, that understanding is the difference between a platform your users can trust and one that's one misconfiguration away from an incident.&lt;/p&gt;

&lt;p&gt;Know what the browser does for you. Don't get in its way.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Have thoughts or questions on frontend security? Drop them in the comments, always happy to discuss.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This article is part of the &lt;a href="https://dev.to/yogeshyadav/series/38132"&gt;&lt;strong&gt;Frontend at Scale&lt;/strong&gt;&lt;/a&gt; series.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>frontend</category>
      <category>security</category>
      <category>javascript</category>
      <category>browser</category>
    </item>
    <item>
      <title>Authentication on the Frontend - More Than Just Tokens</title>
      <dc:creator>Yogesh Yadav</dc:creator>
      <pubDate>Sun, 19 Apr 2026 14:12:36 +0000</pubDate>
      <link>https://forem.com/codescoop/authentication-on-the-frontend-more-than-just-tokens-2kj7</link>
      <guid>https://forem.com/codescoop/authentication-on-the-frontend-more-than-just-tokens-2kj7</guid>
      <description>&lt;p&gt;The decisions behind auth are more consequential than most developers realize.&lt;/p&gt;

&lt;p&gt;In this article we'll cover how token storage works and why the wrong choice creates real security vulnerabilities, how silent refresh keeps users logged in without interrupting their experience, how to handle session expiry gracefully, what actually happens during an OAuth flow, and the authentication decisions most frontend developers get wrong.&lt;/p&gt;




&lt;p&gt;Authentication is one of those things that looks simple from the outside. User logs in, you get a token, you attach it to requests. Done.&lt;/p&gt;

&lt;p&gt;Except it's not done. Not even close.&lt;/p&gt;

&lt;p&gt;I've worked on platforms handling more than 10 million active users across multiple OTT applications. Authentication touches every single one of them. And the decisions you make at the frontend layer - where to store tokens, how to refresh them, how to handle expiry - have consequences that go well beyond a login form.&lt;/p&gt;

&lt;p&gt;Get it wrong and you're looking at XSS vulnerabilities, session hijacking or users getting logged out mid-stream during a live event. At scale, any of those is a serious incident.&lt;/p&gt;

&lt;p&gt;This article is about getting it right.&lt;/p&gt;




&lt;h2&gt;
  
  
  Token Storage - The Decision That Has the Most Security Consequences
&lt;/h2&gt;

&lt;p&gt;When you authenticate a user and get back an access token, the first question is: where do you put it?&lt;/p&gt;

&lt;p&gt;There are three common options. Each has real tradeoffs.&lt;/p&gt;

&lt;h3&gt;
  
  
  localStorage
&lt;/h3&gt;

&lt;p&gt;This is the most common choice because it's the easiest. Persist the token, read it back on page load, attach it to requests. Simple.&lt;/p&gt;

&lt;p&gt;The problem is XSS. If an attacker manages to inject malicious JavaScript into your page - through a compromised dependency, a third-party script or an unsanitized input - they can read everything in localStorage. Your token is gone. The attacker now has it.&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="c1"&gt;// Any injected script can do this&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;access_token&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;// Token is now in the attacker's hands&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At scale, with multiple third-party scripts running on your platform, the XSS surface area is larger than you think.&lt;/p&gt;

&lt;h3&gt;
  
  
  httpOnly Cookies
&lt;/h3&gt;

&lt;p&gt;The browser sets these cookies automatically on every request to your domain. JavaScript cannot read them. That means an XSS attack cannot steal them.&lt;/p&gt;

&lt;p&gt;This sounds like the obvious winner, and for many server-rendered applications it is. But it comes with its own complexity. You need to think about CSRF protection, SameSite cookie attributes and cross-origin request handling. On a platform with multiple subdomains or a separate API domain, cookie configuration gets complicated quickly.&lt;/p&gt;

&lt;h3&gt;
  
  
  In-Memory Storage
&lt;/h3&gt;

&lt;p&gt;This is the approach I consider most secure for single-page applications. Store the access token in a JavaScript variable, never in localStorage, never in a cookie readable by JavaScript.&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;let&lt;/span&gt; &lt;span class="nx"&gt;accessToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;setToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;accessToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getToken&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="nx"&gt;accessToken&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An XSS attack cannot read a JavaScript variable from another script's scope if you're careful about how you expose it. The token lives only in memory and disappears when the tab closes.&lt;/p&gt;

&lt;p&gt;The tradeoff is that you lose persistence across page refreshes. Which is exactly why silent refresh exists.&lt;/p&gt;




&lt;h2&gt;
  
  
  Silent Refresh - Keeping Users Logged In Without Asking Them To
&lt;/h2&gt;

&lt;p&gt;Access tokens are short-lived by design. Typically 15 minutes to an hour. That's intentional - if one gets stolen, the damage window is limited.&lt;/p&gt;

&lt;p&gt;But you can't ask a user to log in again every hour. Especially on an OTT platform where they might be mid-stream during a live sports event.&lt;/p&gt;

&lt;p&gt;Silent refresh solves this. The idea is straightforward: before the access token expires, use the refresh token to get a new one automatically, without any visible interruption to the user.&lt;/p&gt;

&lt;p&gt;Here's the basic pattern:&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;let&lt;/span&gt; &lt;span class="nx"&gt;refreshTimeout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;scheduleTokenRefresh&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;expiresIn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Refresh 60 seconds before expiry&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;refreshIn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;expiresIn&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;
  &lt;span class="nx"&gt;refreshTimeout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &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="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;newToken&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;refreshAccessToken&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
      &lt;span class="nf"&gt;setToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newToken&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;accessToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="nf"&gt;scheduleTokenRefresh&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newToken&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;expiresIn&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="c1"&gt;// Refresh failed, session has expired&lt;/span&gt;
      &lt;span class="nf"&gt;handleSessionExpiry&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;refreshIn&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;A few things worth noting here:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The refresh token should be stored in an httpOnly cookie.&lt;/strong&gt; It's longer-lived and more sensitive than the access token. You don't want JavaScript touching it directly. The server reads it from the cookie and issues a new access token.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Handle the case where the tab has been in the background.&lt;/strong&gt; Browsers throttle timers in inactive tabs. When the user comes back to the tab, check if the token has already expired and refresh immediately if so.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Guard against multiple simultaneous refresh calls.&lt;/strong&gt; On platforms with multiple concurrent API requests, several calls can fail at the same time when a token expires. Without a guard, you'll fire multiple refresh requests simultaneously.&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;let&lt;/span&gt; &lt;span class="nx"&gt;refreshPromise&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&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;refreshAccessToken&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;refreshPromise&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="nx"&gt;refreshPromise&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;refreshPromise&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/auth/refresh&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;refreshPromise&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&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;refreshPromise&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This ensures only one refresh request goes out regardless of how many callers triggered it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Session Expiry -  Handle It Like It's Part of the Product
&lt;/h2&gt;

&lt;p&gt;Session expiry is inevitable. Refresh tokens expire. Users get logged out on other devices. Admins revoke access. Your job is to handle it in a way that doesn't feel broken.&lt;/p&gt;

&lt;p&gt;The worst experience is a silent failure - the user clicks something, nothing happens, no explanation. Or worse, they get a raw 401 error in the UI.&lt;/p&gt;

&lt;p&gt;A better approach has three parts:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Intercept 401 responses globally.&lt;/strong&gt; Don't handle auth errors individually in every API call. Set up a single interceptor that catches 401s, attempts a token refresh, and retries the original request. If the refresh fails, then you handle expiry.&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;apiFetch&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="p"&gt;)&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;response&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="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;getToken&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&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;===&lt;/span&gt; &lt;span class="mi"&gt;401&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;newToken&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;refreshAccessToken&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
      &lt;span class="nf"&gt;setToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newToken&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;accessToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="c1"&gt;// Retry original request with new token&lt;/span&gt;
      &lt;span class="nx"&gt;response&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="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;newToken&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;accessToken&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;handleSessionExpiry&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="nx"&gt;response&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Tell the user what happened.&lt;/strong&gt; When the session genuinely expires, show a clear message. "Your session has expired, please log in again." Not a blank screen, not a generic error. A real explanation with a clear action.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Preserve their intent.&lt;/strong&gt; Store the URL they were trying to access before redirecting to login. After they authenticate, bring them back to where they were. On a content platform this matters - a user who was about to watch something should land back on that content, not the homepage.&lt;/p&gt;




&lt;h2&gt;
  
  
  OAuth and Social Login - What Actually Happens in the Browser
&lt;/h2&gt;

&lt;p&gt;Most developers use a library for OAuth and treat it as a black box. That's fine until something breaks and then you need to understand what's actually happening.&lt;/p&gt;

&lt;p&gt;Here's the simplified flow for a third-party login:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;User clicks "Login with Google."&lt;/li&gt;
&lt;li&gt;Your app redirects the browser to Google's authorization endpoint with your client ID, requested scopes and a redirect URI.&lt;/li&gt;
&lt;li&gt;Google authenticates the user and redirects back to your redirect URI with an authorization code.&lt;/li&gt;
&lt;li&gt;Your frontend sends that code to your backend.&lt;/li&gt;
&lt;li&gt;Your backend exchanges the code for tokens directly with Google using your client secret.&lt;/li&gt;
&lt;li&gt;Your backend issues its own session token to your frontend.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A few things to pay attention to here:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Never exchange the authorization code on the frontend.&lt;/strong&gt; The code exchange requires your client secret. That should never be in frontend code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use the state parameter.&lt;/strong&gt; This is a random value you generate before the redirect and verify when the user comes back. It protects against CSRF attacks on the OAuth flow.&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;initiateOAuthLogin&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;state&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="nx"&gt;sessionStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;oauth_state&lt;/span&gt;&lt;span class="dl"&gt;'&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URLSearchParams&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;client_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CLIENT_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;redirect_uri&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;REDIRECT_URI&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;response_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;code&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;openid email profile&lt;/span&gt;&lt;span class="dl"&gt;'&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="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`https://accounts.google.com/o/oauth2/auth?&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="s2"&gt;`&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handleOAuthCallback&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;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URLSearchParams&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;search&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;returnedState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;state&lt;/span&gt;&lt;span class="dl"&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;savedState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;sessionStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;oauth_state&lt;/span&gt;&lt;span class="dl"&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;returnedState&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;savedState&lt;/span&gt;&lt;span class="p"&gt;)&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="s1"&gt;OAuth state mismatch. Possible CSRF attack.&lt;/span&gt;&lt;span class="dl"&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;// Safe to proceed&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Use PKCE for public clients.&lt;/strong&gt; If you're doing the token exchange from a mobile app or a pure SPA without a backend, PKCE (Proof Key for Code Exchange) replaces the client secret with a cryptographic challenge. It's the current best practice for frontend-only OAuth flows.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Security Decisions Most Developers Get Wrong
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Storing sensitive tokens in localStorage.&lt;/strong&gt; Covered above, but worth repeating. The convenience is not worth the XSS exposure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Not setting token expiry short enough.&lt;/strong&gt; Access tokens should be short-lived. If yours are valid for 24 hours, a stolen token gives an attacker a 24-hour window. Keep them short and lean on silent refresh.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trusting the frontend for authorization.&lt;/strong&gt; Hiding a button in the UI is not authorization. The API must enforce access control. Frontend checks are for user experience only, never for security.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Not handling token refresh race conditions.&lt;/strong&gt; Already covered with the single refresh promise pattern. This one causes subtle bugs that are hard to reproduce and harder to debug in production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Logging out only on the current device.&lt;/strong&gt; When a user logs out or changes their password, invalidate their refresh tokens on the server. Otherwise they're still effectively logged in on every other device they've used.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sending tokens in URL parameters.&lt;/strong&gt; Tokens in URLs end up in browser history, server logs, and referrer headers. Always send them in the Authorization header or a cookie. Never in the URL.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Authentication on the frontend is not just a login form and a token in localStorage. It's a set of decisions that affect the security, reliability, and user experience of your entire platform.&lt;/p&gt;

&lt;p&gt;Get the storage right. Build silent refresh properly. Handle expiry gracefully. Understand the OAuth flow well enough to debug it when it breaks.&lt;/p&gt;

&lt;p&gt;The developers who handle auth well aren't the ones who memorized every OAuth RFC. They're the ones who understood the tradeoffs and made deliberate decisions at each step.&lt;/p&gt;

&lt;p&gt;At scale, deliberate decisions are the only kind that hold up.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Have thoughts or questions on frontend authentication? Drop them in the comments, always happy to discuss.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This article is part of the &lt;a href="https://dev.to/yogeshyadav/series/38132"&gt;&lt;strong&gt;Frontend at Scale&lt;/strong&gt;&lt;/a&gt; series.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>frontend</category>
      <category>authentication</category>
      <category>react</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Frontend Performance That Actually Moves the Needle</title>
      <dc:creator>Yogesh Yadav</dc:creator>
      <pubDate>Thu, 16 Apr 2026 20:01:05 +0000</pubDate>
      <link>https://forem.com/codescoop/frontend-performance-that-actually-moves-the-needle-41b7</link>
      <guid>https://forem.com/codescoop/frontend-performance-that-actually-moves-the-needle-41b7</guid>
      <description>&lt;p&gt;In this article we'll cover why Lighthouse scores alone don't tell the full story, what metrics actually matter at scale, how real user monitoring changes the way you think about performance, and the optimizations that have the most impact on platforms serving millions of users.&lt;/p&gt;

&lt;p&gt;Lighthouse is a great tool. I'm not here to tell you to ignore it. But I've seen teams chase a perfect Lighthouse score while their real users were experiencing 4-second load times on mid-range android devices with a 4G connection.&lt;/p&gt;

&lt;p&gt;The score looked great. The experience wasn't.&lt;/p&gt;

&lt;p&gt;When you're building for 10 million users, performance stops being about a number in a report. It becomes about real people on real devices with real network conditions. And the gap between a lab score and what your users actually feel is wider than most developers realize.&lt;/p&gt;

&lt;p&gt;This article is about closing that gap.&lt;/p&gt;




&lt;h2&gt;
  
  
  Lighthouse Scores Are Lab Data, Not Reality
&lt;/h2&gt;

&lt;p&gt;Lighthouse runs in a controlled environment. Throttled CPU, simulated network, a clean browser with no extensions, no cached data, no background tabs. That's not how your users browse.&lt;/p&gt;

&lt;p&gt;Your users are on a 3-year-old phone with 15 browser tabs open, on a train with patchy network, while your JavaScript is fighting with a background app for CPU time.&lt;/p&gt;

&lt;p&gt;This is why a 90+ Lighthouse score can still result in a poor user experience. The lab doesn't lie, but it only tells you part of the truth.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lab data:&lt;/strong&gt; what Lighthouse gives you - is useful for catching regressions and tracking trends over time. But it should never be your only signal.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Field data:&lt;/strong&gt; what your real users experience - is where performance work actually pays off.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Metrics That Actually Matter at Scale
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Core Web Vitals
&lt;/h3&gt;

&lt;p&gt;Google's Core Web Vitals are the closest thing we have to a standardized set of user-centric performance metrics. Three of them matter most:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;LCP - Largest Contentful Paint:&lt;/strong&gt; How long does it take for the largest visible element to render? For most platforms this is a hero image, a video thumbnail or a headline. This is what the user perceives as "the page loaded."&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Target: under 2.5 seconds.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;INP - Interaction to Next Paint:&lt;/strong&gt; How quickly does the page respond after a user interaction? Click a button, tap a menu, submit a form - how long before the page visually responds? This replaced FID (First Input Delay) in 2024 and is a much better measure of real interactivity.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Target: under 200 milliseconds.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CLS - Cumulative Layout Shift&lt;/strong&gt; How much does the page jump around while loading? Ads loading late, images without dimensions, fonts swapping - these all contribute to CLS. On a content-heavy platform this can quietly destroy the user experience.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Target: under 0.1.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;These three metrics directly impact SEO ranking and user retention. At scale, a 0.1 improvement in CLS or a 500ms reduction in LCP translates to measurable engagement and conversion improvements.&lt;/p&gt;

&lt;h3&gt;
  
  
  TTFB - Time to First Byte
&lt;/h3&gt;

&lt;p&gt;Before the browser can render anything, it needs a response from the server. TTFB measures that wait time. High TTFB usually points to server-side issues - slow API responses, no CDN or unoptimized server rendering.&lt;/p&gt;

&lt;p&gt;On platforms with a global audience, CDN configuration alone can cut TTFB from 800ms to under 100ms for a significant portion of your users.&lt;/p&gt;

&lt;h3&gt;
  
  
  TTI - Time to Interactive
&lt;/h3&gt;

&lt;p&gt;When can the user actually use the page? Not just see it, but interact with it without the UI freezing. This is where JavaScript bundle size and execution time have the most direct impact.&lt;/p&gt;

&lt;p&gt;A page that looks loaded but isn't responding to clicks is one of the most frustrating experiences a user can have.&lt;/p&gt;




&lt;h2&gt;
  
  
  Real User Monitoring - The Signal You Can't Ignore
&lt;/h2&gt;

&lt;p&gt;If you're only running Lighthouse, you're flying partially blind. Real User Monitoring (RUM) captures performance data from actual user sessions and sends it back to you.&lt;/p&gt;

&lt;p&gt;The difference is significant. With RUM you can see:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How performance varies by device type, browser and geography&lt;/li&gt;
&lt;li&gt;Which pages have the worst real-world LCP or INP&lt;/li&gt;
&lt;li&gt;How performance degrades over time as your codebase grows&lt;/li&gt;
&lt;li&gt;What percentage of your users are experiencing poor performance right now&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Tools like Datadog RUM, SpeedCurve, Mux Data or even the free Chrome User Experience Report (CrUX) give you this visibility.&lt;/p&gt;

&lt;p&gt;On a platform serving millions of users, even if 5% of your users are experiencing poor performance, that's 500,000 people having a bad time. RUM makes that visible. Lighthouse doesn't.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Optimizations That Actually Move the Needle
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. JavaScript Bundle Size
&lt;/h3&gt;

&lt;p&gt;This is almost always the biggest lever. JavaScript is the most expensive resource on the web - it has to be downloaded, parsed, and executed before it does anything useful.&lt;/p&gt;

&lt;p&gt;Code splitting is non-negotiable at scale. Every route should load only the JavaScript it needs.&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="c1"&gt;// Instead of importing everything upfront&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;HeavyComponent&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./HeavyComponent&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="c1"&gt;// Load it only when needed&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;HeavyComponent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lazy&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./HeavyComponent&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Audit your bundle regularly. Tools like &lt;code&gt;webpack-bundle-analyzer&lt;/code&gt; or &lt;code&gt;vite-bundle-visualizer&lt;/code&gt; will show you exactly what's in your bundle and where the weight is coming from. You will almost always find something surprising.&lt;/p&gt;

&lt;p&gt;Third-party scripts are usually the worst offenders. Analytics, chat widgets, ad scripts - these are often loaded synchronously and block rendering. Load them async or defer them entirely until after the page is interactive.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Image Optimization
&lt;/h3&gt;

&lt;p&gt;Images are the largest assets on most pages. Getting this wrong has a direct impact on LCP.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use modern formats. WebP is widely supported and significantly smaller than JPEG or PNG. AVIF is even better where supported.&lt;/li&gt;
&lt;li&gt;Always set explicit width and height on images. This prevents layout shift and helps the browser allocate space before the image loads.&lt;/li&gt;
&lt;li&gt;Use lazy loading for images below the fold.&lt;/li&gt;
&lt;li&gt;Serve appropriately sized images. Don't serve a 2000px wide image to a 400px wide mobile screen.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt;
  &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"thumbnail.webp"&lt;/span&gt;
  &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"400"&lt;/span&gt;
  &lt;span class="na"&gt;height=&lt;/span&gt;&lt;span class="s"&gt;"225"&lt;/span&gt;
  &lt;span class="na"&gt;loading=&lt;/span&gt;&lt;span class="s"&gt;"lazy"&lt;/span&gt;
  &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;"Video thumbnail"&lt;/span&gt;
&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a platform with a large content library, image optimization alone can reduce page weight by 40–60%.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Critical Rendering Path
&lt;/h3&gt;

&lt;p&gt;The browser has to download your HTML, parse it, discover CSS and JavaScript, download those, parse them and then render the page. Every step in that chain is an opportunity to either speed things up or slow things down.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Inline critical CSS - the styles needed to render above-the-fold content - directly in the HTML. This eliminates a render-blocking network request for the initial view.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Preload key resources the browser won't discover until late in the parsing process.&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"preload"&lt;/span&gt; &lt;span class="na"&gt;as=&lt;/span&gt;&lt;span class="s"&gt;"font"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/fonts/main.woff2"&lt;/span&gt; &lt;span class="na"&gt;crossorigin&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"preload"&lt;/span&gt; &lt;span class="na"&gt;as=&lt;/span&gt;&lt;span class="s"&gt;"image"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/hero.webp"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Defer non-critical JavaScript. If a script doesn't need to run before the page is interactive, it shouldn't block rendering.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4. Caching Strategy
&lt;/h3&gt;

&lt;p&gt;I covered this in depth in the previous article in this series, but it's worth mentioning here because caching is one of the highest-impact performance optimizations available to you.&lt;/p&gt;

&lt;p&gt;Repeat visitors on a well-cached platform can load pages almost entirely from cache. No network requests for static assets, no server round trips for unchanged resources. The performance improvement for returning users is dramatic.&lt;/p&gt;

&lt;p&gt;If you haven't read the &lt;a href="https://dev.to/codescoop/frontend-caching-done-right-2lem"&gt;caching article&lt;/a&gt; yet, it's worth going back to.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Reducing Main Thread Work
&lt;/h3&gt;

&lt;p&gt;INP and TTI both suffer when the main thread is busy. JavaScript execution, long tasks, layout recalculations - these all compete for the same thread that handles user interactions.&lt;/p&gt;

&lt;p&gt;A few things that help:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Break up long tasks. Any task that takes more than 50ms can cause noticeable jank. Use setTimeout or scheduler.postTask to yield control back to the browser between chunks of work.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Avoid layout thrashing. Reading and writing to the DOM in alternating calls forces the browser to recalculate layout repeatedly. Batch your reads and writes.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Move heavy computation off the main thread with Web Workers.&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Yielding to the browser between heavy tasks&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;processLargeDataset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;)&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;i&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;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&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="nf"&gt;process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&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;i&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;100&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="p"&gt;{&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;resolve&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;resolve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&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;h2&gt;
  
  
  Performance Budgets - Making It Stick
&lt;/h2&gt;

&lt;p&gt;One of the hardest parts of performance work at scale is keeping improvements from regressing over time. A performance budget solves this.&lt;/p&gt;

&lt;p&gt;A performance budget sets explicit limits on metrics like bundle size, LCP or TTI. If a pull request would push you over the budget, it fails the build.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"resourceSizes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"resourceType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"script"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"budget"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"resourceType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"total"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"budget"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"timings"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"metric"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"first-contentful-paint"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"budget"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1500&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"metric"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"interactive"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"budget"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3500&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This keeps performance on everyone's radar, not just the engineer who cared enough to optimize it once.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Scale Actually Teaches You About Performance
&lt;/h2&gt;

&lt;p&gt;Here's what I've learned from working on platforms at this size that you don't find in most performance guides:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Device distribution matters more than you think.&lt;/strong&gt; Your development machine is not representative of your users. Profile on a mid-range Android device and you will find issues you never knew existed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Geography matters.&lt;/strong&gt; A platform with a global audience needs a CDN strategy, not just a fast server in one region. Network latency from a distant origin server can add seconds to TTFB for users in certain regions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Performance degrades gradually.&lt;/strong&gt; Nobody ships a slow app intentionally. It gets slow one dependency, one feature, one third-party script at a time. Without a budget and regular monitoring, you won't notice until users are already complaining.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The 80/20 rule applies.&lt;/strong&gt; A small number of pages usually account for the majority of your traffic. Find those pages, measure them obsessively and optimize them first. That's where your performance work will have the most impact.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Lighthouse is a tool, not a goal. A green score means you've done the basics right. It doesn't mean your users are having a fast experience.&lt;/p&gt;

&lt;p&gt;The teams that get performance right at scale are the ones who measure what their real users experience, set budgets to prevent regression and focus their effort on the optimizations that actually move the needle for their specific platform and audience.&lt;/p&gt;

&lt;p&gt;Start with RUM. Find where your real users are struggling. Fix those things first.&lt;/p&gt;

&lt;p&gt;The Lighthouse score will follow.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Have thoughts or questions on frontend performance? Drop them in the comments, always happy to discuss.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This article is part of the &lt;a href="https://dev.to/yogeshyadav/series/38132"&gt;&lt;strong&gt;Frontend at Scale&lt;/strong&gt;&lt;/a&gt; series.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>frontend</category>
      <category>performance</category>
      <category>react</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Frontend Caching Done Right</title>
      <dc:creator>Yogesh Yadav</dc:creator>
      <pubDate>Sun, 05 Apr 2026 06:39:19 +0000</pubDate>
      <link>https://forem.com/codescoop/frontend-caching-done-right-2lem</link>
      <guid>https://forem.com/codescoop/frontend-caching-done-right-2lem</guid>
      <description>&lt;p&gt;In this article we’ll cover how the browser cache and HTTP headers work, when and how to use stale-while-revalidate, how service workers give you programmatic control over caching, what you should never cache, and how cache invalidation works in practice. All of it from the perspective of building for platforms that can’t afford to get this wrong.&lt;/p&gt;

&lt;p&gt;Caching is one of those topics that every frontend developer thinks they understand, until they’re staring at a production issue where users are getting stale data, or worse, the server is getting hammered because nothing is being cached at all.&lt;/p&gt;

&lt;p&gt;I’ve worked on platforms handling more than 10 million active users. And I can tell you, caching stops being a “nice to have” the moment your scale starts growing. It becomes the difference between a platform that feels fast and one that quietly falls apart under load.&lt;/p&gt;

&lt;p&gt;This article is everything I’ve learned about frontend caching, written the way I wish someone had explained it to me early in my career.&lt;/p&gt;




&lt;h2&gt;
  
  
  First, Understand What You Are Actually Caching
&lt;/h2&gt;

&lt;p&gt;Before you touch a single HTTP header or write a line of service worker code, ask yourself one question.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is the cost of serving stale data here?
&lt;/h3&gt;

&lt;p&gt;That question determines everything. Because caching is always a tradeoff between freshness and performance. The mistake most developers make is treating all resources the same way. They’re not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Here’s how I think about it:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Static assets&lt;/strong&gt; (JS bundles, CSS, fonts, images) - these can be cached aggressively, sometimes forever, if you version them correctly.&lt;br&gt;
&lt;strong&gt;API responses&lt;/strong&gt; - depends entirely on how often the data changes and how much it matters if the user sees something slightly outdated.&lt;br&gt;
&lt;strong&gt;HTML documents&lt;/strong&gt; - usually should not be cached aggressively, especially for authenticated apps.&lt;br&gt;
&lt;strong&gt;User-specific data&lt;/strong&gt; - almost never cache this without thinking carefully.&lt;/p&gt;

&lt;p&gt;Get this mental model right first. Everything else follows from it.&lt;/p&gt;


&lt;h2&gt;
  
  
  Browser Cache and HTTP Headers
&lt;/h2&gt;

&lt;p&gt;The browser cache is your first and most powerful caching layer. It lives between the user and your server, and it’s controlled entirely through HTTP response headers.&lt;/p&gt;
&lt;h3&gt;
  
  
  Cache-Control
&lt;/h3&gt;

&lt;p&gt;This is the header you’ll use the most. Here’s what the key directives actually mean:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;Cache-Control: max-age=31536000, immutablety
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;max-age&lt;/code&gt; tells the browser how many seconds to keep this resource before considering it stale. &lt;code&gt;immutable&lt;/code&gt; tells the browser not to bother revalidating it even on a hard refresh, because the content will never change.&lt;/p&gt;

&lt;p&gt;Use this combination for versioned static assets, your JS bundles, CSS files, and images that have a content hash in the filename. Something like &lt;code&gt;main.a3f9c2.js&lt;/code&gt;. The hash changes every build, so the URL changes, so you never serve stale code. Cache it forever.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;Cache-Control: no-cache
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Despite the name, this does not mean “don’t cache.” It means “cache it, but check with the server every time before using it.” The server can respond with a &lt;code&gt;304 Not Modified&lt;/code&gt; and the browser uses the cached version. No full download needed.&lt;/p&gt;

&lt;p&gt;Use this for your HTML documents. You want the browser to always check if there’s a new version, but still benefit from caching when nothing has changed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;Cache-Control: no-store
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This actually means “don’t cache.” Nothing is stored anywhere. Use this for sensitive data, authentication responses, anything you never want sitting in a cache.&lt;/p&gt;

&lt;h3&gt;
  
  
  ETag and Last-Modified
&lt;/h3&gt;

&lt;p&gt;These work alongside &lt;code&gt;Cache-Control&lt;/code&gt; for revalidation. When the browser asks "has this changed?", the server uses these to answer.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;ETag: "abc123"
Last-Modified: Mon, 01 Jan 2024 00:00:00 GMT
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The browser sends back &lt;code&gt;If-None-Match: "abc123"&lt;/code&gt; or &lt;code&gt;If-Modified-Since&lt;/code&gt; on the next request. If nothing changed, the server returns 304 and saves the bandwidth of sending the full response again.&lt;/p&gt;

&lt;p&gt;At scale, these small savings add up to a significant reduction in server load.&lt;/p&gt;




&lt;h2&gt;
  
  
  Stale-While-Revalidate
&lt;/h2&gt;

&lt;p&gt;This is one of the most underused caching strategies I’ve seen in frontend codebases, and it’s genuinely powerful.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;Cache-Control: max-age=60, stale-while-revalidate=300
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here’s what this does. For the first 60 seconds, serve from cache, no questions asked. Between 60 and 300 seconds, serve the stale cached version immediately, but kick off a background request to revalidate it. After 300 seconds, it’s stale and must be revalidated before serving.&lt;/p&gt;

&lt;p&gt;The user gets an instant response. The cache gets updated in the background. No loading spinner, no waiting.&lt;/p&gt;

&lt;p&gt;This pattern is perfect for data that changes occasionally but doesn’t need to be real-time. Think navigation menus, configuration data, content that updates a few times a day. The user always gets a fast experience and the data stays reasonably fresh.&lt;/p&gt;

&lt;p&gt;You’ll also recognize this pattern from TanStack Query’s &lt;code&gt;staleTime&lt;/code&gt; and &lt;code&gt;gcTime&lt;/code&gt; configuration. The underlying idea is exactly the same, just applied at the JavaScript layer instead of the HTTP layer.&lt;/p&gt;




&lt;h2&gt;
  
  
  Service Workers - The Programmable Cache
&lt;/h2&gt;

&lt;p&gt;HTTP headers give you declarative control over caching. Service workers give you programmatic control. That’s a significant difference.&lt;/p&gt;

&lt;p&gt;A service worker sits between your app and the network, intercepting every request. You decide what happens with each one.&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="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fetch&lt;/span&gt;&lt;span class="dl"&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;event&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;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;respondWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;caches&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;cachedResponse&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cachedResponse&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="nx"&gt;cachedResponse&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;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;request&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;This is a simple cache-first strategy. Check the cache first, fall back to the network if nothing is found. For a platform serving millions of users, this means repeat visitors often never hit your server for static assets at all.&lt;/p&gt;

&lt;h3&gt;
  
  
  Caching Strategies with Service Workers
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Cache First&lt;/strong&gt; - Serve from cache, fall back to network. Best for static assets that rarely change.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Network First&lt;/strong&gt; - Try the network, fall back to cache if offline. Best for API data where freshness matters but offline support is needed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stale While Revalidate&lt;/strong&gt; - Serve from cache immediately, update cache in background. Best for non-critical content where speed matters more than freshness.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cache Only&lt;/strong&gt; - Only serve from cache. Useful for assets you’ve pre-cached during install.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Network Only&lt;/strong&gt; - Always go to network. For requests that should never be cached, like analytics or payment endpoints.&lt;/p&gt;

&lt;p&gt;Pick your strategy per resource type, not globally. A single strategy for everything is almost always the wrong call.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pre-caching vs Runtime Caching
&lt;/h3&gt;

&lt;p&gt;Pre-caching happens when the service worker installs. You explicitly list assets to cache upfront.&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="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;install&lt;/span&gt;&lt;span class="dl"&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;event&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;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitUntil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;caches&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;v1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;cache&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addAll&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/styles/main.css&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/scripts/main.js&lt;/span&gt;&lt;span class="dl"&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;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;Runtime caching happens dynamically as requests come in. You cache responses as they’re fetched, so frequently accessed resources end up in cache naturally over time.&lt;/p&gt;

&lt;p&gt;For most platforms, you want both. Pre-cache your critical shell, runtime cache everything else.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Not to Cache
&lt;/h2&gt;

&lt;p&gt;This is the part that trips people up. Caching the wrong things causes bugs that are genuinely hard to debug in production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Never aggressively cache:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Authentication tokens or session data&lt;/li&gt;
&lt;li&gt;Payment and transaction endpoints&lt;/li&gt;
&lt;li&gt;User-specific personalization data&lt;/li&gt;
&lt;li&gt;Anything that changes per user or per session&lt;/li&gt;
&lt;li&gt;A/B test configurations if they need to be real-time&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I’ve seen teams cache API responses that included user-specific entitlements. The result was users seeing content they shouldn’t have access to, or not seeing content they’d just purchased. At scale, that’s not just a bug. It’s a trust problem.&lt;/p&gt;

&lt;p&gt;When in doubt, don’t cache it, or use &lt;code&gt;no-cache&lt;/code&gt; so at least revalidation happens.&lt;/p&gt;




&lt;h2&gt;
  
  
  Cache Invalidation - The Hard Part
&lt;/h2&gt;

&lt;p&gt;There’s a famous saying in computer science: there are only two hard things, naming things and cache invalidation.&lt;/p&gt;

&lt;p&gt;It’s funny because it’s true.&lt;/p&gt;

&lt;p&gt;For static assets, content-hashed filenames solve this completely. New deploy, new hash, new URL, new cache entry. Old one expires naturally.&lt;/p&gt;

&lt;p&gt;For API responses and service worker caches, you need a versioning strategy.&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;const&lt;/span&gt; &lt;span class="nx"&gt;CACHE_VERSION&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;v2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;activate&lt;/span&gt;&lt;span class="dl"&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;event&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;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitUntil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;caches&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;cacheNames&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="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nx"&gt;cacheNames&lt;/span&gt;
          &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;CACHE_VERSION&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;caches&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;delete&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="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;Every time you deploy, bump the cache version. The activate event cleans up old caches. Users get fresh data on their next visit without you having to manually purge anything.&lt;/p&gt;




&lt;h2&gt;
  
  
  Caching at Scale - What Actually Changes
&lt;/h2&gt;

&lt;p&gt;When you’re building for 10 million users, the fundamentals don’t change. But the consequences of getting it wrong are amplified significantly.&lt;/p&gt;

&lt;p&gt;A few things I’ve learned from operating at that scale:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Measure before you optimize.&lt;/strong&gt; Use Chrome DevTools, Lighthouse, and your RUM (Real User Monitoring) data to understand where your actual cache hit rates are. Don’t guess.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CDN caching and browser caching are different layers.&lt;/strong&gt; Your CDN has its own cache headers, often separate from what the browser sees. Understand both. Misconfiguring your CDN can mean millions of users bypassing the browser cache entirely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cache stampedes are real.&lt;/strong&gt; When a popular cached resource expires simultaneously for millions of users, they all hit your server at once. Stale-while-revalidate and jittered expiry times help prevent this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Monitor your cache hit ratio.&lt;/strong&gt; If it’s low, you’re leaving performance on the table. If it’s too high and you’re seeing stale data complaints, your TTLs are too aggressive.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Caching is not a set-it-and-forget-it feature. It’s an ongoing engineering decision that touches performance, correctness and user experience all at once.&lt;/p&gt;

&lt;p&gt;Start with HTTP headers and get those right. Layer in stale-while-revalidate for the right resources. Add service workers when you need offline support or more granular control. And always think about invalidation before you think about caching.&lt;/p&gt;

&lt;p&gt;The developers who get this right aren’t the ones who know the most cache directives. They’re the ones who ask the right question first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is the cost of serving stale data here?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Answer that honestly for every resource, and the rest follows.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Have thoughts or questions on frontend caching? Drop them in the comments, always happy to discuss.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This article is part of the &lt;a href="https://dev.to/yogeshyadav/series/38132"&gt;&lt;strong&gt;Frontend at Scale&lt;/strong&gt;&lt;/a&gt; series.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>frontend</category>
      <category>caching</category>
      <category>javascript</category>
      <category>performance</category>
    </item>
    <item>
      <title>Explore the lexical Environment &amp; Environment Record in Javascript 2021</title>
      <dc:creator>Yogesh Yadav</dc:creator>
      <pubDate>Wed, 17 Aug 2022 07:23:00 +0000</pubDate>
      <link>https://forem.com/codescoop/what-has-changed-in-lexical-environment-as-per-ecmascript-2021-2bjb</link>
      <guid>https://forem.com/codescoop/what-has-changed-in-lexical-environment-as-per-ecmascript-2021-2bjb</guid>
      <description>&lt;p&gt;Let's first understand the &lt;code&gt;Lexical Environment&lt;/code&gt; &amp;amp; &lt;code&gt;Environment Record&lt;/code&gt; as per different versions of ECMAScript Specification.&lt;/p&gt;

&lt;p&gt;From &lt;a href="https://262.ecma-international.org/6.0/" rel="noopener noreferrer"&gt;&lt;strong&gt;ES2015&lt;/strong&gt;&lt;/a&gt; till &lt;a href="https://262.ecma-international.org/11.0/" rel="noopener noreferrer"&gt;&lt;strong&gt;ES2020&lt;/strong&gt;&lt;/a&gt; Specification:-&lt;/p&gt;

&lt;h2&gt;Lexical Environment: &lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A lexical environment is a &lt;strong&gt;specification type&lt;/strong&gt; used to define the association of Identifiers to specific variables and functions, based upon the lexical nesting structure of your code.&lt;/li&gt;
&lt;li&gt;A lexical environment consists of two components:

&lt;ol&gt;
&lt;li&gt;
&lt;h4&gt;Environment Record&lt;/h4&gt; It records the &lt;strong&gt;identifier bindings&lt;/strong&gt; that are created within the scope of its associated Lexical Environment. It is referred to as the Lexical Environment's EnvironmentRecord.&lt;/li&gt;
&lt;li&gt;
&lt;h4&gt;Outer Reference&lt;/h4&gt; A reference to outer environment (null in the global environment).&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A conceptual view using pseudo-code:&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="nx"&gt;executioncontext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;environment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;environmentRecord&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; 
    &lt;span class="c1"&gt;// storage&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;identifier&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;identifier&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c1"&gt;// reference to the parent environment&lt;/span&gt;
    &lt;span class="nl"&gt;outer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; - The &lt;code&gt;[[Environment]]&lt;/code&gt; created inside Execution Context is of &lt;code&gt;type&lt;/code&gt; &lt;strong&gt;Lexical Environment&lt;/strong&gt; &lt;br&gt;
 &lt;a href="https://262.ecma-international.org/11.0/#sec-ecmascript-function-objects" rel="noopener noreferrer"&gt;[refer ES2020]&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Foeq4770whr2mksmrwt0s.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Foeq4770whr2mksmrwt0s.png" alt="Lexical Environment in Global Scope" width="800" height="418"&gt;&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;According to &lt;a href="https://262.ecma-international.org/12.0/" rel="noopener noreferrer"&gt;&lt;strong&gt;12th Edition ECMAScript2021&lt;/strong&gt;&lt;/a&gt; Specification:&lt;/p&gt;

&lt;h2&gt;Environment Record &lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A Environment Record is a &lt;strong&gt;specification type&lt;/strong&gt; used to define the association of Identifiers to specific variables and functions, based upon the lexical nesting structure of your code.&lt;/li&gt;
&lt;li&gt;Every Environment Record has one component:

&lt;ol&gt;
&lt;li&gt;
&lt;h4&gt;Outer Reference&lt;/h4&gt; An &lt;code&gt;[[OuterEnv]]&lt;/code&gt; field, which is either null or a reference to an outer Environment Record.
A conceptual view using pseudo-code:
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;executioncontext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;environment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// storage&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;identifier&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;identifier&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;// reference to the parent environment&lt;/span&gt;
    &lt;span class="na"&gt;outer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; - The &lt;code&gt;[[Environment]]&lt;/code&gt; created inside Execution Context is of &lt;code&gt;type&lt;/code&gt; &lt;strong&gt;Environment Record&lt;/strong&gt; &lt;a href="https://262.ecma-international.org/12.0/#sec-ecmascript-function-objects" rel="noopener noreferrer"&gt;[refer ES2021]&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F06g8v67qzlgcds5i22fk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F06g8v67qzlgcds5i22fk.png" alt="Environment Record in Global Scope" width="800" height="399"&gt;&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;Let's also understand the &lt;code&gt;Structure of execution context&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt; Execution Context: &lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;An execution context is a specification device that is used to track the runtime evaluation of the code.&lt;/li&gt;
&lt;li&gt;To keeps the track of execution progress of its associated code, it needs various &lt;strong&gt;state components&lt;/strong&gt; like &lt;code&gt;LexicalEnvironment&lt;/code&gt;, &lt;code&gt;VariableEnvironment&lt;/code&gt;, etc.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In pseudo-code:&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="nx"&gt;ExecutionContext&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;VariableEnvironment&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;span class="na"&gt;LexicalEnvironment&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;span class="c1"&gt;// other components&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Till ES2020&lt;/th&gt;
&lt;th&gt;From ES2021&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;- The &lt;code&gt;LexicalEnvironment component&lt;/code&gt; and &lt;code&gt;VariableEnvironment component&lt;/code&gt; of an execution context are always &lt;strong&gt;Lexical Environments&lt;/strong&gt; &lt;a href="https://262.ecma-international.org/11.0/#table-23" rel="noopener noreferrer"&gt;[refer ES2020]&lt;/a&gt;
&lt;/td&gt;
&lt;td&gt;- The &lt;code&gt;LexicalEnvironment component&lt;/code&gt; and &lt;code&gt;VariableEnvironment&lt;/code&gt; components of an execution context are always &lt;strong&gt;Environment Records&lt;/strong&gt; &lt;a href="https://262.ecma-international.org/12.0/#table-23" rel="noopener noreferrer"&gt;[refer ES2021]&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt; Summary &lt;/h2&gt;

&lt;p&gt;Let's have a quick recap of all the steps we perform in the above code snippet.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;In ECMAScript2021, the &lt;code&gt;[[environment]]&lt;/code&gt; which is created inside the execution context is of &lt;code&gt;type&lt;/code&gt; &lt;strong&gt;Environment Record&lt;/strong&gt; instead of Lexical Environment.&lt;/li&gt;
&lt;li&gt;So, The &lt;code&gt;LexicalEnvironment component&lt;/code&gt; and &lt;code&gt;VariableEnvironment components&lt;/code&gt; of an execution context are always &lt;strong&gt;Environment Records&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;Wrap Up!!&lt;/h2&gt;

&lt;p&gt;Thank you for your time!! Let's connect to learn and grow together.&lt;br&gt;
&lt;a href="https://github.com/deltanode" rel="noopener noreferrer"&gt;Github&lt;/a&gt; &lt;a href="https://twitter.com/yogesh_yadv" rel="noopener noreferrer"&gt;Twitter&lt;/a&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>computerscience</category>
    </item>
  </channel>
</rss>
