<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged pwa</title>
<link>https://coderlegion.com/tag/pwa</link>
<description>Powered by Question2Answer</description>
<item>
<title>Building a High-Speed School Marketplace: Why We Ditched Frameworks for Vanilla JS &amp; PWA</title>
<link>https://coderlegion.com/30069/building-a-high-speed-school-marketplace-why-we-ditched-frameworks-for-vanilla-js-pwa</link>
<description>&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/e8hwzlfdk7uxwq5xloxr.webp&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;This is Part 1 of a 4-part series on building a high-speed, race-condition-proof digital marketplace for schools using Vanilla JS, Supabase RLS, PostgreSQL RPC, and Cloudflare Workers.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;In the modern web ecosystem, starting any web project without reaching for React, Next.js, or Vue has almost become taboo. &lt;/p&gt;
&lt;p&gt;Yet, when our team at &lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://setvy.id&quot;&gt;Setvy&lt;/a&gt;&lt;/strong&gt; sat down to architect &lt;strong&gt;Aksara-Mart&lt;/strong&gt;—a digital marketplace serving thousands of students and teachers across our vocational high school—we made an intentional, contrarian decision:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;We threw out the frontend frameworks and built the entire client in pure Vanilla JavaScript, modern CSS, and PWA standards.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Why? Because when your user base consists of students using second-hand, $80 Android phones running on limited prepaid mobile data, developer convenience cannot come at the expense of end-user performance.&lt;/p&gt;
&lt;p&gt;Here is what we learned, why it paid off, and how you can implement a high-speed Vanilla JS architecture.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;1-the-reality-of-school-hardware&quot;&gt;1. The Reality of School Hardware&lt;/h3&gt;
&lt;p&gt;Before writing a single line of code, we profiled our target environment:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Hardware Constraints&lt;/strong&gt;: 65% of students carry entry-level devices with 2GB to 3GB of RAM, powered by aging Quad-core MediaTek or Snapdragon 400-series processors.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Prepaid Data, Not Wi-Fi&lt;/strong&gt;: Most students don't rely on school Wi-Fi at all—they browse on their own prepaid mobile data plans (&lt;em&gt;kuota&lt;/em&gt;). Every megabyte is paid out of their pocket money, so a heavy page literally costs them. On top of that, when hundreds of phones hit the same nearby cell tower during recess—from inside concrete classroom buildings—effective speeds often drop to 2G/3G levels.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The Recess Rush&lt;/strong&gt;: Students have a very brief window between bells to browse the canteen menu, order food, and pick it up. A 4-second loading screen means they don't eat.&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 id=&quot;the-modern-framework-tax-on-budget-devices&quot;&gt;The Modern Framework Tax on Budget Devices&lt;/h4&gt;
&lt;p&gt;A barebones Next.js or React e-commerce template typically ships:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;250 KB – 500 KB&lt;/strong&gt; of minified JavaScript.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;JavaScript Parsing &amp;amp; Compilation Time&lt;/strong&gt;: On an iPhone 16 or M3 MacBook, parsing 300KB of JS takes 15 milliseconds. On a budget Android phone, it takes &lt;strong&gt;1,200 to 2,500 milliseconds&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Hydration Lag (Total Blocking Time)&lt;/strong&gt;: The page &lt;em&gt;looks&lt;/em&gt; loaded, but clicking the &quot;Add to Cart&quot; button does nothing because the JavaScript thread is frozen during hydration.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;On high-end dev laptops, this latency is invisible. On budget devices, it ruins the experience.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;2-the-architecture-under-50-kb-60-fps&quot;&gt;2. The Architecture: Under 50 KB, 60 FPS&lt;/h3&gt;
&lt;p&gt;By returning to web fundamentals, we achieved:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Total Initial JS + CSS Payload&lt;/strong&gt;: &lt;strong&gt;&amp;lt; 48 KB&lt;/strong&gt; (gzipped).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;First Contentful Paint (FCP)&lt;/strong&gt;: &lt;strong&gt;~320 ms&lt;/strong&gt; on low-end 4G.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Time to Interactive (TTI)&lt;/strong&gt;: &lt;strong&gt;Instantaneous&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here is how our client structure looks:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;aksaramart/
├── public/
│   ├── index.html       # Semantic HTML5 shell &amp;amp; modal templates
│   ├── skripmart.js     # Modular Vanilla JS logic (~35 KB)
│   ├── stylemart.css    # Clean, framework-free CSS with CSS Variables
│   ├── sw.js            # PWA Service Worker for caching &amp;amp; offline resilience
│   └── manifest.json    # Progressive Web App configuration
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;3-clean-patterns-for-vanilla-js-state-amp-dom&quot;&gt;3. Clean Patterns for Vanilla JS State &amp;amp; DOM&lt;/h3&gt;
&lt;p&gt;Building with Vanilla JS doesn't mean writing chaotic spaghetti code. We used modern ES6+ patterns, event delegation, and atomic state rendering.&lt;/p&gt;
&lt;h4 id=&quot;pattern-a-modular-state-store&quot;&gt;Pattern A: Modular State Store&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// A lightweight, reactive-free state store
const appState = {
    cart: JSON.parse(localStorage.getItem('aksara_cart') || '[]'),
    products: [],
    currentUser: null,
    activeCategory: 'all'
};

function saveCart() {
    localStorage.setItem('aksara_cart', JSON.stringify(appState.cart));
    renderCartBadge();
}

function addToCart(productId) {
    const product = appState.products.find(p =&amp;gt; p.id === productId);
    if (!product || product.stock &amp;lt;= 0) return;

    const existing = appState.cart.find(item =&amp;gt; item.id === productId);
    if (existing) {
        if (existing.qty &amp;lt; product.stock) existing.qty++;
    } else {
        appState.cart.push({ id: product.id, name: product.name, price: product.price, qty: 1 });
    }
    saveCart();
    renderCartUI();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;pattern-b-high-performance-event-delegation&quot;&gt;Pattern B: High-Performance Event Delegation&lt;/h4&gt;
&lt;p&gt;Instead of attaching individual click listeners to hundreds of catalog items, we attached a single listener to the container:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;document.getElementById('catalogGrid').addEventListener('click', (e) =&amp;gt; {
    const btn = e.target.closest('[data-action=&quot;add-to-cart&quot;]');
    if (!btn) return;
    
    const productId = btn.dataset.productId;
    addToCart(productId);
    
    // Smooth micro-animation without any animation library
    btn.classList.add('scale-bounce');
    setTimeout(() =&amp;gt; btn.classList.remove('scale-bounce'), 250);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;4-making-it-a-true-app-progressive-web-app-pwa-&quot;&gt;4. Making It a True App: Progressive Web App (PWA)&lt;/h3&gt;
&lt;p&gt;Most students won't go to Google Play or the App Store to install a canteen app. But with &lt;strong&gt;PWA standards&lt;/strong&gt;, they can install it directly from the browser in 1 tap.&lt;/p&gt;
&lt;h4 id=&quot;step-1-web-app-manifest-manifest-json-&quot;&gt;Step 1: Web App Manifest (&lt;code&gt;manifest.json&lt;/code&gt;)&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;name&quot;: &quot;Aksara Mart - Kantin &amp;amp; Koperasi Sekolah&quot;,
  &quot;short_name&quot;: &quot;AksaraMart&quot;,
  &quot;start_url&quot;: &quot;/&quot;,
  &quot;display&quot;: &quot;standalone&quot;,
  &quot;background_color&quot;: &quot;#ffffff&quot;,
  &quot;theme_color&quot;: &quot;#10b981&quot;,
  &quot;icons&quot;: [
    {
      &quot;src&quot;: &quot;/icons/icon-192.png&quot;,
      &quot;sizes&quot;: &quot;192x192&quot;,
      &quot;type&quot;: &quot;image/png&quot;
    },
    {
      &quot;src&quot;: &quot;/icons/icon-512.png&quot;,
      &quot;sizes&quot;: &quot;512x512&quot;,
      &quot;type&quot;: &quot;image/png&quot;
    }
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;step-2-custom-install-app-topbar-trigger&quot;&gt;Step 2: Custom &quot;Install App&quot; Topbar Trigger&lt;/h4&gt;
&lt;p&gt;Browsers often show a subtle or clunky native install banner. We intercepted the &lt;code&gt;beforeinstallprompt&lt;/code&gt; event to give students an explicit, high-visibility install button in the top navigation:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;let deferredPrompt;
const installBtn = document.getElementById('installAppBtn');

window.addEventListener('beforeinstallprompt', (e) =&amp;gt; {
    // Prevent default mini-infobar
    e.preventDefault();
    deferredPrompt = e;
    // Show our custom branded install button
    if (installBtn) installBtn.style.display = 'inline-flex';
});

installBtn?.addEventListener('click', async () =&amp;gt; {
    if (!deferredPrompt) return;
    deferredPrompt.prompt();
    const { outcome } = await deferredPrompt.userChoice;
    console.log(`User install outcome: ${outcome}`);
    deferredPrompt = null;
    installBtn.style.display = 'none';
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;step-3-cache-first-service-worker-sw-js-&quot;&gt;Step 3: Cache-First Service Worker (&lt;code&gt;sw.js&lt;/code&gt;)&lt;/h4&gt;
&lt;p&gt;To guarantee instant loading even when the cellular signal drops (or a student's data quota runs out mid-week), our Service Worker caches core static shell assets—so repeat visits cost almost zero data:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const CACHE_NAME = 'aksaramart-v1.4';
const STATIC_ASSETS = [
    '/',
    '/index.html',
    '/skripmart.js',
    '/stylemart.css',
    '/manifest.json',
    '/icons/icon-192.png'
];

self.addEventListener('install', (e) =&amp;gt; {
    e.waitUntil(
        caches.open(CACHE_NAME).then(cache =&amp;gt; cache.addAll(STATIC_ASSETS))
    );
    self.skipWaiting();
});

self.addEventListener('fetch', (e) =&amp;gt; {
    // Only cache static GET requests, let API requests go to network
    if (e.request.url.includes('/rest/v1/') || e.request.url.includes('/rpc/')) {
        return;
    }
    e.respondWith(
        caches.match(e.request).then(cached =&amp;gt; cached || fetch(e.request))
    );
});
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;5-the-performance-verdict&quot;&gt;5. The Performance Verdict&lt;/h3&gt;
&lt;p&gt;Here is the Lighthouse audit comparison run on an emulated Moto G4 with a throttled 3G network:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;Metric &lt;/th&gt;&lt;th align=&quot;left&quot;&gt;Typical Next.js / React E-Commerce &lt;/th&gt;&lt;th align=&quot;left&quot;&gt;Aksara-Mart (Vanilla JS + PWA)&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Performance Score&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;62 / 100 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;99 / 100&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;First Contentful Paint (FCP)&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;2.9 s &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;0.34 s&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Total Blocking Time (TBT)&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;840 ms &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;0 ms&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Speed Index&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;3.4 s &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;0.6 s&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Total Transfer Size&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;380 KB &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;46 KB&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&quot;6-conclusion&quot;&gt;6. Conclusion&lt;/h3&gt;
&lt;p&gt;Ditching frameworks for Vanilla JS proved to be a resounding success for our students and teachers. The app launches immediately, remains completely immune to hydration freezes, and preserves scarce phone battery and memory.&lt;/p&gt;
&lt;p&gt;However, a fast frontend is completely meaningless if the underlying database is vulnerable. How do we secure transaction data from curious IT vocational students who open &lt;code&gt;Inspect Element / DevTools&lt;/code&gt; to try altering menu prices from $1.50 to $0.01?&lt;/p&gt;
&lt;p&gt;In the next article, we'll dive deep into our Zero-Trust database security architecture using &lt;strong&gt;Supabase Row Level Security (RLS)&lt;/strong&gt; and multi-role governance.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/30069/building-a-high-speed-school-marketplace-why-we-ditched-frameworks-for-vanilla-js-pwa</guid>
<pubDate>Wed, 07 Oct 2026 04:14:44 +0000</pubDate>
</item>
<item>
<title>Project LifeOps (Part 5): 24/7 Zero-Cost Cloud Deployment ($0/month), Mobile Optimization, and PWA</title>
<link>https://coderlegion.com/27730/project-lifeops-part-5-24-zero-cost-cloud-deployment-0-month-mobile-optimization-and-pwa</link>
<description>&lt;p&gt;We have arrived at the grand finale of our engineering series. Across the four previous installments, we designed the &lt;a rel=&quot;nofollow&quot; href=&quot;https://datalaria.com/en/posts/app-lifeops_part1_arquitectura_backend/&quot;&gt;database architecture and FastAPI backend&lt;/a&gt;, crafted the &lt;a rel=&quot;nofollow&quot; href=&quot;https://datalaria.com/en/posts/app-lifeops_part2_frontend_dashboard/&quot;&gt;Glassmorphism Dark Mode React interface&lt;/a&gt;, built the &lt;a rel=&quot;nofollow&quot; href=&quot;https://datalaria.com/en/posts/app-lifeops_part3_modulos_kanban/&quot;&gt;interactive fitness, reading, cinema, and Kanban modules&lt;/a&gt;, and developed the &lt;a rel=&quot;nofollow&quot; href=&quot;https://datalaria.com/en/posts/app-lifeops_part4_informes_word_excel/&quot;&gt;in-memory Word and Excel reporting engine&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Yet in software development, a sobering pattern exists: &lt;strong&gt;countless extraordinary personal projects wither and die on &lt;code&gt;localhost:3000&lt;/code&gt; or &lt;code&gt;localhost:8000&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The obstacle is almost always the same: production cloud deployment is often perceived as a labyrinth of daunting server configurations, mounting database subscription fees, or the burden of maintaining live virtual machines. And when aiming to bring web apps to smartphones, developers frequently encounter costly gatekeepers: Apple Developer fees ($99/year), Google Play fees, opaque store review cycles, and massive hybrid frameworks.&lt;/p&gt;
&lt;p&gt;In this final installment, we demonstrate a far more elegant, sustainable path:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Deploying the entire full-stack architecture 24/7 at exactly $0.00/month&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Seamlessly handling free cloud container sleep cycles (&lt;em&gt;cold-starts&lt;/em&gt;) on the client side&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Engineering an ultra-ergonomic touch experience optimized for modern smartphones (Android / Samsung Galaxy and iPhone)&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Transforming the application into an installable Progressive Web App (PWA) added directly to home screens&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;&lt;p&gt;[!TIP]&lt;br&gt;
&lt;strong&gt;Test the live app&lt;/strong&gt;: You can explore the final production release of LifeOps at &lt;a rel=&quot;nofollow&quot; href=&quot;https://datalaria.com/apps/lifeops/&quot;&gt;https://datalaria.com/apps/lifeops/&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h4 id=&quot;-complete-lifeops-series-roadmap&quot;&gt;🗺️ Complete LifeOps Series Roadmap&lt;/h4&gt;
&lt;p&gt;With this post, the full 5-part blueprint is complete:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;🟢 &lt;strong&gt;Part 1&lt;/strong&gt;: &lt;a rel=&quot;nofollow&quot; href=&quot;https://datalaria.com/en/posts/app-lifeops_part1_arquitectura_backend/&quot;&gt;Personal Operating System Architecture and FastAPI + Supabase Backend&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;🟢 &lt;strong&gt;Part 2&lt;/strong&gt;: &lt;a rel=&quot;nofollow&quot; href=&quot;https://datalaria.com/en/posts/app-lifeops_part2_frontend_dashboard/&quot;&gt;React Frontend with Glassmorphism, 360° Dashboard, and Design System&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;🟢 &lt;strong&gt;Part 3&lt;/strong&gt;: &lt;a rel=&quot;nofollow&quot; href=&quot;https://datalaria.com/en/posts/app-lifeops_part3_modulos_kanban/&quot;&gt;Core Interactive Modules: Fitness, Library, Cinema, and Professional Kanban Board&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;🟢 &lt;strong&gt;Part 4&lt;/strong&gt;: &lt;a rel=&quot;nofollow&quot; href=&quot;https://datalaria.com/en/posts/app-lifeops_part4_informes_word_excel/&quot;&gt;Executive Word (.docx) Reporting Engine and Multi-Sheet Excel (.xlsx) Export&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;🟢 &lt;strong&gt;Part 5 (This article)&lt;/strong&gt;: 24/7 Zero-Cost Cloud Deployment ($0/month), Mobile UX, and PWA&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h4 id=&quot;1-production-cloud-topology-0-month-&quot;&gt;1. Production Cloud Topology ($0/month) 🌐☁️&lt;/h4&gt;
&lt;p&gt;To guarantee that LifeOps remains online 24/7 without recurring cloud invoices, we strategically leverage the most generous free tiers available:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌─────────────────────────────────────────────────────────────┐
│                       CLIENT DEVICE                         │
│           Web Browser / Installed Home Screen PWA           │
│            [https://datalaria.com/apps/lifeops/](https://datalaria.com/apps/lifeops/)              │
└──────────────────────────────┬──────────────────────────────┘
                               │
                               ▼
┌─────────────────────────────────────────────────────────────┐
│               FRONTEND: Netlify Edge CDN ($0/month)         │
│   • Vite build with Rollup Code Splitting (vendor-*)        │
│   • Instant global edge delivery via Gzip / Brotli          │
│   • Transparent proxy rewrite under Datalaria domain        │
└──────────────────────────────┬──────────────────────────────┘
                               │
            JWT Bearer Tokens  │  HTTPS REST Calls
                               ▼
┌─────────────────────────────────────────────────────────────┐
│               BACKEND: Render.com Web Service ($0/month)    │
│   • FastAPI + Uvicorn container (Python 3.11 / 3.13)        │
│   • Endpoint: [https://lifeops-api.onrender.com](https://lifeops-api.onrender.com)              │
│   • Anti-DoS Rate Limiting (slowapi) + Strict CORS          │
│   • Auto-spins down after 15 minutes of inactivity          │
└──────────────────────────────┬──────────────────────────────┘
                               │
                               ▼
┌─────────────────────────────────────────────────────────────┐
│              DATABASE: Supabase Cloud ($0/month)            │
│   • Managed PostgreSQL 15+ database instance                │
│   • Isolated `lifeops` schema with Row Level Security (RLS) │
│   • Cryptographic JWT session validation                    │
└─────────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;h5 id=&quot;1-1-vite-bundle-optimization-vite-config-js-&quot;&gt;1.1. Vite Bundle Optimization (&lt;code&gt;vite.config.js&lt;/code&gt;)&lt;/h5&gt;
&lt;p&gt;To guarantee sub-second initial loads across 4G and 5G mobile networks, we configured Rollup manual chunking:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  base: './', // Enables serving the SPA from any nested route (/apps/lifeops/)
  build: {
    rollupOptions: {
      output: {
        manualChunks(id) {
          if (id.includes('node_modules')) {
            if (id.includes('react') || id.includes('react-dom') || id.includes('react-router-dom')) {
              return 'vendor-react';
            }
            if (id.includes('lucide-react')) {
              return 'vendor-icons';
            }
            if (id.includes('@supabase')) {
              return 'vendor-supabase';
            }
            return 'vendor-libs';
          }
        },
      },
    },
    chunkSizeWarningLimit: 600,
  },
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;By segmenting &lt;code&gt;vendor-react&lt;/code&gt; and &lt;code&gt;@supabase&lt;/code&gt;, user browsers permanently cache stable external dependencies. When application features update, visitors download only a lightweight diff of new business logic.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;2-conquering-cold-starts-client-side-resilience-&quot;&gt;2. Conquering Cold-Starts: Client-Side Resilience 🛡️⏱️&lt;/h4&gt;
&lt;p&gt;The only operational tradeoff on Render's free tier is container hibernation after 15 minutes of zero traffic.&lt;/p&gt;
&lt;p&gt;When an end-user opens LifeOps after hours of downtime, the first HTTP request requires 25 to 45 seconds while Render allocates a container and initializes Uvicorn. Without proactive client-side handling, users face gateway timeouts (&lt;em&gt;HTTP 504&lt;/em&gt;) or frozen interfaces.&lt;/p&gt;
&lt;p&gt;In LifeOps, we turned this limitation into an &lt;strong&gt;informative, guided user experience&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;// lifeops-app/src/components/Dashboard/SystemHealth.jsx
const checkHealth = async (isAutoRetry = false) =&amp;gt; {
  setLoading(true);
  if (!isAutoRetry) setError(null);

  try {
    const data = await api.getHealth();
    setHealth(data);
    setError(null);
  } catch (err) {
    setError(err.message || 'Connecting to API...');
    
    // Auto-retry loop up to 3 times with 6-second delay while Render awakens
    if (retryCount &amp;lt; 3) {
      setTimeout(() =&amp;gt; {
        setRetryCount((prev) =&amp;gt; prev + 1);
      }, 6000);
    }
  } finally {
    setLoading(false);
  }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h5 id=&quot;what-happens-behind-the-scenes-&quot;&gt;What happens behind the scenes?&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;The initial health check dispatches a wake-up ping to the backend.&lt;/li&gt;
&lt;li&gt;The UI renders a friendly, transparent notification: &lt;em&gt;&quot;Render instance waking up (Cold start)... retrying automatically&quot;&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;The frontend retries in the background every 6 seconds without interrupting client navigation.&lt;/li&gt;
&lt;li&gt;As soon as FastAPI responds with &lt;code&gt;HTTP 200&lt;/code&gt;, the badge shifts to green (&lt;em&gt;&quot;FastAPI Online • Supabase Connected&quot;&lt;/em&gt;), fetching data without requiring a manual page refresh.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://datalaria.com/es/posts/app-lifeops_part5_deploy_mobile_pwa/lifeops_cloud_infrastructure_en.png&quot; alt=&quot;Account settings and cloud infrastructure panel connected to Render and Supabase&quot;&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;3-thumb-accessible-mobile-ergonomics-android-amp-ios-&quot;&gt;3. Thumb-Accessible Mobile Ergonomics (Android &amp;amp; iOS) 📱👍&lt;/h4&gt;
&lt;p&gt;Over 70% of daily interactions with a personal operating system occur on smartphones: logging a workout right after a run, cataloging a book during a commute, or reviewing Kanban deliverables from the couch.&lt;/p&gt;
&lt;p&gt;On a 6.5-inch device held in one hand, forcing the user's thumb to reach all the way to the top-left corner for a hamburger menu is poor ergonomic design.&lt;/p&gt;
&lt;p&gt;We anchored the mobile user experience on &lt;strong&gt;three ergonomic pillars&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌─────────────────────────────────────────────────────────────┐
│ 📱 SMARTPHONE VIEWPORT (&amp;lt;= 768px)                           │
├─────────────────────────────────────────────────────────────┤
│  [Top Header]: LifeOps Logo       [Locale ES/EN] [User Icon]│
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ┌───────────────────────────────────────────────────────┐  │
│  │ 🪟 FLOATING MODAL (React Portal)                      │  │
│  │ ───────────────────────────────────────────────────── │  │
│  │ [=== Drag Handle: slide down to dismiss ===]          │  │
│  │                                                       │  │
│  │  Title: Log Workout Session                           │  │
│  │  • 16px font inputs (prevents iOS Safari auto-zoom)   │  │
│  │  • Smooth touch scroll (touch-action: pan-y)          │  │
│  │                                                       │  │
│  │ ───────────────────────────────────────────────────── │  │
│  │ [📌 STICKY FOOTER ACTIONS: [ Cancel ] [ Save Record ]]│  │
│  └───────────────────────────────────────────────────────┘  │
│                                                             │
├─────────────────────────────────────────────────────────────┤
│ 📌 FIXED BOTTOM NAVIGATION BAR (Height: 64px)               │
│ [ 📊 Dashboard ] [ 🏃 Personal ] [ 💼 Tasks ] [ ⚙️ Settings ]│
└─────────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;h5 id=&quot;3-1-fixed-bottom-navigation-bar-bottomnav-jsx-&quot;&gt;3.1. Fixed Bottom Navigation Bar (&lt;code&gt;BottomNav.jsx&lt;/code&gt;)&lt;/h5&gt;
&lt;p&gt;Stationed permanently along the screen footer (&lt;code&gt;position: fixed; bottom: 0; z-index: 1000;&lt;/code&gt;), it houses 5 prominent touch targets (48x48px touch bounding boxes following Apple and Google accessibility standards), allowing instantaneous section transitions with a thumb tap.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://datalaria.com/es/posts/app-lifeops_part5_deploy_mobile_pwa/lifeops_mobile_bottom_nav_en.png&quot; alt=&quot;Smartphone mobile viewport featuring the thumb-accessible fixed bottom navigation bar&quot;&gt;&lt;/p&gt;
&lt;h5 id=&quot;3-2-off-canvas-sliding-drawer&quot;&gt;3.2. Off-Canvas Sliding Drawer&lt;/h5&gt;
&lt;p&gt;The primary navigation sidebar (&lt;code&gt;Sidebar.jsx&lt;/code&gt;) switches behavior based on screen real estate:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;On Desktop (&amp;gt; 768px)&lt;/strong&gt;: Expands to 260px or collapses to a slim 72px icon bar with floating tooltips, persisted in &lt;code&gt;localStorage&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;On Mobile (&amp;lt;= 768px)&lt;/strong&gt;: Becomes an Off-Canvas drawer sliding in over a frosted backdrop (&lt;code&gt;backdrop-filter: blur(8px)&lt;/code&gt;), auto-dismissing on link navigation.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://datalaria.com/es/posts/app-lifeops_part5_deploy_mobile_pwa/lifeops_mobile_drawer_en.png&quot; alt=&quot;Off-canvas mobile sliding drawer over frosted glass backdrop with quick links and system status&quot;&gt;&lt;/p&gt;
&lt;h5 id=&quot;3-3-viewport-anchored-modals-with-react-createportal-modal-jsx-&quot;&gt;3.3. Viewport-Anchored Modals with &lt;code&gt;React.createPortal&lt;/code&gt; (&lt;code&gt;Modal.jsx&lt;/code&gt;)&lt;/h5&gt;
&lt;p&gt;Form modals in mobile CSS are notorious for layout bugs: getting clipped by ancestor containers, misaligning behind virtual keyboards, or burying save buttons beneath the fold.&lt;/p&gt;
&lt;p&gt;In LifeOps, we engineered a clean architectural solution:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;// Rendered directly into document.body via React Portal
export function Modal({ isOpen, onClose, title, children }) {
  ...
  if (!isOpen) return null;

  return createPortal(
    &amp;lt;div className=&quot;modal-backdrop&quot; onClick={onClose}&amp;gt;
      &amp;lt;div className=&quot;modal-content&quot; onClick={(e) =&amp;gt; e.stopPropagation()}&amp;gt;
        {/* Touch drag handle for swipe-to-close */}
        &amp;lt;div className=&quot;modal-drag-handle-area&quot; onTouchStart={handleTouchStart} onTouchMove={handleTouchMove} onTouchEnd={handleTouchEnd}&amp;gt;
          &amp;lt;div className=&quot;modal-drag-pill&quot; /&amp;gt;
        &amp;lt;/div&amp;gt;
        &amp;lt;div className=&quot;modal-header&quot;&amp;gt;...&amp;lt;/div&amp;gt;
        &amp;lt;div className=&quot;modal-body&quot;&amp;gt;{children}&amp;lt;/div&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;,
    document.body
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;/* Precise Mobile Viewport Anchoring (Modal.css) */
@media (max-width: 768px) {
  .modal-backdrop {
    padding: 12px 10px calc(64px + 12px) 10px; /* Terminates exactly above the bottom nav */
    height: 100dvh; /* Dynamic Viewport Height handles mobile address bars */
  }

  .modal-body {
    touch-action: pan-y !important; /* Smooth touch gesture scrolling */
  }

  .form-group input, .form-group select {
    font-size: 16px !important; /* Prevents unwanted iOS Safari viewport auto-zooming */
  }

  /* Action buttons remain permanently docked in view */
  .form-actions {
    position: sticky !important;
    bottom: 0 !important;
    background: #111827 !important;
    z-index: 30;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With &lt;code&gt;createPortal&lt;/code&gt;, modals break free from ancestor stacking contexts. And thanks to &lt;code&gt;calc(64px + 12px)&lt;/code&gt; and sticky footers, the &lt;strong&gt;Cancel and Save buttons remain permanently anchored in thumb view&lt;/strong&gt;, regardless of form length.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://datalaria.com/es/posts/app-lifeops_part5_deploy_mobile_pwa/lifeops_mobile_modal_en.png&quot; alt=&quot;Touch-friendly mobile modal rendered via React Portal with swipe drag handle and sticky footer actions&quot;&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;4-progressive-web-app-pwa-transformation-&quot;&gt;4. Progressive Web App (PWA) Transformation 📲✨&lt;/h4&gt;
&lt;p&gt;For an application to feel native on a mobile device, it must install without store middlemen.&lt;/p&gt;
&lt;p&gt;We configured modern meta tags and web app standards in &lt;code&gt;index.html&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover&quot; /&amp;gt;
&amp;lt;meta name=&quot;theme-color&quot; content=&quot;#111827&quot; /&amp;gt;
&amp;lt;meta name=&quot;apple-mobile-web-app-capable&quot; content=&quot;yes&quot; /&amp;gt;
&amp;lt;meta name=&quot;apple-mobile-web-app-status-bar-style&quot; content=&quot;black-translucent&quot; /&amp;gt;
&amp;lt;link rel=&quot;icon&quot; type=&quot;image/svg+xml&quot; href=&quot;favicon.svg&quot; /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h5 id=&quot;key-benefits-as-an-installed-pwa-&quot;&gt;Key Benefits as an Installed PWA:&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Home Screen Presence&lt;/strong&gt;: High-resolution branded icon sitting alongside native apps on Android and iOS.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Fullscreen Standalone Mode&lt;/strong&gt;: Browser URL bars and navigation chrome disappear, reclaiming 15% of vertical screen space.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Status Bar Theming&lt;/strong&gt;: Device battery and clock bars harmonize seamlessly with the &lt;code&gt;#111827&lt;/code&gt; glassmorphic palette via &lt;code&gt;theme-color&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Instant Updates&lt;/strong&gt;: New builds deploy live over Git without waiting days for third-party store approvals.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h4 id=&quot;5-retrospective-lessons-learned-from-the-lifeops-blueprint-&quot;&gt;5. Retrospective: Lessons Learned from the LifeOps Blueprint 🎓💡&lt;/h4&gt;
&lt;p&gt;Architecting and engineering LifeOps from the ground up has been one of the most rewarding journeys documented on Datalaria.&lt;/p&gt;
&lt;p&gt;To conclude our technical journey, here are the &lt;strong&gt;three foundational takeaways&lt;/strong&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Relational modeling outperforms flat schemas as telemetry matures&lt;/strong&gt;:&lt;br&gt;
Structuring general events in &lt;code&gt;lifeops.activities&lt;/code&gt; with 1-to-1 extension tables (&lt;code&gt;workouts&lt;/code&gt;, &lt;code&gt;books&lt;/code&gt;, &lt;code&gt;films&lt;/code&gt;) and employing &lt;code&gt;JSONB&lt;/code&gt; for progress timelines delivered superior maintainability over sprawling, null-riddled tables.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Deliberate design fosters consistent user adherence&lt;/strong&gt;:&lt;br&gt;
Investing in visual polish (Glassmorphism, hardware-accelerated blurs, Outfit typography, and consistent Lucide iconography) transforms daily logging into an enjoyable ritual rather than a chore.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Modern serverless tooling enables $0/month production without compromises&lt;/strong&gt;:&lt;br&gt;
The pairing of &lt;strong&gt;FastAPI + Supabase + React (Vite) + Render + Netlify&lt;/strong&gt; provides reliability, speed, and cryptographic security rivaling commercial SaaS platforms charging $15/month per seat.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h4 id=&quot;series-conclusion-amp-complete-index-&quot;&gt;Series Conclusion &amp;amp; Complete Index 🚀&lt;/h4&gt;
&lt;p&gt;With this fifth installment, the official &lt;strong&gt;LifeOps&lt;/strong&gt; series stands complete. You have access to the complete source code, architectural rationales, and production implementation details step by step.&lt;/p&gt;
&lt;p&gt;I invite you to try the live application, experiment with its modules, and draw inspiration to build your own bespoke systems guided by Datalaria's core motto: &lt;strong&gt;&quot;Learn by building&quot;&lt;/strong&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;complete-series-reference-amp-links-&quot;&gt;Complete Series Reference &amp;amp; Links 🔗&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;🚀 &lt;strong&gt;LifeOps Live Application&lt;/strong&gt;: &lt;a rel=&quot;nofollow&quot; href=&quot;https://datalaria.com/apps/lifeops/&quot;&gt;datalaria.com/apps/lifeops&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;🌐 &lt;strong&gt;Interactive OpenAPI Docs&lt;/strong&gt;: &lt;a rel=&quot;nofollow&quot; href=&quot;https://lifeops-api.onrender.com/docs&quot;&gt;lifeops-api.onrender.com/docs&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;📚 &lt;strong&gt;Part 1&lt;/strong&gt;: &lt;a rel=&quot;nofollow&quot; href=&quot;https://datalaria.com/en/posts/app-lifeops_part1_arquitectura_backend/&quot;&gt;Personal Operating System Architecture and FastAPI + Supabase Backend&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;🎨 &lt;strong&gt;Part 2&lt;/strong&gt;: &lt;a rel=&quot;nofollow&quot; href=&quot;https://datalaria.com/en/posts/app-lifeops_part2_frontend_dashboard/&quot;&gt;React Frontend with Glassmorphism, 360° Dashboard, and Design System&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;📋 &lt;strong&gt;Part 3&lt;/strong&gt;: &lt;a rel=&quot;nofollow&quot; href=&quot;https://datalaria.com/en/posts/app-lifeops_part3_modulos_kanban/&quot;&gt;Core Interactive Modules: Fitness, Library, Cinema, and Professional Kanban Board&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;📊 &lt;strong&gt;Part 4&lt;/strong&gt;: &lt;a rel=&quot;nofollow&quot; href=&quot;https://datalaria.com/en/posts/app-lifeops_part4_informes_word_excel/&quot;&gt;Executive Word (.docx) Reporting Engine and Multi-Sheet Excel (.xlsx) Export&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;☁️ &lt;strong&gt;Part 5 (This article)&lt;/strong&gt;: 24/7 Zero-Cost Cloud Deployment ($0/month), Mobile UX, and PWA.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Thank you for following along throughout this series! If you are building your own personal operating systems or have ideas for enhancements, share your insights in the comments below. 👇&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/27730/project-lifeops-part-5-24-zero-cost-cloud-deployment-0-month-mobile-optimization-and-pwa</guid>
<pubDate>Sat, 19 Sep 2026 10:13:09 +0000</pubDate>
</item>
<item>
<title>Caching Strategy for Offline Web Games: Install Group vs Lazy Group</title>
<link>https://coderlegion.com/25724/caching-strategy-for-offline-web-games-install-group-vs-lazy-group</link>
<description>&lt;p&gt;Most of the PWA writing out there is aimed at content sites, and games break those assumptions immediately because the asset budget is an order of magnitude larger. This is the split that has worked for us, written for people who ship builds rather than blog posts.&lt;/p&gt;
&lt;h3 id=&quot;the-three-requirements-briefly&quot;&gt;The Three Requirements, Briefly&lt;/h3&gt;
&lt;p&gt;A browser game becomes installable when three things are true: it is served over HTTPS, it links a web app manifest, and it registers a service worker. The manifest is a JSON file describing the name, the icon set, the theme colors, the display mode and the start URL, and it is what turns the thing from a page into a home screen icon with a splash screen. The service worker is the part that does real work.&lt;/p&gt;
&lt;h3 id=&quot;split-your-assets-into-two-groups&quot;&gt;Split Your Assets Into Two Groups&lt;/h3&gt;
&lt;p&gt;The mistake is caching everything during service worker install. A 200 MB precache means the install event runs for a minute on a bad connection and the browser may kill it, so you get no offline support at all rather than partial support.&lt;/p&gt;
&lt;p&gt;Split instead. The install group is whatever the player needs to reach an interactive first screen: the engine bundle, the UI atlas, the font, the first level, the menu music. That should be small enough to fetch in a few seconds. Everything else is the lazy group, cached on first use with a cache-first handler, so level 12 lands in the cache the first time someone reaches level 12 and is available offline afterward.&lt;/p&gt;
&lt;p&gt;Version your cache names and delete old caches in the activate event. Without that, a shipped update leaves the previous build's assets on the device forever, and players report bugs you already fixed.&lt;/p&gt;
&lt;h3 id=&quot;where-saves-actually-live&quot;&gt;Where Saves Actually Live&lt;/h3&gt;
&lt;p&gt;Small state goes in localStorage: settings, the current level, a coin count. It is synchronous, it is capped around 5 MB, and it is fine for anything you would have put in a config file.&lt;/p&gt;
&lt;p&gt;Anything structured goes in IndexedDB: full save states, replays, downloaded user content, large inventories. It is asynchronous and awkward to use directly, so wrap it or use a small library, but it is the only browser storage that will hold a real save file.&lt;/p&gt;
&lt;p&gt;Both are subject to eviction. A player who installs the game to their home screen gets persistent storage in most browsers, which is a good argument for prompting the install rather than treating it as a nice extra.&lt;/p&gt;
&lt;h3 id=&quot;ios-is-where-the-plan-breaks&quot;&gt;iOS Is Where the Plan Breaks&lt;/h3&gt;
&lt;p&gt;Test on iOS before you commit to a PWA-only distribution plan. Storage limits are tighter and eviction is more aggressive, audio behavior around the first user gesture is stricter, and the install flow is a manual Add to Home Screen through the share sheet rather than a prompt you can trigger. None of that is fatal, but all of it is easier to design around than to retrofit.&lt;/p&gt;
&lt;p&gt;The longer version, covering install prompts, the manifest fields that matter and the honest comparison against native, is in our &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.abratabia.com/pwa-games/&quot;&gt;guide to PWA games&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;when-not-to-bother&quot;&gt;When Not To Bother&lt;/h3&gt;
&lt;p&gt;If your game is a one-off jam entry that nobody will open twice, the service worker is wasted work. PWA pays off when players come back, and the return visit is the whole reason to spend the effort on cache versioning and save durability.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/25724/caching-strategy-for-offline-web-games-install-group-vs-lazy-group</guid>
<pubDate>Sat, 29 Aug 2026 01:22:38 +0000</pubDate>
</item>
<item>
<title>The Web needs a Save button. </title>
<link>https://coderlegion.com/16951/the-web-needs-a-save-button</link>
<description>&lt;p&gt;Every web app today behaves like it has short-term memory loss.&lt;/p&gt;
&lt;p&gt;You type something, switch tabs, get a call, refresh the page… and everything is gone.&lt;/p&gt;
&lt;p&gt;We accepted this like it’s normal. It’s not.&lt;/p&gt;
&lt;h3 id=&quot;the-idea&quot;&gt;The Idea&lt;/h3&gt;
&lt;p&gt;I’ve been building something around a simple concept:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What if web apps had a Save button?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Not autosave. Not background sync. Not “hope the browser keeps it.”&lt;/p&gt;
&lt;p&gt;A real, intentional Save button — like in desktop apps.&lt;/p&gt;
&lt;p&gt;Click it, and your app state is preserved.&lt;br&gt;
Close the tab, reopen it later, and you continue exactly where you left off.&lt;/p&gt;
&lt;h3 id=&quot;why-this-matters&quot;&gt;Why this matters&lt;/h3&gt;
&lt;p&gt;The modern web is powerful, but fragile:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Page refresh = data loss  &lt;/li&gt;
&lt;li&gt;Tab crash = lost work  &lt;/li&gt;
&lt;li&gt;Mobile background kill = reset state  &lt;/li&gt;
&lt;li&gt;Poor connectivity = broken flow  &lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We keep rebuilding “state persistence” in every app like it’s a custom problem.&lt;/p&gt;
&lt;p&gt;It shouldn’t be.&lt;/p&gt;
&lt;h3 id=&quot;-what-i-m-working-on&quot;&gt;⚙️ What I’m working on&lt;/h3&gt;
&lt;p&gt;A small persistence layer for web apps that acts like a &lt;strong&gt;Save button for state&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Manual or automatic save triggers  &lt;/li&gt;
&lt;li&gt;Restore exact UI state on reload  &lt;/li&gt;
&lt;li&gt;Works with modern browser storage APIs  &lt;/li&gt;
&lt;li&gt;Designed to be lightweight, not a framework  &lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Think of it as:&lt;/p&gt;
&lt;p&gt;   Not a backend replacement&lt;br&gt;
   Not a full database layer&lt;br&gt;
   Just a &lt;strong&gt;reliable memory switch for web apps&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id=&quot;the-goal&quot;&gt;The goal&lt;/h3&gt;
&lt;p&gt;To make persistence:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;obvious  &lt;/li&gt;
&lt;li&gt;reusable  &lt;/li&gt;
&lt;li&gt;boring (in a good way)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So instead of every developer reinventing state recovery, they just… press Save.&lt;/p&gt;
&lt;h3 id=&quot;links&quot;&gt;Links&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Original article: &lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/edmundsparrow/your-browser-has-short-term-memory-loss-i-fixed-it-1456&quot;&gt;https://dev.to/edmundsparrow/your-browser-has-short-term-memory-loss-i-fixed-it-1456&lt;/a&gt;  &lt;/li&gt;
&lt;li&gt;Source code: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/edmundsparrow/gnoke-persist&quot;&gt;https://github.com/edmundsparrow/gnoke-persist&lt;/a&gt;  &lt;/li&gt;
&lt;li&gt;Demo: &lt;a rel=&quot;nofollow&quot; href=&quot;https://edmundsparrow.github.io/gnoke-persist&quot;&gt;https://edmundsparrow.github.io/gnoke-persist&lt;/a&gt;  &lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;why-i-m-posting-this&quot;&gt;Why I’m posting this&lt;/h3&gt;
&lt;p&gt;Because if this idea is useful, it shouldn’t stay hidden in one project.&lt;/p&gt;
&lt;p&gt;If you build web apps, you’ve probably rebuilt this problem in your own way already.&lt;/p&gt;
&lt;p&gt;Now imagine if it was standardized as a simple pattern:&lt;/p&gt;
&lt;p&gt;  &quot;Just add a Save button.”&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;final-thought&quot;&gt;Final thought&lt;/h3&gt;
&lt;p&gt;The web doesn’t need more complexity.&lt;/p&gt;
&lt;p&gt;It needs better memory.&lt;/p&gt;
</description>
<category>Discussions</category>
<guid isPermaLink="true">https://coderlegion.com/16951/the-web-needs-a-save-button</guid>
<pubDate>Thu, 07 May 2026 10:53:44 +0000</pubDate>
</item>
<item>
<title>Enhancement on service worker and caching</title>
<link>https://coderlegion.com/11224/enhancement-on-service-worker-and-caching</link>
<description>&lt;hr&gt;
&lt;p&gt;title: &quot;Enhancement on Caching and Service Workers&quot;&lt;br&gt;
published: true&lt;/p&gt;
&lt;h3 id=&quot;tags-video-youtube-devcommunity-javascript-cache-performance&quot;&gt;tags: video, youtube, devcommunity, javascript, cache, performance&lt;/h3&gt;
&lt;h2 id=&quot;welcome-to-my-video-post&quot;&gt;Welcome to My Video Post&lt;/h2&gt;
&lt;p&gt;Following my &lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/jaboarnoldlandry/comprehensive-guide-on-getting-started-with-caching-using-service-workers-11b1&quot;&gt;previous article on getting started with service workers and caching&lt;/a&gt;,&lt;br&gt;
I’ve created a video explaining how to update an already existing cache.  &lt;/p&gt;
&lt;p&gt;Enjoy!&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;final-thoughts&quot;&gt;Final Thoughts&lt;/h3&gt;
&lt;p&gt;Thanks for watching!&lt;br&gt;
If you’ve tried similar caching strategies, share your experience in the comments—I’d love to compare notes.&lt;/p&gt;
</description>
<category>Videos</category>
<guid isPermaLink="true">https://coderlegion.com/11224/enhancement-on-service-worker-and-caching</guid>
<pubDate>Sat, 07 Feb 2026 17:00:46 +0000</pubDate>
</item>
<item>
<title>Bad UX every website/PWA overlooks — the mobile navigation</title>
<link>https://coderlegion.com/11065/bad-ux-every-website-pwa-overlooks-the-mobile-navigation</link>
<description>&lt;p&gt;Building a progressive web app is different from building a regular website, because every part of it has to be treated like a native app.&lt;/p&gt;
&lt;p&gt;I’ve built several progressive web apps, and the most important thing to get right is the mobile navigation. Once a user installs your PWA (hopefully), they expect it to behave exactly like an app.&lt;/p&gt;
&lt;p&gt;Have you ever been on a website on a mobile device, tapped something that opened a modal, and then the moment you pressed or swiped back, the whole site just closed?&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=12992029422903287709&quot; alt=&quot;ChatGPT Site&quot;&gt;&lt;/p&gt;
&lt;p&gt;Yes! Not even the ChatGPT website handles mobile navigation properly. After clicking the login button, a dialog appears. On mobile, the expected way to close a dialog is by pressing or swiping back, but that wasn’t considered, you’re expected to tap the overlay or the close button instead.&lt;/p&gt;
&lt;p&gt;Compare that to this progressive web app, which handles navigation properly: &lt;a rel=&quot;nofollow&quot; href=&quot;https://pluxscore.com&quot;&gt;Pluxscore&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=6799146983962971197&quot; alt=&quot;Pluxscore&quot;&gt;&lt;/p&gt;
&lt;p&gt;We’ve been building the web with a huge focus on &lt;strong&gt;mobile responsiveness&lt;/strong&gt;, while quietly ignoring &lt;strong&gt;mobile navigation&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Mobile users, especially Android users, are used to swiping back to close modals or dialogs, navigate between pages, or interact with a website or PWA in different situations.&lt;/p&gt;
&lt;p&gt;It was while building Pluxscore, the first progressive web app I worked on, that I ran into this problem.&lt;/p&gt;
&lt;p&gt;In plain JavaScript, when a user swipes back on mobile, the current location is removed from the history stack. The fix is to control UI elements by storing their visibility state as part of navigation.&lt;/p&gt;
&lt;p&gt;That means when a user triggers a dialog, we navigate to the same URL, but with a different state.&lt;/p&gt;
&lt;p&gt;I had two ways to approach this: using the &lt;code&gt;history.state&lt;/code&gt; API, or using &lt;code&gt;URLSearchParams&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/History/pushState#examples&quot;&gt;https://developer.mozilla.org/en-US/docs/Web/API/History/pushState#examples&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;/* When using history.state */
function openSidebar(){
  const state = { open_sidebar: true };
  history.pushState(state, &quot;&quot;);
}

/* When using URLSearchParams */
function openSidebar(){
  const url = new URL(location);
  url.searchParams.set(&quot;open_sidebar&quot;, &quot;true&quot;);
  history.pushState({}, &quot;&quot;, url);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I tried &lt;code&gt;URLSearchParams&lt;/code&gt; at first, but it makes the URL look like this:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;https://example.com?open_sidebar=true&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Using history.state is much better, because the URL doesn’t change and you can store serializable values in it.&lt;/p&gt;
&lt;p&gt;Since I use React for my progressive web apps, I have to manage this state through React Router.&lt;/p&gt;
&lt;p&gt;A basic example of that would look like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;import { useLocation, useNavigate } from &quot;react-router&quot;;
import { useCallback } from &quot;react&quot;;

export default function App() {
  const navigate = useNavigate();
  const location = useLocation();

  /** Track sidebar state */
  const isSidebarOpened = location.state?.isSidebarOpened || false;

  /* Open sidebar */
  const openSidebar = useCallback(() =&amp;gt; {
    navigate(location, {
      state: {
        ...location.state,
        isSidebarOpened: true,
      },
    });
  }, [location, navigate]);

  /* Close sidebar */
  const closeSidebar = useCallback(() =&amp;gt; {
    navigate(-1);
  }, [navigate]);

  return (
    &amp;lt;&amp;gt;
      &amp;lt;button onClick={openSidebar}&amp;gt;Open sidebar&amp;lt;/button&amp;gt;
      {isSidebarOpened ? (
        &amp;lt;div&amp;gt;
          {/* Sidebar content */}
          &amp;lt;button onClick={closeSidebar}&amp;gt;Close sidebar&amp;lt;/button&amp;gt;
        &amp;lt;/div&amp;gt;
      ) : null}
    &amp;lt;/&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But in a complex UI, there are usually more elements that trigger a modal/dialog within a single page, e.g. a sidebar dialog, login dialog, post edit dialog, etc.&lt;br&gt;
So I created two hooks specifically for this: &lt;code&gt;useLocationState.ts&lt;/code&gt; and &lt;code&gt;useLocationToggle.ts&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;useLocationState.ts&lt;/strong&gt;&lt;br&gt;
This hook exposes an API similar to &lt;code&gt;useState&lt;/code&gt;, allowing you to push any serializable value into the history stack. It accepts a default value if the item doesn’t exist in the current location state.&lt;/p&gt;
&lt;p&gt;Removing an item is as simple as navigating back or setting the value to &lt;code&gt;undefined&lt;/code&gt;. If the app is reloaded in this state, it falls back to navigating to the root.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;//useLocationState.ts
import { useCallback, useMemo } from &quot;react&quot;;
import { useLocation, useNavigate, type NavigateOptions } from &quot;react-router&quot;;

export default function useLocationState&amp;lt;T&amp;gt;(
    key: string,
    defaultValue: T,
): [T, (value?: T, options?: NavigateOptions) =&amp;gt; void] {
    const navigate = useNavigate();
    const location = useLocation();
    
    /** Current value */
    const value =
        typeof location.state?.[key] !== &quot;undefined&quot;
            ? location.state?.[key]
            : defaultValue;

    /** Set value */
    const setValue = useCallback(
        (newValue?: T, options?: NavigateOptions) =&amp;gt; {
            if (typeof newValue !== &quot;undefined&quot;) {
                navigate(location, {
                    ...options,
                    state: {
                        ...location.state,
                        ...options?.state,
                        [key]: newValue,
                    },
                });
            } else {
                if (location.key !== &quot;default&quot;) {
                    navigate(-1);
                } else {
                    navigate(&quot;/&quot;, { ...options, replace: true });
                }
            }
        },
        [key, navigate, location],
    );

    return useMemo(() =&amp;gt; [value, setValue], [value, setValue]);

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;useLocationToggle.ts&lt;/strong&gt;&lt;br&gt;
This is a wrapper around the &lt;code&gt;useLocationState&lt;/code&gt; hook. It stores a boolean and provides a method to toggle the state.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;//useLocationToggle.ts
import { useCallback, useMemo } from &quot;react&quot;;
import { type NavigateOptions } from &quot;react-router&quot;;
import useLocationState from &quot;./useLocationState&quot;;

export default function useLocationToggle(
    key: string,
): [boolean, (status: boolean, options?: NavigateOptions) =&amp;gt; void] {
    /** Current State */
    const [show, setShow] = useLocationState(key, false);

    /** Toggle Location */
    const toggle = useCallback(
        (status: boolean, options?: NavigateOptions) =&amp;gt; {
            if (status) {
                setShow(true, options);
            } else {
                setShow(undefined);
            }
        },
        [key, setShow],
    );

    return useMemo(() =&amp;gt; [show, toggle], [show, toggle]);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When put into use, it behaves much like &lt;code&gt;useState()&lt;/code&gt;, which makes it easy to replace existing code that already relies on &lt;code&gt;useState()&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;export default function App(){
  const [opened, setOpened] = useLocationToggle(&quot;homepage-sidebar&quot;);
  return (
    &amp;lt;&amp;gt;
      &amp;lt;button onClick={()=&amp;gt;setOpened(true)}&amp;gt;Open sidebar&amp;lt;/button&amp;gt;
      {opened ? (
        &amp;lt;div&amp;gt;
          {/* Sidebar content */}
          &amp;lt;button onClick={()=&amp;gt;setOpened(false)}&amp;gt;Close sidebar&amp;lt;/button&amp;gt;
        &amp;lt;/div&amp;gt;
      ) : null}
    &amp;lt;/&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=6799146983962971197&quot; alt=&quot;Pluxscore&quot;&gt;&lt;/p&gt;
&lt;p&gt;In the demonstration above, the sidebar is controlled by its own state in the location, and the same applies to the matches being displayed. We can end up with a state that looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;/* Sidebar */
{
    sidebar: true,
}

/* Match details */
{
    ['match-1234-details']: true,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Ever since then, I’ve been implementing this across all the progressive web apps I build.&lt;/p&gt;
&lt;p&gt;As the creator of &lt;a rel=&quot;nofollow&quot; href=&quot;https://pwabucket.com&quot;&gt;PWABucket&lt;/a&gt;, I believe we can build progressive web apps that are fast, responsive, indistinguishable from native apps, and deliver a solid user experience across platforms.&lt;/p&gt;
&lt;p&gt;You can see the same implementation in Parcel (&lt;a rel=&quot;nofollow&quot; href=&quot;https://parcel.pwabucket.com&quot;&gt;https://parcel.pwabucket.com&lt;/a&gt;&lt;br&gt;
).&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=5567601622272112325&quot; alt=&quot;Parcel PWA&quot;&gt;&lt;/p&gt;
&lt;p&gt;Another example shown in the Tracker PWA (&lt;a rel=&quot;nofollow&quot; href=&quot;https://tracker.pwabucket.com/&quot;&gt;https://tracker.pwabucket.com/&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=1711245175785688792&quot; alt=&quot;Tracker PWA&quot;&gt;&lt;/p&gt;
&lt;p&gt;The modern web offers a ton of rich APIs, and if you build a progressive web app carefully and pay close attention to detail, you can create truly engaging experiences.&lt;/p&gt;
&lt;p&gt;I specialize in pixel-perfect progressive web apps. If you’re looking for someone to bring your app ideas to life, I’m currently open for work. Hire me: &lt;a rel=&quot;nofollow&quot; href=&quot;https://sadiqsalau.com&quot;&gt;https://sadiqsalau.com&lt;/a&gt;.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/11065/bad-ux-every-website-pwa-overlooks-the-mobile-navigation</guid>
<pubDate>Wed, 04 Feb 2026 20:18:33 +0000</pubDate>
</item>
<item>
<title>Comprehensive guide on getting started with caching using service workers</title>
<link>https://coderlegion.com/10501/comprehensive-guide-on-getting-started-with-caching-using-service-workers</link>
<description>&lt;p&gt;Ever wondered why some sites loads slow when you first visit them, but after loading the full page when you get back the next time the site loads instantly fast, in today’s guide I will be  walking you through the beauty of service worker by demonstrating how you can reduce you’re site loading time in the users browsing device the next time they visit you’re site.&lt;/p&gt;
&lt;h3 id=&quot;table-of-contents&quot;&gt;Table of Contents&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;#prerequisites&quot;&gt;Prerequisites&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;#what-is-a-service-worker&quot;&gt;What is a service Worker&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;#caching&quot;&gt;Caching&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;#basic-caching-implementation-using-service-worker&quot;&gt;Basic caching implementation using Service worker&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;#starting-cache-operations&quot;&gt;Starting Cache operations&lt;/a&gt;&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;#install-event&quot;&gt;install event&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;#activate-event&quot;&gt;activate event&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;#fetch-event&quot;&gt;Fetch event&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;#wrap-up&quot;&gt;Wrap Up&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;#final-thoughts&quot;&gt;Final Thoughts&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;prerequisites-&quot;&gt;Prerequisites:&lt;/h2&gt;
&lt;p&gt;To follow along this guide, you must be familiar with the following JavaScript concept:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Promises&lt;/li&gt;
&lt;li&gt;Events in JavaScript&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;what-is-a-service-worker-&quot;&gt;What is a service Worker:&lt;/h2&gt;
&lt;p&gt;service workers is a JavaScript technique of running tasks in the background from the page main thread to help us developers, intercept request, cache resources from the client and even allows the app to run even offline, we will be focusing on how to cache resources/assets of the site in this article today.&lt;/p&gt;
&lt;h2 id=&quot;caching&quot;&gt;Caching&lt;/h2&gt;
&lt;p&gt;caching is to save resources and assets of the site that takes too much time to load to the browser’s memory so the next time we try to access them we won’t need to re-download them from the server rather but on the browser's memory which is instantly first than getting them from the server.&lt;/p&gt;
&lt;h2 id=&quot;basic-caching-implementation-using-service-worker&quot;&gt;Basic caching implementation using Service worker&lt;/h2&gt;
&lt;p&gt;I have a created a simple site of space tourism which contains information about different location on the space you can visit in the space and the details about it; is a frontend mentor challenge which you can find using this &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.frontendmentor.io/challenges/space-tourism-multipage-website-gRWj1URZ3&quot;&gt;link&lt;/a&gt; if you want to try it out and follow along with.&lt;/p&gt;
&lt;p&gt;this project is the perfect scenario to explain how we can cache the resources to reduce the load time on our site, and to be clear a site with a lot of visuals like images can be slow so I want to cache the home page to show how you can cache the site if you find site loading slowly and give you an insight on how service worker and caching works.&lt;/p&gt;
&lt;p&gt;Looking at this image below, I am displaying all the time and size each file is taking to load the full homepage or index page&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/w10ayidfwql8v9o57o5e.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
&lt;p&gt;Image showing the network status when I first load the site on my device for the first time&lt;/p&gt;
&lt;p&gt;You can see that I am fetching files with 264 KB in 266ms as the time it takes to load the whole landing page which means when I again visit the site it will take the same amount of time and size to load which is not ideal, again you find this information by going to the &lt;strong&gt;devtools→network&lt;/strong&gt; tab&lt;/p&gt;
&lt;p&gt;We start by registering a service worker in the app entry or main file mine is &lt;code&gt;index.js&lt;/code&gt; &lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;if(&quot;serviceWorker&quot; in window.navigator){
window.navigator.serviceWorker.register('/sw.js').then(registeredSw =&amp;gt; console.log(registeredSw).catch(err =&amp;gt; console.log(err))
} 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We start by checking if the current browser the app/site running on supports the service Worker, &lt;code&gt;serviceWorker&lt;/code&gt; is a property of the &lt;strong&gt;navigator object of the windows object.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;we use the &lt;code&gt;register()&lt;/code&gt;  on the &lt;code&gt;serviceWorker&lt;/code&gt; to register a service worker in the browser, which takes in 2 arguments the first one is the destination where we’ve created a file which we will be writing in all our caching logic it is advised to put the service worker file in global directory in order to have access to all files&lt;/p&gt;
&lt;p&gt;and the second argument is optional, &lt;strong&gt;the scope&lt;/strong&gt; of files the service worker will track if you’ve saved it in a certain folder, it’s scope would be that given directory that why it advisable to put in the global directory.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;register()&lt;/code&gt; returns a &lt;strong&gt;Promise&lt;/strong&gt; we use &lt;code&gt;then&lt;/code&gt; to display the registered service worker and &lt;code&gt;catch&lt;/code&gt; to catch any errors that may occur while registering the service worker&lt;/p&gt;
&lt;p&gt;if it is successfully registered will be returned its instance in &lt;code&gt;console&lt;/code&gt; which will look like this&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pf234vrtqgbzqceyhgwj.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
&lt;p&gt;The console shows the registered service worker&lt;/p&gt;
&lt;p&gt;To view the created service worker, you will need to navigate to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;devtools&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;application and choose the &lt;strong&gt;Service workers&lt;/strong&gt; pane on the left panel of the application section&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/79wd52pze7gun7j3wh0e.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
&lt;p&gt;service worker takes 3 phases it passes through to be installed which are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;installation&lt;/strong&gt;: which have completed in the first step when we register a new service worker&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Activation&lt;/strong&gt;: activation of the service worker to start operating&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;wait&lt;/strong&gt;:  Still waiting for the service to be activating&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;starting-cache-operations-&quot;&gt;Starting Cache operations:&lt;/h3&gt;
&lt;p&gt;In service Workers we use event driven approach to cache and fetch our resources and assets; in this section we will look each one of events needed to start service worker and cache your assets and resources and explain its part in caching resources/assets.&lt;/p&gt;
&lt;h4 id=&quot;install-event-&quot;&gt;install event:&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;Exploration of the event&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;this is the first event to be fired, remember when we registered a new service worker that’s when the event is triggered&lt;/p&gt;
&lt;p&gt;in this event we create a cache storage in the browser and add the resources that we will need to be cached in the cache.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Implementation of the event&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;to implement switch to the &lt;code&gt;sw.js&lt;/code&gt; you have registered as you're service worker file and then implement the following snippet&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;const resourcesToCache = [
  &quot;/&quot;,
  &quot;/index.html&quot;,
  &quot;src/JavaScript/index.js&quot;,
  &quot;src/output.css&quot;,
];
const cacheName = &quot;version-1&quot;;

self.addEventListener(&quot;install&quot;, (event) =&amp;gt; {
  event.waitUntil(
    caches
      .open(cacheName)
      .then((cache) =&amp;gt; {
        cache.addAll(resourcesToCache);
      })
      .catch((err) =&amp;gt; console.log(err)),
  );
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The above snippet declare  &lt;code&gt;resourcesToCache&lt;/code&gt; array to keep track of multiple resources and assets we want to cache and &lt;code&gt;cacheName&lt;/code&gt;  is the name that will use as the cache name.&lt;/p&gt;
&lt;h4 id=&quot;event-setup-&quot;&gt;event setup:&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;the &lt;code&gt;self&lt;/code&gt; is service worker we have registered, essentially it is attaching event to itself that’s why they call it &lt;code&gt;self&lt;/code&gt; &lt;/li&gt;
&lt;li&gt;we attach an event of &lt;code&gt;install&lt;/code&gt;  as well as a callback function that will trigger when service worker is first registered on any device browser our site/app is running in.&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 id=&quot;cache-setup-&quot;&gt;Cache setup:&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;The &lt;code&gt;waitUntil()&lt;/code&gt;  on the install event ensures that the service worker is installed after the operation inside the &lt;code&gt;waitUntil()&lt;/code&gt; has completed.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;caches&lt;/code&gt;  object helps create, manage the browser’s cache storage.&lt;/li&gt;
&lt;li&gt;We start by opening a new cache with &lt;code&gt;open()&lt;/code&gt;  which takes a string as an argument that will act as a cache name, we used the &lt;code&gt;cacheName&lt;/code&gt;  variable.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;open()&lt;/code&gt; returns a promise we chain &lt;code&gt;then()&lt;/code&gt;  to access the created cache.&lt;/li&gt;
&lt;li&gt;The  &lt;code&gt;.addAll()&lt;/code&gt; to add multiple resources we would like to cache and finally a &lt;code&gt;catch&lt;/code&gt;  block to handle any errors that may occur in caching.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt; you’ve just cached you’re first assets/resources in your browser cache, to view the changes go to &lt;strong&gt;devtools(application→Service workers)&lt;/strong&gt; and you should see you a button labeled as &lt;code&gt;skipWaiting&lt;/code&gt;  &lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rxvyh4f12nfpgpsa3uud.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
&lt;p&gt;Service worker panel in the browser&lt;/p&gt;
&lt;p&gt;click on &lt;code&gt;skipWaiting&lt;/code&gt; and then look on the left and then expand to &lt;code&gt;Cache Storage&lt;/code&gt; tab you should see the name of you cache and when you click on it you will see resources cached insider there&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/70onuwi141w3f4snergk.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
&lt;p&gt;created cache in the browser&lt;/p&gt;
&lt;p&gt;and this should be it for setting up the cache for the site&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; that to make testing easier it is better to enable the checkbox for &lt;code&gt;Update on reload&lt;/code&gt;  in service workers’ section; to test without needing to click on &lt;code&gt;skipWaiting&lt;/code&gt; every single time you make changes to the page.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;activate-event-&quot;&gt;activate event:&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;Exploration of the event&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;this event fires every time there is a new change in a service worker file, its responsible is to activate a service worker.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Implementation of the event&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;self.addEventListener(&quot;activate&quot;, (event) =&amp;gt; {
  console.log(&quot;Activated service worker&quot;);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;we use the &lt;code&gt;activate&lt;/code&gt;  event to trigger &lt;strong&gt;activation of our service worker&lt;/strong&gt; every time there is a change in service worker file, so the next time you change something from the &lt;code&gt;sw.js&lt;/code&gt; the &lt;code&gt;activate&lt;/code&gt; event will be triggered and in console you will see the message ‘&lt;strong&gt;Activated service worker&lt;/strong&gt;’.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;fetch-event-&quot;&gt;Fetch event:&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;Exploration of the event:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This event is fired every time you make a request to the server requesting resources/assets, and this is where we intercept the request and first check if the data we are trying to download from the server includes in our cache if we have it in our cache we can directly return them if not we can continue with the request.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Implementation of the event:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;self.addEventListener(&quot;fetch&quot;, (event) =&amp;gt; {
  event.respondWith(
    caches.match(event.request).then((response) =&amp;gt; {
      return response || fetch(event.request);
    }),
  );
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;in above snippet we have:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;attached &lt;code&gt;fetch&lt;/code&gt; event to service worker that will trigger any time we make a request&lt;/li&gt;
&lt;li&gt;we access the &lt;code&gt;respondWith()&lt;/code&gt; of the &lt;code&gt;fetch&lt;/code&gt;  event, the &lt;code&gt;respondWith()&lt;/code&gt; method will help intercept the request before it sends to the server requesting some assets/ resources&lt;/li&gt;
&lt;li&gt;we access the &lt;code&gt;request&lt;/code&gt; property on the &lt;code&gt;event&lt;/code&gt;  object.&lt;/li&gt;
&lt;li&gt;inside the &lt;code&gt;respondWith()&lt;/code&gt; we then use &lt;code&gt;match()&lt;/code&gt; and pass our request to find the matching resources/assets in cache similar to the request we are sending to the server.&lt;/li&gt;
&lt;li&gt;the &lt;code&gt;caches.match()&lt;/code&gt; returns a promise, we attach &lt;code&gt;then()&lt;/code&gt; to handle the resolve the promise and if it has found some assets/resources in our cache we then return them instantly if not we continue with the request.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;p&gt;to see the effects of this let’s compare the network of the first image when we had no service worker and now, when we have a service worker&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Figure A:&lt;/strong&gt; Before service worker implementation network report&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jyq16woj0r988yry62p9.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
&lt;p&gt;network request report of site with not service worker and cache&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Figure B:&lt;/strong&gt; After service worker implementation network report&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fj8lqmgev2tibgcjnyub.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
&lt;p&gt;network request report of site with service worker and cache&lt;/p&gt;
&lt;p&gt;With this you can see the difference, the file that we are getting from our service worker have a low load time compared to before the service worker and you can see they are taking up 0 size compared to before.&lt;/p&gt;
&lt;p&gt;before we had a load time of 266 and after service worker, we have a load time of 124 which is less than half of the first load time we had before we cache.&lt;/p&gt;
&lt;p&gt;we also had &lt;strong&gt;264Kb&lt;/strong&gt; of our resource size before we apply service worker but now, we only have &lt;strong&gt;116Kb&lt;/strong&gt; of resource size.&lt;/p&gt;
&lt;h2 id=&quot;wrap-up&quot;&gt;Wrap Up&lt;/h2&gt;
&lt;p&gt;In this guide we covered how to apply cache to static resources that may cause long load time which can be a bad user experience on the site, &lt;/p&gt;
&lt;h2 id=&quot;final-thoughts-1&quot;&gt;Final Thoughts&lt;/h2&gt;
&lt;p&gt;Service workers are not only limited to caching resources they can be used for push notifications, enabling site accessibility in offline mode which is commonly known as PWA (Progress web app)&lt;/p&gt;
&lt;p&gt;This guide helps you get started working with service worker, basic implementation and fundamental explanation of how caching works, in future articles I am planning to dive deep of how we can use service worker not only for caching but also for offline accessibility and background sync techniques.&lt;/p&gt;
</description>
<category>Tutorials</category>
<guid isPermaLink="true">https://coderlegion.com/10501/comprehensive-guide-on-getting-started-with-caching-using-service-workers</guid>
<pubDate>Mon, 26 Jan 2026 10:28:13 +0000</pubDate>
</item>
<item>
<title>PWA and Django #4: Installing a PWA as a native application</title>
<link>https://coderlegion.com/880/pwa-and-django-4-installing-a-pwa-as-a-native-application</link>
<description>&lt;p&gt;Welcome to the fourth entry on the Progressive Web Application with Django series. In this chapter we will learn how to install our webapp as a native application. Very useful and really easy.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://substackcdn.com/image/fetch/w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fba57a900-5b98-45d0-bd38-550d5645c3df_512x512.png&quot; alt=&quot;image&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;allowing-native-installation&quot;&gt;Allowing native installation&lt;/h3&gt;
&lt;p&gt;With a small change to the source code, we can ask the user if they want to install our supercool webapp as a &quot;native&quot; app.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let beforeInstallPromptEvent = null;
let installed = false;

async function installPWA() {
   if (beforeInstallPromptEvent === null || installed) {
       return;
   }

   try {
       beforeInstallPromptEvent.prompt();

       const { outcome } = await beforeInstallPromptEvent.userChoice;
       if (outcome === 'accepted') {
           console.log(&quot;App install dialog accepted!&quot;);
           beforeInstallPromptEvent = null;
           installed = true;
       }

   } catch(e) {
       console.error(e);
   }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We can also listen to a couple of events to customize the responses and behavior of our webapp in every step of the process:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;window.addEventListener('beforeinstallprompt', (e) =&amp;gt; {
   beforeInstallPromptEvent = e;
});

window.addEventListener('appinstalled', () =&amp;gt; {
   installed = true;
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The user needs to fire the event, so an install button is a good place to call the new code:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://substackcdn.com/image/fetch/w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F21a2596b-e8ac-4751-b745-8c8e8bd4db74_302x390.png&quot; alt=&quot;image&quot;&gt;&lt;/p&gt;
&lt;p&gt;When clicked, the user will see a browser dialog asking for the installation like this:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://substackcdn.com/image/fetch/w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb127466f-1047-4437-a449-2ebcae04a73e_470x204.png&quot; alt=&quot;image&quot;&gt;&lt;/p&gt;
&lt;p&gt;If accepted, the webapp will be registered on the operating system, with its own launcher icon:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://substackcdn.com/image/fetch/w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb1a321fe-b3f5-446c-ab4a-2289681b6308_756x462.png&quot; alt=&quot;image&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;executing-as-a-standalone-app&quot;&gt;Executing as a standalone app&lt;/h3&gt;
&lt;p&gt;Once installed, the web app will launch in its own window, providing a more integrated experience with styling based on the manifest (see previous chapters). Like this:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://substackcdn.com/image/fetch/f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8ca65df6-5df0-4aeb-a7ae-ba2ce15c5e85_507x861.png&quot; alt=&quot;image&quot;&gt;&lt;/p&gt;
&lt;p&gt;Note that you can customize the icon, the behavior of the webapp, colors, etc. in the PWA manifest. You can read more about this here:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://developer.mozilla.org/en-US/docs/Web/Manifest&quot;&gt;https://developer.mozilla.org/en-US/docs/Web/Manifest&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://web.dev/learn/pwa/web-app-manifest&quot;&gt;https://web.dev/learn/pwa/web-app-manifest&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;about-the-list&quot;&gt;About the list&lt;/h3&gt;
&lt;p&gt;Among the Python and Docker posts, I will also write about other related topics, like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Software architecture&lt;/li&gt;
&lt;li&gt;Programming environments&lt;/li&gt;
&lt;li&gt;Linux operating system&lt;/li&gt;
&lt;li&gt;Etc.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you found some interesting technology, programming language or whatever, please, let me know! I'm always open to learning something new!&lt;/p&gt;
&lt;h3 id=&quot;about-the-author&quot;&gt;About the author&lt;/h3&gt;
&lt;p&gt;I'm Andrés, a full-stack software developer based in Palma, on a personal journey to improve my coding skills. I'm also a self-published fantasy writer with four published novels to my name. Feel free to ask me anything!&lt;/p&gt;
</description>
<category>Getting Started</category>
<guid isPermaLink="true">https://coderlegion.com/880/pwa-and-django-4-installing-a-pwa-as-a-native-application</guid>
<pubDate>Sun, 23 Feb 2025 10:58:34 +0000</pubDate>
</item>
<item>
<title>PWA and Django #3: Online and offline resources in a PWA - Developing Progressive Web Applications with Django</title>
<link>https://coderlegion.com/486/pwa-django-online-offline-resources-developing-progressive-web-applications-with-django</link>
<description>&lt;p&gt;Welcome to the third entry on the Progressive Web Application with Django series. In this chapter, we'll learn how to cache resources for our PWA to be able to use them offline, without an active internet connection.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://substackcdn.com/image/fetch/f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc2285582-02bc-4d1d-a6fa-f0298469c065_2048x2048.png&quot; alt=&quot;Online and offline resources in a PWA&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;implementing-offline-functionality&quot;&gt;Implementing Offline Functionality&lt;/h3&gt;
&lt;p&gt;In the previous chapters we defined an small PWA application with every required part: the manifest and the ServiceWorker. We learned how to register the PWA and developed a very simple interface with some images:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://substackcdn.com/image/fetch/f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcff26966-a642-403e-9308-b5971d534994_367x302.png&quot; alt=&quot;UI for our PWA&quot;&gt;&lt;/p&gt;
&lt;p&gt;Now we will learn how to store data in the PWA cache and how to choose from where to load every image: from the internet or from the local cache.&lt;/p&gt;
&lt;p&gt;To store one or more resources on the PWA cache we use a function like this on the ServiceWorker:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const CACHE_NAME = &quot;DJANGO_PWA_TEST&quot;
const MAIN_URL = &quot;https://laboratorio.alvarezperello.com/djangopwa/&quot;;

self.addEventListener(&quot;install&quot;, (event) =&amp;gt; {
   console.info(&quot;*** PWA event *** install&quot;, event);
   event.waitUntil(activateApp());
});

self.addEventListener(&quot;activate&quot;, (event) =&amp;gt; {
   console.info(&quot;*** PWA event *** activate&quot;, event);
   event.waitUntil(activateApp());
});

async function activateApp() {
   // When a service worker is initially registered, pages won't use it
   // until they next load. The claim() method causes those pages to be
   // controlled immediately.
   console.log('Claiming control...');
   clients.claim().then((ev) =&amp;gt; {
       console.log('...claimed!', ev);
   })

   manageCache();
}

self.addEventListener(&quot;sync&quot;, (event) =&amp;gt; {
   console.info(&quot;*** PWA event *** sync&quot;, event);
   manageCache();
});

async function manageCache() {
   const cache = await caches.open(CACHE_NAME);
   if (!cache) {
       console.error(&quot;Error storing resources in cache!&quot;);
       return;
   }

   storeResourceInCache(cache, MAIN_URL+&quot;static/demo/img/snake1.jpg&quot;);
   //storeResourceInCache(cache, MAIN_URL+&quot;static/demo/img/snake2.png&quot;);
   //storeResourceInCache(cache, MAIN_URL+&quot;static/demo/img/snake3.png&quot;);
}

async function storeResourceInCache(cache, element) {
   console.log(&quot;Storing resource in cache: &quot;+element);
   cache.add(element).then(event =&amp;gt; {
       console.info(&quot;Resource stored successfully! &quot;+element);
   }).catch(event =&amp;gt; {
       console.error(&quot;Error storing resource! &quot;+element, event);
   });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now, when we execute our PWA, we can read the cache messages in the developer console:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Registering service worker...
...register completed!
The service worker is active!

serviceworker.js: Claiming control...
serviceworker.js: Resource already in cache! static/demo/img/snake1.jpg
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Our PWA cache is working!&lt;/p&gt;
&lt;h3 id=&quot;choosing-from-where-to-load-each-resource&quot;&gt;Choosing from where to load each resource&lt;/h3&gt;
&lt;p&gt;When the PWA loads a resource, calls the fetch event, like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;self.addEventListener(&quot;fetch&quot;, async (event) =&amp;gt; {
   console.info(&quot;*** PWA event *** fetch&quot;, event);
  
   let url = event.request.url.toString();
   console.info(&quot;The PWA is loading a resource from: &quot;+url);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We are in control of the request now, and can choose from where return the requested resource: from cache or from internet.&lt;/p&gt;
&lt;p&gt;Here is an example of how to check if we have a resource cached and return it from cache. And, if not cached, request it from the Internet instead.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;self.addEventListener(&quot;fetch&quot;, async (event) =&amp;gt; {
   let url = event.request.url.toString();
   if (!url.includes(&quot;static/demo/img/snake&quot;)) {
       return false;
   }

   const cache = await caches.open(CACHE_NAME);
   if (!cache) {
       console.error(&quot;Error loading resources from cache!&quot;);
       return false;
   }

   let fetchResponsePromise = await cache.match(url).then(async (cachedResponse) =&amp;gt; {
       if (cachedResponse &amp;amp;&amp;amp; cachedResponse.ok) {
           console.warn(&quot;Loading from cache: &quot;+url);
           return cachedResponse;

       } else {
           console.error(&quot;Error! the cache does not have this url! &quot;+url);
           console.error(cache.keys());

           remoteFetchResponsePromise = await fetch(event).then(async (networkResponse) =&amp;gt; {
               console.warn(&quot;Loading from internet: &quot;+url);
               return networkResponse;
           });

           return remoteFetchResponsePromise;
       }
   });


   return (await fetchResponsePromise);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We can read the developer console to know from where every image has been loaded, like this:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://substackcdn.com/image/fetch/f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F142f62e0-1acd-46ab-b257-6c5a64ac5bff_708x71.png&quot; alt=&quot;From where every image has been loaded&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;in-the-next-chapter&quot;&gt;In the next chapter&lt;/h3&gt;
&lt;p&gt;We now have a PWA. Now we will learn how to make an installable PWA, that will show as a native application in the operating system. That's one of the greatest functionalities of the PWAs: we can use them to create &quot;almost native&quot; applications using Django.&lt;/p&gt;
&lt;p&gt;See you in the next chapter!&lt;/p&gt;
&lt;h3 id=&quot;about-the-list-1&quot;&gt;About the list&lt;/h3&gt;
&lt;p&gt;Among the Python and Docker posts, I will also write about other related topics, like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Software architecture&lt;/li&gt;
&lt;li&gt;Programming environments&lt;/li&gt;
&lt;li&gt;Linux operating system&lt;/li&gt;
&lt;li&gt;Etc.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you found some interesting technology, programming language or whatever, please, let me know! I'm always open to learning something new!&lt;/p&gt;
&lt;h3 id=&quot;about-the-author-1&quot;&gt;About the author&lt;/h3&gt;
&lt;p&gt;I'm Andrés, a full-stack software developer based in Palma, on a personal journey to improve my coding skills. I'm also a self-published fantasy writer with four published novels to my name. Feel free to ask me anything!&lt;/p&gt;
</description>
<category>Web Dev</category>
<guid isPermaLink="true">https://coderlegion.com/486/pwa-django-online-offline-resources-developing-progressive-web-applications-with-django</guid>
<pubDate>Wed, 27 Nov 2024 22:52:48 +0000</pubDate>
</item>
</channel>
</rss>