No Space on Your ESP32? Ship React Without Hosting React
I built vite-plugin-tiny-spa for a very specific kind of problem. You want a nice React UI for an ESP32 dashboard, and/or tiny device admin page, but you do not want the device to bundle all of React, or entire UI library because you just have a few kilobytes of space to work with.
The idea is simple, let the ESP32 serve one tiny index.html, your code, and let the browser fetch dependencies from a CDN.
It feels new because it uses Vite and ES Modules. It feels old because it brings back the copy-n-paste-CDN energy of the jQuery era, when you could grab a hosted library URL, drop it into a page, and move on.
With todays modern development, everyone builds and hosts their own assets - that kinda bloats the internet. What if we loaded off some dependencies from a shared disk on the internet?
The ESP32 problem
A lot of ESP32 web UIs follow the same pattern, build a frontend, copy the output into SPIFFS or LittleFS, mount the filesystem, then serve index.html and static assets from the device. That works, and posts like React on the ESP32 with PlatformIO show the pattern clearly.
But the storage budget gets awkward fast.
Many ESP32 dev boards ship with 4 MB of flash. That flash is not only for your web app; it also has to cover firmware, partitions, OTA space if you use it, filesystem overhead, and everything else your device needs. Espressif's SPIFFS guide also notes that SPIFFS reliably uses only about 75 percent of the assigned partition.
So the problem is not “React cannot run with an ESP32.” React runs in the browser. The problem is making the ESP32 store and serve a frontend bundle that was designed for a small static host app. If your UI is a small settings page, sensor dashboard, provisioning screen, or debug panel, hosting the whole dependency graph on-device can feel silly.
The old web already knew this trick
Back in the jQuery days, you could open Google Hosted Libraries, copy a script tag, and use a shared CDN-hosted library instead of putting the file on your own server.
vite-plugin-tiny-spa is the modern version of that idea:
- You still develop with Vite.
- You still write normal app code.
- You still use npm dependencies.
- But production could emit one
index.html. - Big dependencies import from
esm.shinstead of living on the ESP32.
Not globals. Not manual script-tag. Just native ESM imports pointed at a CDN. This feature has long been supported when Vite was created.
How small can it get?
For a tiny React counter app, I measured a build output of **1,644 bytes** for a single index.html. It renders a React app, but it does not contain React. The generated HTML imports React and React DOM from esm.sh:
js
import React from "https://esm.sh/react@19.2.7?target=es2022";
import { createRoot } from "https://esm.sh/react-dom@19.2.7/client?target=es2022";So yes, a real React app shell can be comfortably run under 2 KB when the device is only responsible for your app code. The browser downloads any library you wish to use from the CDN.
Using vite-plugin-tiny-spa
Install it:
sh
npm install -D vite-plugin-tiny-spaAdd it to Vite:
ts
import { defineConfig } from "vite";
import { tinySpa } from "vite-plugin-tiny-spa";
export default defineConfig({
plugins: [tinySpa()],
});Build your app, upload the generated index.html to the ESP32 filesystem, and serve it as text/html. The plugin folds scripts, styles, and assets into the HTML file. Dependencies you choose are rewritten to CDN ESM URLs. Vite dev and HMR still use local node_modules, so development stays normal.
Why this is useful
This is especially nice for:
- ESP32 dashboards
- Wi-Fi provisioning pages
- captive portals
- tiny device admin panels
- one-file prototypes
- diagnostic tools you want to email or attach to an issue
It is not only about IoT storage, though. Sometimes one index.html is just the best distribution format. No zip file. No asset folder. Just send the file over to your colleague.
The honest tradeoff
This approach assumes the browser can reach esm.sh. If your device UI must work fully offline, bundle the dependencies instead and accept the larger file. Modern browsers partition caches for privacy and security, so the old “everyone shares the same jQuery cache” story is weaker now. In addition, your website might load a bit slower but provided they get a better user experience. The practical win is that your ESP32 serves fewer bytes and fewer files.
Why I made it
I like React. I like Vite. I like the internet of things.
vite-plugin-tiny-spa is my bridge between those worlds.
Build with modern tools. Ship like it is 2009. Keep dependencies off your ESP32.