DEV Community

Tristan Kilhwan Chai
Tristan Kilhwan Chai

Posted on

[Homelab AI Project] Ep.6 - Aiming for a 0MB Memory Footprint on Frontend Servers with Astro and React Islands

1. Background: "Zero RAM Left to Waste on Frontend Servers"

After establishing our Gitea-based CI/CD pipeline in Episode 5, it was time to build the frontend dashboard to display our AI news pipeline stats.

For modern web apps, the default tech stack is almost always Next.js (App Router). While Next.js provides excellent developer velocity, it suffers from a massive architectural drawback when deployed on hardware-constrained homelabs:

  • Persistent Node.js Runtime Footprint: Running a Next.js app in SSR/ISR mode requires a continuous Node.js server instance running in the background. This instantly swallows 150MB to 300MB of idle RAM.
  • The Threat of the OOM Killer: In our homelab (Ryzen 5500U, 16GB RAM), memory must be preserved for PostgreSQL page caches, Redis queues, and intensive Python/RAG embedding operations. Wasting 200MB+ on a frontend server would invite out-of-memory crashes.

What about standard React SPAs (e.g., Vite builds)? They compile to static assets, allowing Nginx static serving (0MB constant runtime RAM). However, they enforce Client-Side Rendering (CSR), meaning users must download hundreds of kilobytes of JavaScript before anything is painted, leading to sluggish LCP and poor SEO crawlers indexation.

We needed a system that shipped close to 0 bytes of initial JavaScript, loaded instantly, scored 100/100 on Google Lighthouse SEO, and still let us build interactive charts using the rich React ecosystem.

This is why we chose Astro and React's Island Architecture.


2. The Solution: Island Architecture (Astro + React)

Astro is a static-first framework. At build time, it processes components and outputs pure static HTML/CSS. Since we serve these files using Nginx, we allocate 0MB constant runtime memory for a Node.js frontend process.

To support dynamic interfaces (like our analytics charts), we use Island Architecture.

graph TD
    subgraph HTML_Page["Astro Static Page (HTML / No JS)"]
        Hero["Hero Section (Static HTML)"]
        Arch["Architecture Diagram (Static SVG/HTML)"]

        subgraph React_Island_1["React Island (Hydrated)"]
            ChartComponent["Live Demo Chart (React + Chart.js)"]
        end

        subgraph React_Island_2["React Island (Hydrated)"]
            LogViewer["Live Log Streamer (React)"]
        end

        Journal["Journal Cards (Static HTML)"]
    end

    HTML_Page --> Hero
    HTML_Page --> Arch
    HTML_Page --> React_Island_1
    HTML_Page --> React_Island_2
    HTML_Page --> Journal

    style React_Island_1 fill:#61dafb,stroke:#333,stroke-width:2px
    style React_Island_2 fill:#61dafb,stroke:#333,stroke-width:2px
Enter fullscreen mode Exit fullscreen mode

The layout and static descriptions render instantly as plain HTML. Interactive widgets are marked as independent Islands (e.g. <LiveDemoChart client:visible />). The browser only requests the React bundle and hydrating JavaScript when the scroll position hits the visible chart viewport, bypassing massive initial payloads.


3. Implementation: Packages & Configuration

(1) Package Directory Layout

To scale into a monorepo later, we organized the frontend code under packages/frontend/:

# packages/frontend/
├── public/
├── src/
│   ├── components/       # Astro & React Components
│   │   ├── Header.astro
│   │   ├── Hero.astro
│   │   └── LiveDemoChart.jsx (React)
│   ├── layouts/          # Layout Templates
│   │   └── Layout.astro
│   ├── pages/            # File-based Routing
│   │   └── index.astro
│   └── styles/           # Vanilla CSS Styling
│       └── global.css
├── astro.config.mjs
├── package.json
└── tsconfig.json
Enter fullscreen mode Exit fullscreen mode

(2) Astro Configuration (astro.config.mjs)

We enabled the React integration and enforced strict static file compilation:

import { defineConfig } from 'astro/config';
import react from '@astrojs/react';

export default defineConfig({
  integrations: [react()],
  output: 'static', // Forces build-time static HTML file generation
  build: {
    format: 'file', // Generates concrete physical files (index.html)
  }
});
Enter fullscreen mode Exit fullscreen mode

(3) Lightweight CSS Design Tokens (global.css)

To keep builds clean and eliminate dependencies, we shunned CSS frameworks (like Tailwind) and built a custom dark theme using CSS variables:

:root {
  --font-sans: 'Outfit', 'Inter', -apple-system, sans-serif;

  /* Harmonious Dark Palette */
  --bg-primary: hsl(224, 25%, 12%);
  --bg-secondary: hsl(224, 25%, 16%);
  --bg-tertiary: hsl(224, 25%, 20%);
  --border-color: hsl(224, 15%, 25%);

  --text-primary: hsl(210, 40%, 98%);
  --text-secondary: hsl(215, 20%, 75%);

  /* Accent Colors */
  --accent-cyan: hsl(180, 100%, 50%);
  --accent-purple: hsl(270, 100%, 65%);
  --gradient-accent: linear-gradient(135deg, var(--accent-cyan), var(--accent-purple));

  --shadow-lg: 0 10px 30px rgba(0, 0, 0, 0.5);
  --transition-smooth: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}

body {
  background-color: var(--bg-primary);
  color: var(--text-primary);
  font-family: var(--font-sans);
  margin: 0;
  padding: 0;
  line-height: 1.6;
}

.card-glass {
  background: rgba(36, 44, 60, 0.6);
  backdrop-filter: blur(12px);
  border: 1px solid var(--border-color);
  border-radius: 12px;
  box-shadow: var(--shadow-lg);
  transition: var(--transition-smooth);
}

.card-glass:hover {
  border-color: var(--accent-cyan);
  transform: translateY(-2px);
}
Enter fullscreen mode Exit fullscreen mode

4. Troubleshooting & Debugging logs

🚨 Issue 1: window is not defined Build Failure

Compiling the project with npm run build failed instantly with a compilation error:
ReferenceError: window is not defined inside the React chart component.

  • Cause: Astro attempts to prerender all components on the server side (SSR) to spit out clean HTML during compilation. Because Node.js lacks browser globals like window or document, libraries parsing canvas dimensions crash during static rendering.
  • Resolution: Force Astro to skip SSR compilation for client-dependent modules by applying the client:only="react" directive:
  <!-- Bypasses server-side build render; mounts directly in the browser -->
  <LiveDemoChart client:only="react" />
Enter fullscreen mode Exit fullscreen mode

Alternatively, wrapping browser API declarations inside React's useEffect hook ensures code only runs after hydration.

🚨 Issue 2: Style Flash (FOUC) during Page Reloads

Refreshing the dashboard caused a flash of unstyled content (FOUC)—showing raw black-on-white text for ~100ms before snapping to our dark theme.

  • Cause: Styles scoped within React Islands were asynchronously loaded and parsed only after the React bundles hydrated.
  • Resolution: Decoupled the styling from React component scopes. Imported global styles (global.css) directly in the static Astro layout header (Layout.astro), allowing the browser to parse all CSS classes instantly alongside the initial HTML stream.

5. Results & Metrics: Next.js vs. Astro

Here are the performance metrics compared to a default Next.js (App Router) project:

Metric Next.js (App Router, blank page) Astro (Static + 1 React Island) Improvement
Server RAM Footprint 240 MiB 0 MiB (Static Nginx serving) -100% (No runtime node)
Initial JS Size ~82.4 kB 7.2 kB (Excluding React core) -91.2%
Lighthouse Performance 88 / 100 100 / 100 +12 points
TTI (Time to Interactive) 0.8s 0.15s -81.2%

By using Astro, we saved 240MB of memory on our server and reduced the initial JS footprint down to a mere 7.2KB, securing a perfect 100/100 Lighthouse Performance score even on lower-end hardware configurations.


6. Next Up

With our frontend skeleton running efficiently, Episode 7 shifts focus to our core codebase. We will walk through configuring our monorepo architecture: Monorepo Structure Design — Decoupling Node.js & Python modules and sharing core modules.

Top comments (0)