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.
In the modern web ecosystem, starting any web project without reaching for React, Next.js, or Vue has almost become taboo.
Yet, when our team at Setvy sat down to architect Aksara-Mart—a digital marketplace serving thousands of students and teachers across our vocational high school—we made an intentional, contrarian decision:
We threw out the frontend frameworks and built the entire client in pure Vanilla JavaScript, modern CSS, and PWA standards.
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.
Here is what we learned, why it paid off, and how you can implement a high-speed Vanilla JS architecture.
1. The Reality of School Hardware
Before writing a single line of code, we profiled our target environment:
- Hardware Constraints: 65% of students carry entry-level devices with 2GB to 3GB of RAM, powered by aging Quad-core MediaTek or Snapdragon 400-series processors.
- Prepaid Data, Not Wi-Fi: Most students don’t rely on school Wi-Fi at all—they browse on their own prepaid mobile data plans (kuota). 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.
- The Recess Rush: 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.
The Modern Framework Tax on Budget Devices
A barebones Next.js or React e-commerce template typically ships:
- 250 KB – 500 KB of minified JavaScript.
- JavaScript Parsing & Compilation Time: On an iPhone 16 or M3 MacBook, parsing 300KB of JS takes 15 milliseconds. On a budget Android phone, it takes 1,200 to 2,500 milliseconds.
- Hydration Lag (Total Blocking Time): The page looks loaded, but clicking the “Add to Cart” button does nothing because the JavaScript thread is frozen during hydration.
On high-end dev laptops, this latency is invisible. On budget devices, it ruins the experience.
2. The Architecture: Under 50 KB, 60 FPS
By returning to web fundamentals, we achieved:
- Total Initial JS + CSS Payload: < 48 KB (gzipped).
- First Contentful Paint (FCP): ~320 ms on low-end 4G.
- Time to Interactive (TTI): Instantaneous.
Here is how our client structure looks:
aksaramart/
├── public/
│ ├── index.html # Semantic HTML5 shell & 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 & offline resilience
│ └── manifest.json # Progressive Web App configuration
Enter fullscreen mode Exit fullscreen mode
3. Clean Patterns for Vanilla JS State & DOM
Building with Vanilla JS doesn’t mean writing chaotic spaghetti code. We used modern ES6+ patterns, event delegation, and atomic state rendering.
Pattern A: Modular State Store
// 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 => p.id === productId);
if (!product || product.stock <= 0) return;
const existing = appState.cart.find(item => item.id === productId);
if (existing) {
if (existing.qty < product.stock) existing.qty++;
} else {
appState.cart.push({ id: product.id, name: product.name, price: product.price, qty: 1 });
}
saveCart();
renderCartUI();
}
Enter fullscreen mode Exit fullscreen mode
Pattern B: High-Performance Event Delegation
Instead of attaching individual click listeners to hundreds of catalog items, we attached a single listener to the container:
document.getElementById('catalogGrid').addEventListener('click', (e) => {
const btn = e.target.closest('[data-action="add-to-cart"]');
if (!btn) return;
const productId = btn.dataset.productId;
addToCart(productId);
// Smooth micro-animation without any animation library
btn.classList.add('scale-bounce');
setTimeout(() => btn.classList.remove('scale-bounce'), 250);
});
Enter fullscreen mode Exit fullscreen mode
4. Making It a True App: Progressive Web App (PWA)
Most students won’t go to Google Play or the App Store to install a canteen app. But with PWA standards, they can install it directly from the browser in 1 tap.
Step 1: Web App Manifest (manifest.json)
{
"name": "Aksara Mart - Kantin & Koperasi Sekolah",
"short_name": "AksaraMart",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#10b981",
"icons": [
{
"src": "/icons/icon-192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/icons/icon-512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}
Enter fullscreen mode Exit fullscreen mode
Step 2: Custom “Install App” Topbar Trigger
Browsers often show a subtle or clunky native install banner. We intercepted the beforeinstallprompt event to give students an explicit, high-visibility install button in the top navigation:
let deferredPrompt;
const installBtn = document.getElementById('installAppBtn');
window.addEventListener('beforeinstallprompt', (e) => {
// 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 () => {
if (!deferredPrompt) return;
deferredPrompt.prompt();
const { outcome } = await deferredPrompt.userChoice;
console.log(`User install outcome: ${outcome}`);
deferredPrompt = null;
installBtn.style.display = 'none';
});
Enter fullscreen mode Exit fullscreen mode
Step 3: Cache-First Service Worker (sw.js)
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:
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) => {
e.waitUntil(
caches.open(CACHE_NAME).then(cache => cache.addAll(STATIC_ASSETS))
);
self.skipWaiting();
});
self.addEventListener('fetch', (e) => {
// 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 => cached || fetch(e.request))
);
});
Enter fullscreen mode Exit fullscreen mode
5. The Performance Verdict
Here is the Lighthouse audit comparison run on an emulated Moto G4 with a throttled 3G network:
Metric Typical Next.js / React E-Commerce Aksara-Mart (Vanilla JS + PWA) Performance Score 62 / 100 99 / 100 First Contentful Paint (FCP) 2.9 s 0.34 s Total Blocking Time (TBT) 840 ms 0 ms Speed Index 3.4 s 0.6 s Total Transfer Size 380 KB 46 KB6. Conclusion
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.
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 Inspect Element / DevTools to try altering menu prices from $1.50 to $0.01?
In the next article, we’ll dive deep into our Zero-Trust database security architecture using Supabase Row Level Security (RLS) and multi-role governance.
