[{"categories":["Blog","JavaScript"],"content":"The portfolio landing page has a full-screen matrix-rain canvas behind the content — 14 px monospace glyphs falling on a 60 fps loop. It\u0026rsquo;s the kind of feature that looks fine in casual use and turns out to be quietly expensive once you instrument it.\nThe first version had three perf bugs. Each one would have been invisible in a dev-tool profile snapshot; together, they were enough to make a mid-2020 MacBook\u0026rsquo;s fans spin up on a static page.\nBug 1: 60 forced reflows per second The canvas reads the active glyph colour from a CSS custom property — --neon-blue — so it stays in sync when the user switches themes. The first implementation did this:\nfunction frame() { var colour = getComputedStyle(document.documentElement) .getPropertyValue(\u0026#39;--neon-blue\u0026#39;).trim(); ctx.fillStyle = colour; // ... draw drops ... } getComputedStyle() forces a layout read. Calling it 60 times per second is roughly 60 forced reflows per second on a long-lived page. Chrome\u0026rsquo;s \u0026ldquo;forced reflow\u0026rdquo; warning fires constantly.\nThe fix: cache the colour, refresh it on theme change.\nvar cachedGlyph = \u0026#39;#00f3ff\u0026#39;; function refreshGlyph() { try { var v = getComputedStyle(document.documentElement) .getPropertyValue(\u0026#39;--neon-blue\u0026#39;).trim(); if (v) cachedGlyph = v; } catch (e) { /* keep previous */ } } window.addEventListener(\u0026#39;lt:theme-change\u0026#39;, refreshGlyph); One read at boot, one read on every theme switch, zero reads in the frame loop. Performance panels go quiet.\nBug 2: the resize storm Window resize fires at whatever rate the OS reports it — sometimes 60 Hz, sometimes higher. The naive implementation re-initialised the drop array on every event:\nwindow.addEventListener(\u0026#39;resize\u0026#39;, function () { width = window.innerWidth; height = window.innerHeight; canvas.width = width; canvas.height = height; columns = Math.ceil(width / fontSize); drops = makeDrops(columns); // \u0026lt;- allocates a new array }); A 2-second window-drag on a 4K monitor allocates and discards thousands of small arrays.\nThe fix: debounce at 150 ms.\nvar resizeTimer = 0; window.addEventListener(\u0026#39;resize\u0026#39;, function () { clearTimeout(resizeTimer); resizeTimer = setTimeout(resize, 150); }); The drop array only rebuilds after the user stops dragging. The visible effect is the same — at 60 fps, 150 ms is below the perceptual threshold for \u0026ldquo;rain re-initialised.\u0026rdquo;\nBug 3: the double-rAF self-perpetuating loop The original visibilitychange handler queued a fresh requestAnimationFrame every time the tab flipped back into view:\ndocument.addEventListener(\u0026#39;visibilitychange\u0026#39;, function () { if (!document.hidden) { requestAnimationFrame(frame); // \u0026lt;- in addition to the loop\u0026#39;s own rAF } }); Each visibility flip added another self-perpetuating loop. After 20 tab flips, 20 concurrent frame() calls were running. Each one clears the canvas and redraws it, so the visible result was\u0026hellip; still a working rain, but with 20x the work.\nThe fix: a single running flag plus one rAF guard.\nvar running = true; var rafId = 0; function frame() { rafId = 0; // \u0026lt;- this rAF is now \u0026#34;consumed\u0026#34; if (!running) return; // ... draw ... rafId = requestAnimationFrame(frame); } document.addEventListener(\u0026#39;visibilitychange\u0026#39;, function () { if (document.hidden) { running = false; } else if (!rafId) { // \u0026lt;- only restart if not already running running = true; rafId = requestAnimationFrame(frame); } }); The frame loop always re-arms itself; the visibility handler only restarts it if no rAF is currently pending. Tab flips can no longer multiply the work.\nBonus: the boot guard One more change worth calling out. The rain is toggleable from the theme dashboard, and the preference is persisted to localStorage under lt.rain.enabled. If the user has disabled rain, the canvas is never created in the first place:\nif (localStorage.getItem(\u0026#39;lt.rain.enabled\u0026#39;) === \u0026#39;0\u0026#39;) return; The boot guard means a disabled-rain user pays exactly zero GPU cost. Reload after re-enabling, and the canvas mounts and starts the loop.\nWhy this matters beyond the rain None of these are exotic fixes. The pattern is the same in every long-lived UI:\nCache expensive reads. Anything inside a frame loop that touches the DOM, the layout, or getComputedStyle is a candidate. Read once at boot, refresh on change events. Debounce bursty events. Resize, scroll, input — any event that fires faster than the user can perceive change. Idempotent event handlers. A handler should be safe to call any number of times and have the same effect as calling it once. The double-rAF bug is what idempotency violations look like in animation code. A profile-driven debug session usually finds one of these. The trick is to keep looking after you fix the first one.\n","date":"2026-08-15","permalink":"http://fahimimam.pro.bd/blog/matrix-rain-performance/","section":"blog","summary":"The first version of the matrix-rain canvas called getComputedStyle() every animation frame to read the active \u0026ndash;neon-blue token. Three small changes turned 60 reflows/sec into zero.","tags":["JavaScript","Performance","Canvas","Frontend"],"title":"60 forced reflows a second: fixing the matrix rain"},{"categories":["Blog","Go"],"content":"The lunch-tracker backend persists one JSON file per write. Every record is small. The file is rewritten on every clock-in, every clock-out, every prune. The naive implementation — os.WriteFile(path, buf, 0644) — is \u0026ldquo;correct\u0026rdquo; until the process crashes, the VPS loses power, or someone kill -9s it mid-write. Then it\u0026rsquo;s a 50/50 whether the file on disk is the previous version, the new version, or a half-written JSON that won\u0026rsquo;t parse.\nThe fix is the standard POSIX dance: write to a temp file, fsync, then rename. It fits in 30 lines and gives you a real durability guarantee.\nThe function // flushLocked writes the in-memory map atomically: encode to a // temp file in the same directory and rename over the destination. // Caller must hold writeMu. func (s *Store) flushLocked() error { s.mu.RLock() buf, err := json.MarshalIndent(s.records, \u0026#34;\u0026#34;, \u0026#34; \u0026#34;) s.mu.RUnlock() if err != nil { return fmt.Errorf(\u0026#34;encode %s: %w\u0026#34;, s.path, err) } dir := filepath.Dir(s.path) tmp, err := os.CreateTemp(dir, \u0026#34;data-*.json.tmp\u0026#34;) if err != nil { return fmt.Errorf(\u0026#34;create temp: %w\u0026#34;, err) } tmpName := tmp.Name() cleanup := func() { _ = os.Remove(tmpName) } if _, err := tmp.Write(buf); err != nil { _ = tmp.Close() cleanup() return fmt.Errorf(\u0026#34;write temp: %w\u0026#34;, err) } if err := tmp.Sync(); err != nil { // \u0026lt;- fsync _ = tmp.Close() cleanup() return fmt.Errorf(\u0026#34;sync temp: %w\u0026#34;, err) } if err := tmp.Close(); err != nil { cleanup() return fmt.Errorf(\u0026#34;close temp: %w\u0026#34;, err) } if err := os.Rename(tmpName, s.path); err != nil { cleanup() return fmt.Errorf(\u0026#34;rename: %w\u0026#34;, err) } return nil } Three properties fall out of this pattern.\n1. The temp file lives in the same directory as the destination os.CreateTemp(dir, \u0026quot;data-*.json.tmp\u0026quot;) — the dir is filepath.Dir(s.path), not os.TempDir(). This matters because of the next property.\n2. rename(2) is atomic on POSIX — but only on the same filesystem A rename syscall within one filesystem is atomic: at any instant, a reader either sees the old path or the new path, never a half-state. This is the kernel guarantee the pattern is built on.\nBut \u0026ldquo;atomic within one filesystem\u0026rdquo; is a real constraint. If the temp file lives in /tmp (often tmpfs) and the destination lives in /var/lib/lunch-tracker/data.json (likely ext4), then rename falls back to copy + delete, which is not atomic. Creating the temp in the same directory keeps the two paths on the same filesystem, so the rename is atomic.\n3. tmp.Sync() is fsync(2), and it\u0026rsquo;s the part most people skip os.File.Write returns as soon as the data is in the kernel\u0026rsquo;s page cache. Without Sync, a power loss between Write returning and the kernel flushing the page cache to disk leaves you with an empty or partial file — even if rename was atomic.\nSync calls fsync(2) on the file, which blocks until the data and metadata are on the platter (or SSD). It\u0026rsquo;s slow — a Sync per write is enough to make this unsuitable for hot-path databases — but for a configuration-style write at maybe one per minute, the cost is invisible and the safety is real.\nAfter rename, the file is durable. A crash anywhere before that point leaves either the previous version of the file or a partial temp file — never a partial destination file.\nThe cleanup pattern Every error path calls cleanup(), which is _ = os.Remove(tmpName). Without that, every crash leaks a data-*.json.tmp file in the data directory. Over months, you accumulate hundreds of them. With it, the directory stays clean.\n_ = os.Remove(...) deliberately ignores the error: if the file is already gone (e.g. another goroutine cleaned it up first), that\u0026rsquo;s fine. The cleanup function is best-effort.\nThe lock naming convention flushLocked and pruneOldRecordsLocked both end in Locked. That\u0026rsquo;s the convention I borrowed from the Go standard library: the Locked suffix is a contract — \u0026ldquo;the caller must hold the lock.\u0026rdquo; There\u0026rsquo;s no enforcement; it\u0026rsquo;s just a name. But the comment block above the function states it explicitly, and that makes a code review catch missing-lock bugs at a glance.\nWhen to use this pattern Any time a single process writes a small file that humans (or you, debugging at 2 AM) will read back:\nJSON config files — yes, use this. SQLite databases — the SQLite library does this internally; you don\u0026rsquo;t need to. Write-ahead logs — yes, this is the same pattern under a different name. Bulk data files (GBs) — no. The Sync per write is too expensive. The whole pattern is twenty lines. It\u0026rsquo;s the kind of code you write once, copy forever, and never have to think about again — except on the days when you do, and then it saves you.\n","date":"2026-08-15","permalink":"http://fahimimam.pro.bd/blog/atomic-file-writes-go/","section":"blog","summary":"Every write to data.json in lunch-tracker goes through a temp file in the same directory, an explicit fsync, then a rename. Three lines of reasoning turn that into correctness.","tags":["Go","Persistence","Linux","POSIX"],"title":"Atomic writes in Go: temp file, fsync, rename"},{"categories":["Projects"],"content":"A personal lunch-eligibility tracker. The policy at my workplace is 6 hours 45 minutes after clock-in → eligible for lunch, and this project exists so I never have to remember the math. The full engineering case study — problem, architecture, challenges, API, deployment, lessons — is rendered on the page below.\n","date":"2026-08-15","permalink":"http://fahimimam.pro.bd/projects/lunch-tracker/","section":"projects","summary":"A live lunch-eligibility countdown kept in sync between the Pathao HRMS web UI and a tiny Go backend. See the engineering case study below.","tags":["Go","Vanilla JS","Tampermonkey","Nginx","systemd"],"title":"Lunch Tracker"},{"categories":["Projects"],"content":"This very website. Hugo on a 1-core VPS, deployed with a single rsync, and equipped with a few of my own pieces: a four-theme palette engine, a live matrix-rain background, a floating design dashboard with keyboard shortcuts, and a custom showcase layout for the deeper project pages. The full engineering case study — problem, architecture, challenges, theming system, deployment, lessons — is rendered on the page below.\n","date":"2026-08-15","permalink":"http://fahimimam.pro.bd/projects/portfolio-website/","section":"projects","summary":"The portfolio you\u0026rsquo;re reading right now. Hugo + PaperMod with a custom cyberpunk overlay, a live matrix-rain background, and a four-theme design dashboard. See the full engineering case study below.","tags":["Hugo","PaperMod","Vanilla JS","CSS Variables","Nginx"],"title":"Portfolio Website"},{"categories":["Blog","Hugo"],"content":"The portfolio site is served from two domains — fahimimam.pro.bd and fahimimam.sytes.net — from the same public/ directory on the same VPS. Nginx picks the right one by Host header. Same files, no redirects, no per-domain rebuilds.\nThat part works. What didn\u0026rsquo;t work was every visitor getting yanked back to pro.bd on the first click.\nWhat the bug looked like Open https://fahimimam.sytes.net/about/ in a browser. Click any link in the navigation — say, \u0026ldquo;Projects\u0026rdquo;. You land on https://fahimimam.pro.bd/projects/. The mirror is dead weight.\nThis was the symptom. The cause was structural.\nThe cause: every internal link was absolute Hugo has a baseURL setting. The default behavior of absURL (and absLangURL) is to resolve a path against baseURL and emit an absolute URL. PaperMod\u0026rsquo;s templates use these everywhere — header navigation, breadcrumbs, footer, favicon, head icons.\nWhen you serve from one domain, that\u0026rsquo;s correct: every link is a fully-qualified URL pointing at the production host. When you serve from two domains, it\u0026rsquo;s a navigation bug: every link silently redirects visitors to whatever baseURL says, regardless of which domain they came from.\nI had two options:\nBuild twice. One build per domain, each with its own baseURL. Doubles the deploy time, doubles the storage, and the two builds would have to stay in sync. No. Make every internal link host-relative. A single build that works on any domain. Option 2 is the right answer. The fix is small but requires overriding several PaperMod partials.\nThe fix: four partial overrides Hugo\u0026rsquo;s template lookup lets you shadow a theme\u0026rsquo;s template by placing the same path under layouts/. The theme stays untouched; only the partials I want to change get overridden. I copied four PaperMod partials into layouts/partials/ and replaced the absURL/absLangURL calls with their host-relative siblings:\nPartial Was Became header.html logo + main menu use absURL/absLangURL use relURL/relLangURL head.html favicon + apple-touch + mask icons use absURL use relURL footer.html copyright link uses absURL use relURL breadcrumbs.html Home + intermediate crumbs use absLangURL / Permalink use relLangURL / RelPermalink That\u0026rsquo;s the whole change. A single sed-like find-and-replace across four files.\nWhat stays absolute — and why Not every URL on the page should be host-relative. Some need to stay absolute on purpose:\n\u0026lt;link rel=\u0026quot;canonical\u0026quot;\u0026gt; — search engines dedupe cross-domain signals via canonical, and the canonical URL must point at one specific host. RSS / JSON feed links — feed readers fetch them out of band; relative URLs don\u0026rsquo;t work. OpenGraph / Twitter card tags — the OG spec requires absolute URLs. JSON-LD url and @id — schema.org requires absolute URLs. So the rule is: navigation links are relative; metadata links are absolute. Verifying the build obeys this is one grep:\nhugo --minify grep -rh \u0026#39;href=\u0026#34;https://fahimimam\u0026#39; public/ | sort -u The output should be limited to index.xml, the JSON-LD blocks, the OG / Twitter meta tags, and the lunch-tracker demo URLs (those point at the actual deployed demo, not internal navigation). If you see href=\u0026quot;https://fahimimam...\u0026quot; anywhere else, a partial override is missing.\nLessons The lesson isn\u0026rsquo;t \u0026ldquo;Hugo has a bug.\u0026rdquo; The lesson is: default behaviors that don\u0026rsquo;t fail loudly on the happy path are the most expensive ones to discover later. Two-domain serving is a niche requirement, so nobody writes a test for it. The bug lived in the gap between PaperMod\u0026rsquo;s \u0026ldquo;always absolute\u0026rdquo; template and my \u0026ldquo;sometimes multi-domain\u0026rdquo; deployment.\nThe fix isn\u0026rsquo;t worth shipping upstream — Hugo\u0026rsquo;s baseURL model is correct for the 99% case. But the override pattern is general: any time a framework\u0026rsquo;s defaults optimize for the common case and you need different behavior for your case, shadow the partial, not the whole theme. The override is small, surgical, and easy to revert.\n","date":"2026-08-15","permalink":"http://fahimimam.pro.bd/blog/host-locked-navigation/","section":"blog","summary":"Hugo\u0026rsquo;s baseURL makes every internal link absolute. That\u0026rsquo;s fine for one domain. The moment you serve the same site from two domains, it\u0026rsquo;s a navigation bug.","tags":["Hugo","PaperMod","Multi-domain","Bugs"],"title":"The bug that locked every visitor to one domain"},{"categories":["Projects"],"content":"Overview A self-hosted expense tracker built to replace the spreadsheet I had been using for years. The goal is small surface area, fast keyboard-driven input, and charts that answer \u0026ldquo;where did the money go this month?\u0026rdquo; without requiring a SaaS account.\nStack Backend: Go (standard library + chi router) Database: PostgreSQL Frontend: Server-rendered HTML + HTMX — no SPA, no build step Deployment: Single binary on my VPS, reverse-proxied through Caddy Status 🚧 Active development. Core schema and CRUD are in place; charts and CSV import are next.\nPlanned features Add / edit / delete transactions Categories with custom color per category Monthly budget per category with over-budget warnings Charts (spend by category, month-over-month trend) CSV import from bank statements Multi-currency Mobile-friendly quick-add form Why HTMX? The interaction model is \u0026ldquo;submit form, see result.\u0026rdquo; That maps perfectly to server-rendered HTML and HTMX swaps — no JSON API, no React hydration, no state management. The whole app fits in a single binary and the entire stack runs on ~30MB RAM.\nRepository github.com/fahimimam/expense-tracker\n","date":"2026-08-09","permalink":"http://fahimimam.pro.bd/projects/expense-tracker/","section":"projects","summary":"A personal finance tracker for categorizing expenses, setting monthly budgets, and visualizing where the money goes.","tags":["Go","PostgreSQL","HTMX","Docker"],"title":"Expense Tracker"},{"categories":["Blog","DevOps"],"content":"Optimizing VPS Deployment When you\u0026rsquo;re working with a resource-constrained VPS (1 core, 2GB RAM), every megabyte counts. Here\u0026rsquo;s how I optimized my deployment strategy.\nThe Challenge I have a VPS with:\n1 CPU core 2GB RAM 25GB SSD I needed to run:\nPortfolio website Chat application PostgreSQL Redis The Solution: Binary Uploads Instead of using Docker for everything, I decided to:\nBuild locally on my MacBook Air M3 Cross-compile for Linux Upload binaries via SCP Run natively on VPS Build Script #!/bin/bash GOOS=linux GOARCH=amd64 CGO_ENABLED=0 \\ go build -ldflags=\u0026#34;-s -w\u0026#34; -o app-linux main.go The flags -s -w strip the symbol table and DWARF debug info, which typically cuts the binary size by 20–30%.\nWhy this matters A statically-linked Go binary uses ~10MB of RAM at idle. The same application inside a Docker container typically uses 30–60MB once you count the daemon, container layers, and overlay filesystem. On a 2GB box, that difference is the difference between running four services and running one.\nWhen Docker still makes sense For complex multi-service stacks where reproducibility matters more than RAM — staging environments, CI, anything you throw away after a day — Docker is still the right tool. The point isn\u0026rsquo;t to avoid Docker entirely, it\u0026rsquo;s to match the deployment shape to the workload.\n","date":"2024-02-06","permalink":"http://fahimimam.pro.bd/blog/my-first-post/","section":"blog","summary":"How I optimized my VPS deployment strategy to save resources","tags":["DevOps","Go","VPS","Docker"],"title":"Optimizing VPS Deployment: Binary Uploads vs Docker"},{"categories":["Projects"],"content":"A real-time chat application built with Go, featuring WebSocket connections and Redis pub/sub for scalability.\nFeatures Real-time messaging using WebSockets Redis pub/sub for horizontal scaling JWT authentication Message history persistence Typing indicators Tech Stack Backend: Go (Fiber framework) Database: PostgreSQL Cache: Redis Frontend: Vanilla JavaScript Deployment: Docker Compose Challenges \u0026amp; Solutions The main challenge was horizontal scaling. With a single Go process, every WebSocket connection sits in the same memory space and a message can be routed to its recipient in O(1). With multiple Go processes behind a load balancer, two users connected to different nodes cannot message each other directly — they live in separate processes.\nThe fix is Redis pub/sub: each node publishes every outgoing message to a Redis channel and subscribes to incoming messages from the same channel. When a message comes in, the node fans it out to whichever local sockets match the recipient. The result is a chat server that scales horizontally with no application-level awareness of how many nodes exist.\n","date":"2024-01-15","permalink":"http://fahimimam.pro.bd/projects/chat-application/","section":"projects","summary":"Real-time chat built with Go, WebSockets, and Redis pub/sub for horizontal scaling.","tags":["Go","WebSockets","Redis","PostgreSQL"],"title":"Chat Application"}]