Optimizing Assets
AI coding tools drop videos, PDFs and images straight into public/ and point at them directly, so every visitor downloads multi-megabyte files from your app's own server, uncompressed, with no CDN and often the wrong cache policy. On mobile data, your visitors pay for every one of those megabytes. Here is how to find the heavy files, shrink them, serve them from the right place, and cache them without serving stale copies for a year.
Check your cache headers first. A blanket "immutable" rule is the one problem here that is wrong rather than slow: replace a file at the same URL and returning visitors can keep seeing the old one for up to a year, however often they refresh.
Paste into your AI chat
Copy the whole guide as markdown to use as context in Cursor, Claude, ChatGPT, or any AI tool.
What it looks like
Multi-megabyte files dropped into public/, an autoplaying video, and one blanket cache rule for everything.
Everything straight into public/
A typical AI-built marketing site or portfolio ends up with raw camera exports, uncompressed PDFs and a full-size video sitting next to each other, each referenced by a literal filename.
public/
├── about-hero-video.mp4 # 12 MB, autoplays on page load
├── brochure-2026.pdf # 8.1 MB
├── prospectus.pdf # 8.4 MB
├── team-photo.jpg # 3.2 MB, straight off a phone camera
└── logo.png # 400 KB (should be an SVG)A video that downloads the moment the page mounts
Nothing has been compressed and there's no poster image. The browser starts pulling 12 MB straight away, from the same server that's also sending your JavaScript and answering your API calls.
// AboutPage.tsx
<video autoPlay loop muted playsInline src="/about-hero-video.mp4" />One cache rule for everything
That one rule hides two separate problems: the files are too big to begin with, and once they're cached, they're cached wrong.
// vercel.json (the bad version)
{
"headers": [
{
"source": "/(.*)",
"headers": [
{ "key": "Cache-Control", "value": "public, max-age=31536000, immutable" }
]
}
]
}Why AI tools generate this
Dropping a file in public/ is the simplest thing that works on localhost, and an immutable caching snippet looks like best practice.
What it costs
Worse Core Web Vitals, visitors paying for your files out of their data allowance, and stale files served for a year.
How to measure it in your app
Find the big files on disk, check the headers production really sends, then confirm in DevTools and Lighthouse.
The fix
Compress first, load the hero video after the page, size and lazy-load images, move big media to a CDN, and scope immutable caching to hashed files.
A representative before and after
Illustrative numbers for the About page above, not one specific audited project.
| What loads | Before | After |
|---|---|---|
| Hero video | 12 MB, downloading from the first moment | ~60 KB poster first; ~1.5 MB video after the page has loaded, none on Save-Data |
| Team photo | 3.2 MB JPEG straight off a phone | ~180 KB WebP at 1600px wide |
| Logo | 400 KB PNG | ~4 KB SVG |
| PDFs (only when clicked) | 16.5 MB from your app's server | ~3-4 MB, compressed, from a CDN |
| Downloaded before the page finishes loading | ~15.6 MB | ~250 KB |
The PDFs only cost anything when someone clicks them, but the hero video, photo and logo are paid for by every visitor on every first visit. That is where to start.
Checklist
Work through this before you call your static assets done.
Audit
- find public -size +500k comes back empty, or every hit is justified
- The whole static folder is in the low tens of MB, not hundreds
- curl -I on a real unhashed file in production shows no immutable header
Fix
- Videos, PDFs and images are compressed, and images are WebP or AVIF at sensible dimensions
- Logos and icons are SVG
- The hero video shows a poster first and loads after the page, never on Save-Data
- Images below the fold have width, height and loading="lazy"; the top image doesn't
- Large media and downloads come from object storage plus a CDN, not the app's server
- Cache-Control: immutable appears only on content-hashed build paths
Verify
- Lighthouse no longer flags image delivery, cache lifetimes or enormous payloads
- The Network tab on Slow 4G shows the page usable before any large file downloads
- Re-check whenever someone adds media, since every new file dropped in public/ starts the problem again
Earlier in this track
Media is the heaviest download. JavaScript is usually the next one.
Stop your landing page downloading your admin dashboard: route-level code splitting, done fully instead of halfway.
Want the steps as context for your AI coding assistant?