Building and Deploying Vite Assets

+15 Mana ✨

Introduction

The dev server is great for local work, but production needs hashed, minified, cache-friendly bundles served by your web server or a CDN. This lesson walks through the production build pipeline and the pieces you need to get right before shipping.

Key Concepts

  • Build output: The static files emitted by npm run build, written to public/build/.
  • Manifest: A JSON file that maps source entry points (like resources/js/app.js) to their hashed production filenames. Laravel's @vite directive reads this to generate the correct <script> and <link> tags.
  • Cache busting: Each build produces filenames with content hashes (app-DfJk2XMz.js), so the browser's cache is invalidated automatically when the code changes.
  • Asset URL: The public URL prefix from which assets are served — often the app origin, sometimes a CDN.

Real World Context

If you deploy without running npm run build, Laravel will try to connect to the Vite dev server at localhost:5173 — users see broken pages. Every deploy pipeline should build assets in CI and ship the public/build/ output alongside the PHP files.

Deep Dive

Running the Production Build

bash
npm ci          # Install pinned dependencies
npm run build   # Emits public/build/*

The output looks like:

public/build/
├── assets/
│   ├── app-BrYLxZ9n.css
│   └── app-DfJk2XMz.js
└── manifest.json

The manifest tells Laravel which hashed file corresponds to each source entry:

json
{
  "resources/js/app.js": {
    "file": "assets/app-DfJk2XMz.js",
    "src": "resources/js/app.js",
    "isEntry": true,
    "css": ["assets/app-BrYLxZ9n.css"]
  }
}

When @vite(['resources/js/app.js']) runs in production, it reads the manifest and renders <script src="/build/assets/app-DfJk2XMz.js"> plus the associated CSS <link>.

CI Pipeline Sketch

A typical GitHub Actions step:

yaml
- name: Build frontend
  run: |
    npm ci
    npm run build

- name: Deploy
  run: rsync -avz public/build/ user@server:/var/www/app/public/build/

The key idea: build assets in CI, then ship the public/build/ directory. Never run npm run build on production with limited memory — large builds can OOM tiny VMs.

Serving Assets from a CDN

To serve public/build/ from a CDN, set ASSET_URL in your .env:

env
ASSET_URL=https://cdn.example.com

Now @vite(...) emits https://cdn.example.com/build/assets/app-DfJk2XMz.js instead of a relative path. Upload the public/build/ folder to the CDN during deploy and you get edge caching for free.

Detecting a Missing Build

If public/build/manifest.json is missing in production, @vite throws ViteManifestNotFoundException. Catch this failure early in deployment health checks rather than letting end users hit a 500 page.

Common Pitfalls

  1. Committing public/build/ to git — It's build output. Let CI regenerate it on every deploy.
  2. Forgetting npm ci and using npm install — npm ci respects the lockfile exactly; install may drift and produce different bundles across environments.
  3. Running the dev server on production — npm run dev is for local work only. Production must use the build output.

Best Practices

  1. Pin your lockfile — Commit package-lock.json (or pnpm-lock.yaml) and use npm ci in CI so builds are reproducible.
  2. Build once, deploy many — If you deploy to staging and then promote to production, reuse the same built artifacts rather than rebuilding.
  3. Monitor bundle size — Add a size-limit check in CI so a runaway import doesn't silently ship a 5 MB bundle.

Summary

  • npm run build emits hashed files to public/build/ and a manifest Laravel reads.
  • The manifest is how @vite maps source entries to final filenames.
  • Build in CI, deploy the output, never run the dev server on production.
  • Point ASSET_URL at a CDN to serve assets from the edge.
✓ Completed