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.

text
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.

tsx
// 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.

json
// 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 loadsBeforeAfter
Hero video12 MB, downloading from the first moment~60 KB poster first; ~1.5 MB video after the page has loaded, none on Save-Data
Team photo3.2 MB JPEG straight off a phone~180 KB WebP at 1600px wide
Logo400 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.

Ship Less JavaScript

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?