No Space on Your ESP32? Ship React Without Hosting React
As a UI dev building on ESP32, I often want a polished interface without rebuilding everything from scratch, like rolling a smaller version of React or a UI component library just to ship something usable. I could skip the fancy schmacy tools the Node.js ecosystem offers, or reach for web components instead, but that shifts focus to infrastructure before the product. React is my bread and butter for everyday UI. It can be overkill on a microcontroller hosted page, but it helps me focus on the issue I'm trying to build. The problem though is storage. Using React on ESP32 means sacrificing flash space on the device. So what if we tried loading third party dependencies from somewhere else like esm.sh instead. Vite leaned on es modules from day one, which made me wonder if build output could rewrite deps to CDN URLs instead of bundling them as assets and hosting them on the microcontroller. Whoever built esm.sh did the hard part.
💡 I have a few comments on microfrontends. People who build microfrontends should definitely start building with ES modules in mind. I think one day we'll move past that technology as module federation is such a complicated and old setup with Webpack. Module Federation through native ES modules should be supported by now. Doing a quick Google search, I believe there is support for that soon, Native Federation, but I'll check it out another day.
The esm.sh approach mirrors an older pattern. You paste a script from a CDN and get jQuery globally in the app. Same copy-n-paste CDN idea as back in the day. With Vite and esm.sh, you get that workflow without globals or manual script tags. Just native ES Modules.
The ESP32 problem
A lot of ESP32 web UIs follow the same pattern. You 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.
The storage budget gets tight quickly once the bundle grows beyond a few kilobytes.
A lot of ESP32 dev boards ship with 4 MB of flash. That flash isn't just 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 usable space is even smaller than the partition size suggests.
React is not the constraint here. It runs in the browser. The constraint is flash. The ESP32 has to store and serve a frontend bundle built for a normal static host. For a settings page, sensor dashboard, provisioning screen, or debug panel, shipping the full dependency graph on the device hits the limit fast.
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 doesn't 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";A real React app shell can sit under 2 KB when the device only hosts your application code. The browser fetches libraries from the CDN at runtime.
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 get rewritten to CDN ESM URLs. Vite dev and HMR still use local node_modules, so development stays normal. You're not giving up the fancy schmacy dev experience.
Why this is useful
This is not only useful for IoT storage. A single index.html is often the simplest distribution format. No zip, no asset folder, just one file to share.
The honest tradeoff
This approach requires network access to esm.sh. For fully offline UIs, bundle dependencies locally and accept the larger flash footprint. Initial load may be slower over CDN, but the ESP32 serves fewer bytes and fewer files, which keeps headroom for interesting things you want to build.
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.