9.7 KiB
Frontend Technology Choices for ESP32-S3 Provider
Authored by Claude Opus 4.6 Date: 2026-03-02
1. Goal
Create a lightweight web frontend for the ESP32-S3 Provider that displays system information and provides a reboot control. The frontend must compile to static files small enough to live in ESP32 flash memory.
2. Chosen Stack
| Technology | Version | Role |
|---|---|---|
| Svelte | 5 | UI framework (compiles away at build time) |
| Vite | Latest | Build tool and dev server |
| TailwindCSS | 4 | Utility-first CSS (JIT, tree-shaken at build time) |
| vite-plugin-singlefile | Latest | Inlines all JS+CSS into one HTML file |
| gzip | — | Compresses final output for ESP32 flash |
3. Why Svelte Over Other Frameworks
Svelte is a compile-time framework. Unlike React or Vue, it does not ship a runtime to the browser. The compiler transforms .svelte components into highly optimized vanilla JavaScript at build time. This means:
- No virtual DOM overhead — direct DOM manipulation in the compiled output.
- Tiny bundle size — a simple Svelte app compiles to a few kilobytes of JS, compared to React's ~40kB runtime or Vue's ~30kB.
- Ideal for constrained devices — the ESP32-S3 has limited flash (typically 4-16MB shared with firmware) and serves files over a potentially slow connection.
Other frameworks considered:
| Framework | Runtime Size | Why Not |
|---|---|---|
| React | ~42kB min+gzip | Ships a runtime; too heavy for ESP32 |
| Vue | ~33kB min+gzip | Ships a runtime; unnecessary for a simple dashboard |
| Vanilla JS | 0kB | No tooling, harder to maintain, no component model |
| Svelte | ~2kB min+gzip | ✅ Chosen — compiles away, smallest output |
Vanilla JS was considered but Svelte provides a component model, reactivity, and maintainability with effectively zero runtime cost.
4. Why TailwindCSS Over Vanilla CSS
TailwindCSS v4 uses a Just-In-Time (JIT) compiler that scans source files and generates only the CSS classes actually used. For a simple dashboard page, this produces under 5kB of CSS (before gzip).
Benefits over vanilla CSS:
- Guaranteed small output — unused styles are never generated (no manual purging needed).
- Faster development — utility classes directly in markup, no switching between files.
- Consistent design system — spacing, colors, typography are constrained to a scale.
- Dark mode — built-in
dark:variant with zero extra effort.
TailwindCSS is a build-time tool only. It produces a plain .css file — no runtime, no JavaScript.
5. Why Single-File Build
The vite-plugin-singlefile plugin inlines all compiled JavaScript and CSS directly into index.html. The ESP32 serves one file instead of multiple.
Benefits for ESP32:
- One HTTP request — avoids multiple round-trips over a slow embedded server.
- No filesystem overhead — SPIFFS/LittleFS have per-file overhead; one file is optimal.
- No filename length issues — LittleFS has a 32-character filename limit; Vite normally generates hashed filenames like
assets/index-a1b2c3d4.jsthat can exceed this. - Simpler backend — the HTTP server only needs one route to serve the frontend.
6. Why Not ESP32-SvelteKit
ESP32-SvelteKit is a full-featured framework that bundles SvelteKit + TailwindCSS + DaisyUI with an ESP32 backend. It was evaluated and rejected for the following reasons:
Incompatible Build System
ESP32-SvelteKit requires PlatformIO with the Arduino framework. Our project uses native ESP-IDF with CMake. Adopting ESP32-SvelteKit would require rewriting all backend code (connect.cpp, led_status.cpp, main.cpp) to use Arduino APIs. This is not acceptable.
Unnecessary Complexity
ESP32-SvelteKit includes features we do not need:
- MQTT client
- JWT-based user authentication
- WiFi provisioning UI (we already have our own wifi/ethernet logic in
connect.cpp) - DaisyUI theming system
- PsychicHttp web server
- ArduinoJson
These add weight to both the firmware and the frontend bundle without benefit for our use case (a simple system info dashboard).
Licensing Concern
The ESP32-SvelteKit backend code is licensed under LGPL v3, which has copyleft implications for linked code. Our custom setup has no such constraints.
What We Borrow From It
Although we don't use the framework, we adopt the same proven patterns:
- Svelte + TailwindCSS + Vite as the frontend toolchain
- Gzipped static files served from ESP32 flash
- The concept of embedding frontend into firmware binary for OTA (future task)
7. Build Pipeline
Source Files Build Steps Output
───────────── ─────────── ──────
src/App.svelte ─┐
src/app.css ├─→ Vite + Svelte Compiler ─→ dist/index.html (47kB)
src/lib/api.js ─┘ + TailwindCSS JIT │
+ vite-plugin-singlefile ▼
gzip compression (level 9)
│
▼
dist/index.html.gz (16kB)
│
▼
ESP32 Flash (SPIFFS/LittleFS)
or embedded in firmware binary
8. Environment-Based API URL
The frontend calls the ESP32's REST API. The base URL depends on the environment:
| Environment | VITE_API_BASE |
Reason |
|---|---|---|
| Development (PC) | http://<ESP32_IP> |
Frontend on PC, API on ESP32 over network |
| Production (ESP32) | (empty string) | Frontend and API on same device, relative URLs |
This is handled via Vite's .env.development and .env.production files. The value is baked in at compile time — zero runtime overhead.
9. OTA Considerations (Future)
When OTA updates are implemented, the frontend will be embedded into the firmware binary as a C header array:
npm run build:esp32→dist/index.html.gz(~16kB)- A script converts the gzipped file to a C
const uint8_t[]array - The array is compiled into the firmware binary
- OTA flashes one binary that includes both firmware and frontend
This avoids needing a separate SPIFFS partition for the frontend and ensures the UI always matches the firmware version.
10. Summary
We chose Svelte + TailwindCSS + Vite because they are build-time tools that produce the smallest possible static output, ideal for ESP32 constraints. We build to a single gzipped HTML file of ~16kB. We did not adopt ESP32-SvelteKit because it requires PlatformIO/Arduino, which is incompatible with our native ESP-IDF project, and includes unnecessary complexity for our simple dashboard.
11. Implementation Results
Added 2026-03-02 after implementation was completed.
Final Build Output
| File | Size | Notes |
|---|---|---|
dist/index.html |
47.0 kB | Single self-contained file (all JS + CSS inlined) |
dist/index.html.gz |
16.1 kB | Gzipped at max level 9 (65% reduction) |
Size Breakdown (before inlining)
| Asset | Uncompressed | Gzipped |
|---|---|---|
| HTML shell | 0.4 kB | 0.3 kB |
| CSS (TailwindCSS JIT) | 12.4 kB | 3.2 kB |
| JS (Svelte compiled) | 34.9 kB | 13.5 kB |
| Total | 47.7 kB | 17.0 kB |
The singlefile plugin reduces total size by ~0.8kB (removes multi-file boilerplate). Its main value is operational (one file, one HTTP request, no filesystem overhead).
File Structure
Provider/frontend/
├── .env.development # VITE_API_BASE=http://ESP32_IP_HERE
├── .env.production # VITE_API_BASE= (empty, same-origin)
├── index.html # Vite entry point
├── package.json # npm scripts: dev, build, build:esp32
├── vite.config.js # Svelte + TailwindCSS + singlefile plugins
├── scripts/
│ └── gzip.js # Node.js gzip script (built-in zlib, no deps)
└── src/
├── main.js # Svelte mount point
├── app.css # TailwindCSS v4 imports + @theme tokens
├── App.svelte # Dashboard UI (system info, reboot button)
└── lib/
└── api.js # API helpers (getSystemInfo, reboot)
Dashboard Features
- System info display: chip model, free heap, uptime, firmware version, connection type
- Reboot button with confirmation modal
- Auto-refresh polling every 5 seconds
- Four status states: loading, connected, offline, rebooting
- Dark theme with custom color tokens
- Fully responsive layout
npm Scripts
| Script | Command | Purpose |
|---|---|---|
npm run dev |
vite |
Dev server with HMR (for PC development) |
npm run build |
vite build |
Production build (single index.html) |
npm run build:esp32 |
vite build && node scripts/gzip.js |
Build + gzip for ESP32 deployment |
Versions Used
| Package | Version |
|---|---|
| Svelte | 5.45.2 |
| Vite | 7.3.1 |
| TailwindCSS | 4.2.1 |
| @tailwindcss/vite | 4.2.1 |
| vite-plugin-singlefile | 2.3.0 |
Known Issues
- W: drive: Vite requires
resolve.preserveSymlinks: trueinvite.config.jsbecauseW:is asubstdrive mapped toC:\Dev\.... Without this, the build fails withfileNamepath resolution errors. - ESP32 backend not yet implemented: The frontend expects
GET /api/system/infoandPOST /api/system/rebootendpoints. These need to be added tomain.cppusingesp_http_server.