When I started building Snappy-Fix — both the online tools platform and the admin dashboard — I had one clear goal in mind: every interaction must feel instant. No spinners where they can be avoided. No layout shifts. No sluggish tables. No freezing editors. In the world of online tools and content management, speed is not a feature — it is the product itself.
This article is the honest breakdown of how I approached performance across the entire Snappy-Fix stack: Next.js on the frontend, FastAPI on the tools backend, and a Go-powered API serving the admin dashboard. The mistakes I made, the strategies that worked, and the architectural decisions that transformed a slow prototype into a platform that feels genuinely snappy — by name and by experience.

The Reality Check: What Was Breaking Early
Before solutions, the honest picture of what the initial builds looked like.
Frontend Problems
UI freezing during file processing operations
The TipTap editor is causing hydration mismatches and 500 errors on first load
The admin blog table is loading the entire
News[]array client-side — all records, all at onceLayout shift on image-heavy pages is destroying Core Web Vitals scores
Marketing site assets bleeding into the admin bundle — global navigation, tracking scripts, heavy footers all loading for authenticated admin users
Backend Problems
File uploads load entire files into server memory before processing begins
Blocking synchronous processing on the FastAPI tools backend is causing request queues
No cleanup of temporary files — disk space accumulating silently
API response times on the Go backend inconsistent under concurrent admin load
The root cause of almost every problem was the same: doing too much, too early, for too many. Loading all data when only a slice was needed. Rendering on the server what only the client could know. Sending the entire bundle when only one route was loaded. The fixes, once the pattern was clear, followed naturally.
Pillar One: Structural Efficiency — Decoupling the Admin From Everything Else
The single architectural decision with the highest performance return in the Next.js frontend was route grouping.
Without route groups, the admin dashboard shared its layout with the marketing site. Every admin page load triggered parsing of the global navigation, the marketing footer, the homepage hero fonts, and every tracking script attached to the public site. None of these has any business being in memory while an admin is editing a blog post.
The fix:
app/
├── (marketing)/ ← Marketing layout — fonts, nav, footer, analytics
│ ├── page.tsx
│ ├── blog/
│ └── tools/
├── (admin)/ ← Admin layout — sidebar, topbar only. Zero marketing overhead
│ ├── layout.tsx ← AdminShell lives here exclusively
│ ├── blog/
│ └── categories/
What Route Isolation Achieved
Before Route Groups | After Route Groups |
|---|---|
Admin loads marketing fonts | Admin loads only admin fonts |
Global analytics scripts on every admin page | Analytics only on public routes |
Full navigation re-renders between admin pages | Sidebar and topbar persist, only content re-renders |
Shared bundle — 340KB+ JS on admin routes | Isolated bundle — 180KB on admin routes |
Navigating between "Blog Posts" and "Categories" in the admin no longer triggers a full re-render of the shell. The AdminShell — sidebar, topbar, breadcrumb — is mounted once and persists. Only the route content slot updates. This is Next.js App Router's nested layout system working exactly as designed, but only if you deliberately isolate your layout groups.
Pillar Two: Solving the TipTap Hydration Bottleneck
The TipTap rich text editor is the most complex component in the Snappy-Fix admin. It is also the one that caused the most performance and stability issues before it was handled correctly.
The initial implementation mounted TipTap directly in a Server Component context. The result: hydration mismatches, 500 errors on first load, and the main thread locking up during the initial paint as the browser tried to reconcile the server-rendered editor shell with the client-side editor reality.
The Three-Part TipTap Fix
1. immediatelyRender: false The most impactful single-line fix. Setting immediatelyRender: false in the useEditor hook tells TipTap not to attempt a server-side render at all. The shell loads instantly. The editor mounts cleanly once the client environment is available.
2. Conditional client-only mounting
const [mounted, setMounted] = useState(false)
useEffect(() => setMounted(true), [])
if (!mounted) return <EditorSkeleton />
return <TipTapEditor />
The skeleton placeholder eliminates layout shift during mount. The editor appears to load instantly because something is always visible — the skeleton during SSR, the real editor immediately after hydration.
3. Dynamic import with ssr: false
const RichEditor = dynamic(
() => import('@/components/editor/RichEditor'),
{ ssr: false, loading: () => <EditorSkeleton /> }
)
This removes the editor's JavaScript entirely from the server-rendered HTML. The initial page load is a fraction of the size. The editor JS downloads in the background and hydrates silently.
Pillar Three: Server-Side Pagination — Killing the Table Bloat
Admin panels are notorious for what I call Table Bloat — fetching every record from the database on page load and filtering, sorting, and paginating client-side. It works at 50 records. It fails at 500. It crashes at 5,000.
The Snappy-Fix blog listing initially fetched the entire News[] array. As the content library grew, load times grew proportionally.
The migration to server-side pagination was the highest-impact backend change in the admin dashboard.
The Pagination Architecture
// Every admin list query follows this contract
const params: Record<string, any> = {
page: currentPage,
limit: limit, // 10 | 20 | 50 — user controlled
}
if (search) params.search = search
if (category_id) params.category_id = category_id
if (is_featured) params.is_featured = true
if (is_exclusive) params.is_exclusive = true
The backend returns a pagination tuple [PaginationMeta, totalCount] on every response. The frontend renders only the current page slice — never the full dataset.
Impact of Server-Side Pagination
Metric | Client-Side Pagination | Server-Side Pagination |
|---|---|---|
Initial data transfer | All records (~2–8MB) | One page (~20–80KB) |
Time to first render | 3–6 seconds | Under 500ms |
Memory usage | Grows with content volume | Constant |
Search performance | Client JS filtering | Database index search |
Works on 10,000 records | ❌ | ✅ |
Pillar Four: File Handling and Backend Processing
The tools platform — image converters, PDF processors, optimisers — presented the most technically demanding performance challenges. Files are large, processing is CPU-intensive, and users expect results in seconds.
The File Handling Overhaul
Streaming uploads instead of memory loading: The initial implementation loaded the entire uploaded file into server memory before processing began. For a 10MB image, this meant 10MB of RAM allocated per concurrent user, multiplied by every user uploading simultaneously.
Streaming processes the file as bytes arrive — memory usage stays constant regardless of file size.
Async background processing: Heavy operations moved off the request thread entirely:
Operation | Before | After |
|---|---|---|
Image compression | Blocking — the user waits | Async — immediate acknowledgement |
PDF generation | Blocking — timeout risk | Background task — progress polling |
Format conversion | Synchronous | Non-blocking async handler |
File cleanup | Never — disk accumulation | Auto-cleanup after delivery |
File size limits and early validation: Client-side validation rejects invalid file types and oversized files before they reach the server. This eliminates a whole category of server round-trips — the fastest API call is the one that never happens.
Pillar Five: Asset Management and CDN Delivery
With a content platform handling blog thumbnails, gallery images, and tool output files, unmanaged assets are a silent performance killer.
The Asset Strategy
Cloudinary for all media — automatic WebP conversion, responsive
srcsetgeneration, and CDN edge delivery from nodes closest to the usernext/imageeverywhere — automatic lazy loading, explicit dimensions preventing CLS, andpriorityflag on above-the-fold images onlyLow-fidelity placeholders in the admin update view — the blog thumbnail preview loads a blurred placeholder while the full image fetches
Drag-and-drop client validation — file type and size validated in the browser before upload begins, preventing wasted server requests
Image Optimisation Impact
Metric | Unoptimised | Optimised |
|---|---|---|
Average image weight | 800KB–2MB | 80–200KB |
Format | JPEG/PNG | WebP/AVIF |
Layout shift (CLS) | 0.18 — failing | 0.04 — passing |
LCP on blog pages | 4.2s | 1.6s |
Pillar Six: Memory Management and Reliability Over Time
Performance is not just about speed — it is about reliability over time. A dashboard that starts fast and degrades over a long editing session is not performant.
Two patterns prevented memory accumulation in the Snappy-Fix admin:
Effect cleanup on every subscription:
useEffect(() => {
const handler = () => setSidebarOpen(false)
window.addEventListener('resize', handler)
return () => window.removeEventListener('resize', handler)
}, [])
Every addEventListener In the mobile sidebar, every resize observer, every interval-based polling function has a corresponding cleanup in the useEffect return. Without this, every component mount adds a listener and nothing removes it — memory grows until the tab is refreshed.
beforeunload warning on unsaved editor changes: The blog editor warns users before navigating away with unsaved content. Beyond the UX benefit, the beforeunload listener itself is cleaned up correctly on component unmount, preventing the ghost-listener accumulation that causes subtle memory leaks in long-running sessions.
Before vs After: The Full Platform Picture
Metric | Before Optimisation | After Optimisation |
|---|---|---|
Admin initial bundle | ~340KB JS | ~180KB JS |
Blog list load time | 3–6 seconds | Under 500ms |
TipTap editor mount | Hydration errors | Clean, instant |
Tools page load | 4–6 seconds | Under 2 seconds |
Image weight (average) | 800KB+ | Under 200KB |
CLS score | 0.18 (failing) | 0.04 (passing) |
LCP score | 4.2 seconds | 1.6 seconds |
Memory after 1hr session | Growing | Stable |
The Lessons That Cost the Most to Learn
Never do on the client what the server can do before the response arrives
Never load all records when you will display ten
Never mount a complex editor without isolating it from SSR
Route groups are not a cosmetic choice — they are a performance boundary
File handling is always the bottleneck — validate early, stream always, clean up immediately
Mobile users are the majority — test on real devices, not DevTools simulation
Every
addEventListenerWithout a cleanup is a future memory leak


