The Trap
Every client site I build runs SolidStart with the cloudflare-module preset on the Cloudflare Workers free tier, which caps the deployed bundle at 3 MB. (The older cloudflare-pages preset was deprecated in April 2025; this is the supported path.)
Three megabytes sounds like plenty for a server bundle, and it is, until you learn what Nitro does with public/.
Nitro bundles images it finds in public/ as base64 strings inside .mjs server chunks. Your hero photo, your gallery shots, your team headshots all get inlined into the server bundle instead of shipping as static assets, and the bundle quietly balloons past 3 MB. The build itself succeeds locally; the deploy is what dies.
The worst single offender in my own fleet was a 5.86 MB headshot.png sitting in the brand site's public/ -- one file, nearly double the entire bundle budget by itself. It's gone now, and the site's remaining public/ rasters total around 359 KB.
Why Auto-Deploy Makes It Worse
On sites I deploy manually, a blown bundle fails loudly at my terminal and I deal with it on the spot. The brand site is different: the repo is connected to Cloudflare Workers Builds, so a push to main auto-builds and deploys with nobody watching. Commit a multi-MB raster and the Cloudflare build fails silently. The merge looks fine, CI is green on the repo side, and the live site just never picks up the change. You find out later, if you find out at all.
That asymmetry is what pushed this from "annoying build quirk" to "needs a standing rule."
The Workaround
Every client project carries a scripts/deploy.sh that brackets the build:
- Move images out of
public/(to a temp directory) before the build, so Nitro never sees them. - Run the Vinxi/Nitro build. The server bundle stays small.
- Copy the images into
.output/public/after the build, where they're served as plain static assets. - Move the images back into
public/so local dev still works. - Check the server bundle size against the 3 MB limit and fail the deploy if it's over.
Step 5 matters as much as the move itself. The size check converts a silent failure into a loud one, which is the whole game with this class of bug.
This is a workaround rather than a fix, and I'll own that. The honest trade-off is a moving-parts deploy script where npm run build && wrangler deploy should suffice, plus a window during the build where public/ is temporarily empty. If you Ctrl-C mid-build, the restore step never runs and your images are sitting in a temp directory. I accept that because the alternative is auditing Nitro's chunking behavior on every framework upgrade.
The Prevention Rule
The deploy script protects manual deploys, but the auto-deployed brand site never runs it, so prevention has to happen before commit: every raster gets converted to WebP first, no exceptions. For the Selected Work thumbnails the pipeline is:
sips -z 815 1280 in.png --out t.png
cwebp -q 82 -m 6 -sharp_yuv t.png -o out.webp
Resize with sips, then encode with cwebp at quality 82. The output lands at a small fraction of the source PNG's size, and keeping the total public/ raster weight in the hundreds of kilobytes means even base64 inflation can't reach the limit.
The rule costs one command per image and removes the only way the auto-deploy pipeline can fail without telling me. A 359 KB public/ directory is safe under any bundling behavior; a single forgotten PNG export is not.