<?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: SHANKI BANSAL</title>
    <description>The latest articles on DEV Community by SHANKI BANSAL (@shanki_bansal).</description>
    <link>https://dev.to/shanki_bansal</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%2F3993002%2F6e019aaa-2f9e-4b65-8dae-394a272810e5.jpg</url>
      <title>DEV Community: SHANKI BANSAL</title>
      <link>https://dev.to/shanki_bansal</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/shanki_bansal"/>
    <language>en</language>
    <item>
      <title>Browser Caching Broke My Compose WASM Deployment — Here's the nginx Fix</title>
      <dc:creator>SHANKI BANSAL</dc:creator>
      <pubDate>Fri, 19 Jun 2026 18:47:26 +0000</pubDate>
      <link>https://dev.to/shanki_bansal/browser-caching-broke-my-compose-wasm-deployment-heres-the-nginx-fix-2acf</link>
      <guid>https://dev.to/shanki_bansal/browser-caching-broke-my-compose-wasm-deployment-heres-the-nginx-fix-2acf</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;You deployed. The server has the new files. But users are still seeing the old UI. Here's exactly why — and the nginx fix that solves it permanently.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You push new code. You deploy. You check the live URL — and see the old UI.&lt;br&gt;
You force-refresh. Now it's fine. But your users don't know what force-refresh means.&lt;/p&gt;

&lt;p&gt;This is the browser caching problem. I ran into it while deploying a Compose Multiplatform WebAssembly app behind nginx on a VPS. The deployment succeeded, the new &lt;code&gt;.wasm&lt;/code&gt; files were on the server, the backend changes were live — yet users kept seeing the previous UI.&lt;/p&gt;

&lt;p&gt;Opening Chrome DevTools mysteriously fixed it. That clue led me straight to the root cause.&lt;/p&gt;


&lt;h2&gt;
  
  
  My Setup
&lt;/h2&gt;

&lt;p&gt;I'm building a Kotlin Multiplatform app with a Compose WASM frontend served through nginx on a VPS. After deploying a new version with UI changes, I expected users to automatically receive the latest build. Instead, the browser kept loading the old one — until DevTools was open, which disables caching by default.&lt;/p&gt;


&lt;h2&gt;
  
  
  What Is Browser Caching?
&lt;/h2&gt;

&lt;p&gt;When you visit a website, the browser downloads HTML, JS, WASM, CSS, and images. On future visits, instead of re-downloading everything, it loads from local cache — controlled by headers like:&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: public, max-age=31536000
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This tells the browser: &lt;em&gt;cache this file for one year and don't ask the server again.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;When no explicit cache headers are set, nginx falls back to heuristic caching — the browser guesses how long to keep files based on &lt;code&gt;Last-Modified&lt;/code&gt;. The result is unpredictable.&lt;/p&gt;




&lt;h2&gt;
  
  
  The index.html Problem (and Why It's the Real Culprit)
&lt;/h2&gt;

&lt;p&gt;Every web app starts with &lt;code&gt;index.html&lt;/code&gt;:&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="cp"&gt;&amp;lt;!DOCTYPE html&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;html&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;head&amp;gt;&lt;/span&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;"app.js"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"app"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This file is the &lt;em&gt;map&lt;/em&gt; — it tells the browser which JS, WASM, and CSS assets to load.&lt;/p&gt;

&lt;p&gt;Modern build tools already solve asset caching through content hashing. Instead of &lt;code&gt;app.js&lt;/code&gt;, they emit &lt;code&gt;app-a3f8c291.js&lt;/code&gt;. If the file changes, the hash changes, so the filename changes — the browser is forced to fetch it fresh. This is called &lt;strong&gt;cache busting&lt;/strong&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;But this entire strategy collapses if &lt;code&gt;index.html&lt;/code&gt; is cached.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If the browser serves a stale &lt;code&gt;index.html&lt;/code&gt;, it never discovers the new filenames and keeps loading the old assets.&lt;/p&gt;

&lt;p&gt;Since &lt;code&gt;index.html&lt;/code&gt; always has the same filename and nginx wasn't sending explicit cache headers for it, Chrome was happily caching it.&lt;/p&gt;

&lt;p&gt;The entire issue can be summarized in the following flow:&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9tf9ux18cgc151abgps9.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9tf9ux18cgc151abgps9.png" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Fix — Two nginx Location Blocks
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Never cache the HTML entry point&lt;/span&gt;
&lt;span class="k"&gt;location&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;/index.html&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;Cache-Control&lt;/span&gt; &lt;span class="s"&gt;"no-cache,&lt;/span&gt; &lt;span class="s"&gt;no-store,&lt;/span&gt; &lt;span class="s"&gt;must-revalidate"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;Pragma&lt;/span&gt; &lt;span class="s"&gt;"no-cache"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;Expires&lt;/span&gt; &lt;span class="s"&gt;"0"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Cache hashed assets forever&lt;/span&gt;
&lt;span class="k"&gt;location&lt;/span&gt; &lt;span class="p"&gt;~&lt;/span&gt;&lt;span class="sr"&gt;*&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="s"&gt;.(js|wasm|css)&lt;/span&gt;$ &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;Cache-Control&lt;/span&gt; &lt;span class="s"&gt;"public,&lt;/span&gt; &lt;span class="s"&gt;max-age=31536000,&lt;/span&gt; &lt;span class="s"&gt;immutable"&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;Then reload nginx:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nginx &lt;span class="nt"&gt;-t&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; systemctl reload nginx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No application changes. No rebuilds. No redeployment.&lt;/p&gt;




&lt;h2&gt;
  
  
  Breaking Down the Headers
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;For &lt;code&gt;index.html&lt;/code&gt;:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;no-cache, no-store, must-revalidate&lt;/code&gt; — always fetch a fresh copy&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Pragma: no-cache&lt;/code&gt; — HTTP/1.0 compatibility&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Expires: 0&lt;/code&gt; — immediately marks the response as expired&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;For JS, WASM, and CSS:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;public, max-age=31536000&lt;/code&gt; — cache for one year&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;immutable&lt;/code&gt; — the browser won't even attempt revalidation (safe, because content changes always produce new filenames)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  This Is the Industry Standard
&lt;/h2&gt;

&lt;p&gt;Vercel, Netlify, Next.js, and Create React App all implement this exact pattern. The rule is simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Cache the files that change their name. Never cache the file that doesn't.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Never cache &lt;code&gt;index.html&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Content hashing already handles JS/WASM cache invalidation — but only if &lt;code&gt;index.html&lt;/code&gt; is fresh.&lt;/li&gt;
&lt;li&gt;Missing cache headers create unpredictable browser behavior.&lt;/li&gt;
&lt;li&gt;If Chrome DevTools "fixes" a stale UI, it's a caching clue, not a solution.&lt;/li&gt;
&lt;li&gt;This is a deployment configuration problem, not a Compose WASM problem.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The next time a deployment looks successful but users still see an old UI, check your cache headers before touching your application code.&lt;/p&gt;




&lt;h2&gt;
  
  
  About Me
&lt;/h2&gt;

&lt;p&gt;I'm building production apps with Kotlin Multiplatform, Compose Multiplatform, Ktor, and WebAssembly. I'll keep sharing real-world debugging stories and deployment lessons as I hit them.&lt;/p&gt;

</description>
      <category>webassembly</category>
      <category>kotlin</category>
      <category>compose</category>
      <category>nginx</category>
    </item>
  </channel>
</rss>
