Vite 07 ⚡ Static Assets — public directory vs. imported assets
Vite handles static assets through two distinct paths that produce different output. An imported asset enters the build pipeline: it gets a hashed filename, can be inlined as base64 if small enough, and is processed by plugins for optimization. An asset in the public directory is copied as-is to the build output, keeps its exact filename, and is never processed by the bundler. The choice between them is not about convenience—it determines whether the browser can cache-bust the file, whether the build fails on a missing file, and whether the asset is optimized at all.
This chapter covers the two mechanisms, the decision rule that Vite’s documentation states explicitly, and the edge cases where the “wrong” choice is actually correct.
Key point: Import assets by default. Use the public directory only when the file must keep its exact name, is never referenced from source code, or is a root-level file like robots.txt or favicon.ico that browsers expect at a fixed path .
Why the two mechanisms exist
The build-graph problem. An imported asset is part of the module graph. Vite sees the import, includes the file in the build, and emits it with a content hash. If the file is missing, the build fails. If the file is small (under assetsInlineLimit, default 4 KB), Vite inlines it as a base64 data URL, eliminating the network request entirely . This is the behavior most assets want.
The fixed-name problem. Some files cannot have a hash in their name. robots.txt must be at /robots.txt. favicon.ico is requested by browsers at /favicon.ico. A PWA manifest.json is referenced by a fixed URL. A service worker script must be served from a predictable path. These files cannot go through the hashing pipeline because their consumers expect a specific name .
The root-path problem. An asset in public/ is served at the root path during development and copied to the root of dist/ during build. A file at public/images/logo.png is referenced as /images/logo.png, not ./images/logo.png. This is a deliberate design: public assets are addressed by absolute path, and Vite does not transform those references .
a. Imported assets
An import statement is the default way to reference an asset from application code. The import returns a URL string that Vite resolves differently in development and production.
import logo from './assets/logo.png'
document.getElementById('hero').src = logo
In development, logo is /src/assets/logo.png. In production, it becomes /assets/logo.2d8efhg.png—a hashed filename that changes when the file content changes. This is content-based cache busting: the browser can cache the hashed URL indefinitely, and a new hash forces a new download when the file is updated .
The import works for common image, media, and font types automatically. For other file types, the assetsInclude option extends the list, or the ?url suffix forces URL treatment . The ?raw suffix imports the file as a string. The ?inline and ?no-inline suffixes override the inlining decision .
The benefit is not just hashing. A missing imported asset fails the build. A missing public asset fails silently at runtime with a 404. For assets that the application depends on, the build-time failure is the safer behavior .
b. The public directory
A file in public/ is copied to the build output without processing. No hash, no optimization, no transformation. It is referenced by absolute path from the root.
// public/logo.png is referenced as:
<img src="/logo.png" alt="Logo" />
The file structure under public/ is preserved. public/images/logo.png becomes /images/logo.png in both development and production . The publicDir option changes the directory name if public conflicts with something else.
The documentation is explicit about when to use it: assets that are never referenced in source code (like robots.txt), assets that must retain their exact filename, or assets that the developer does not want to import just to get a URL .
The critical warning: never import from public/. An import like import x from '../public/foo.png' bypasses the asset pipeline and may produce a URL that does not survive the build. public assets are referenced by absolute path, not by import .
Complete Example Session
// ============================================
// PART 1: IMPORTING AN ASSET
// ============================================
import logo from './assets/logo.png'
// Development: /src/assets/logo.png
// Production: /assets/logo.2d8efhg.png
// ============================================
// PART 2: THE HASHED OUTPUT
// ============================================
// The hash changes when the file content changes.
// The browser caches the old hash; the new hash is a new request.
// This is content-based cache busting.
// ============================================
// PART 3: SMALL ASSET INLINING
// ============================================
import icon from './assets/icon.svg'
// If the file is under assetsInlineLimit (4 KB default):
// icon is a base64 data URL, not a network request.
// ============================================
// PART 4: FORCING URL OR RAW
// ============================================
import url from './shader.glsl?url' // always a URL
import raw from './shader.glsl?raw' // file contents as string
// ============================================
// PART 5: THE PUBLIC DIRECTORY
// ============================================
// public/robots.txt → served at /robots.txt
// public/favicon.ico → served at /favicon.ico
// public/og-image.png → served at /og-image.png
<!-- ============================================
PART 6: REFERENCING PUBLIC ASSETS
============================================ -->
<link rel="icon" href="/favicon.ico" />
<meta property="og:image" content="/og-image.png" />
// ============================================
// PART 7: THE WRONG IMPORT
// ============================================
import robots from '../public/robots.txt?raw' // ❌
// Bypasses the pipeline; may break in production.
// ============================================
// PART 8: THE MISSING FILE FAILURE
// ============================================
import missing from './assets/missing.png' // ❌ build fails
// vs.
// <img src="/missing.png" /> // ❌ runtime 404, silent
// ============================================
// PART 9: DYNAMIC IMPORT WITH GLOB
// ============================================
const images = import.meta.glob('./assets/*.png', { eager: true })
// All matched files are hashed and included in the build.
// Use when the asset name is not known at build time.
// ============================================
// PART 10: THE DECISION RULE
// ============================================
// Import by default.
// Public only when:
// - exact filename required
// - never referenced from source
// - root-level file (robots.txt, favicon)
The ten parts covered importing, hashing, inlining, explicit URL/raw, the public directory, referencing public assets, the wrong import, the missing-file failure, dynamic glob imports, and the decision rule.
Quick Reference
Comparison
| Property | Import | public directory |
|---|---|---|
| Build processing | Yes (hash, optimize, inline) | No (copied as-is) |
| Filename | Content-hashed | Exact name preserved |
| Reference syntax | import x from './path' | /path (absolute root) |
| Missing file | Build fails | Runtime 404 |
| Cache busting | Automatic (hash changes) | Manual (rename or version query) |
| Inlining | Yes, under assetsInlineLimit | Never |
| Best for | Component assets, images, fonts | robots.txt, favicon.ico, manifest |
Suffixes
| Suffix | Effect |
|---|---|
?url | Force URL import (for files not in the default list) |
?raw | Import file contents as string |
?inline | Force base64 inline |
?no-inline | Force separate file |
Decision rule
| Condition | Use |
|---|---|
| Referenced from source code | Import |
| Needs cache busting | Import |
| Must keep exact name | public |
| Never referenced from source | public |
Root-level file (robots.txt, favicon.ico) | public |
Best Practices
✅ Do This:
// Import assets referenced from code
import logo from './assets/logo.png' // ✅
// Use public for fixed-name files
// public/robots.txt → /robots.txt // ✅
// Use ?url for files outside the default asset list
import worklet from './paint.js?url' // ✅
❌ Don’t Do This:
// Import from public
import x from '../public/foo.png' // ❌
// Put component images in public
// <img src="/logo.png" /> without import // ❌ (no cache busting)
// Assume public assets fail at build time
// <img src="/missing.png" /> // ❌ (runtime 404)
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Image cached after update | public asset has no hash | Import the asset or add a version query |
| Build does not fail on missing asset | Asset is in public | Import it so the build validates it |
| Asset path breaks in production | Imported from public | Reference public by absolute path, import only from source |
| SVG not inlined | File over assetsInlineLimit | Use ?inline suffix |
| TypeScript error on import | Missing vite/client types | Add vite/client to tsconfig |
Real-World Examples
1. Import an image
import hero from './assets/hero.png'
2. Public favicon
<link rel="icon" href="/favicon.ico" />
3. Public robots.txt
public/robots.txt → /robots.txt
4. Force raw import
import shader from './shader.glsl?raw'
5. Force URL import
import worklet from './paint.js?url'
6. Force inline
import icon from './icon.svg?inline'
7. Glob import
const flags = import.meta.glob('./flags/*.svg', { eager: true, as: 'url' })
8. Public OG image
<meta property="og:image" content="/og-image.png" />
9. Dynamic URL
const url = new URL(`./assets/${name}.png`, import.meta.url).href
10. Wrong import from public
import x from '../public/foo.png' // ❌
Visual
The two pipelines
┌─────────────────────────────────────────────────────────────┐
│ IMPORTED ASSET │
│ │
│ import logo from './assets/logo.png' │
│ │ │
│ ▼ │
│ Build graph │
│ │ │
│ ├── hash filename │
│ ├── inline if small │
│ └── optimize │
│ │ │
│ ▼ │
│ /assets/logo.2d8efhg.png │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ PUBLIC ASSET │
│ │
│ public/robots.txt │
│ │ │
│ ▼ │
│ Copy as-is │
│ │ │
│ ▼ │
│ /robots.txt (exact name) │
└─────────────────────────────────────────────────────────────┘
Cache busting
┌─────────────────────────────────────────────────────────────┐
│ IMPORT (AUTOMATIC) │
│ │
│ logo.png changes │
│ │ │
│ ▼ │
│ Hash changes: logo.a1b2c3d4.png → logo.e5f6g7h8.png │
│ │ │
│ ▼ │
│ Browser sees new URL, downloads new file │
│ │
│ PUBLIC (MANUAL) │
│ │
│ logo.png changes │
│ │ │
│ ▼ │
│ URL unchanged: /logo.png │
│ │ │
│ ▼ │
│ Browser serves cached version until cache expires │
└─────────────────────────────────────────────────────────────┘
Decision flow
┌─────────────────────────────────────────────────────────────┐
│ WHICH MECHANISM? │
│ │
│ Does the file need an exact, stable name? │
│ └── Yes ──► public │
│ │
│ Is the file referenced from source code? │
│ └── Yes ──► import │
│ │
│ Is it a root-level file (robots.txt, favicon)? │
│ └── Yes ──► public │
│ │
│ Default ──► import │
└─────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Default choice | Import |
| Import output | Hashed filename in dist/assets/ |
| Import inlining | Under assetsInlineLimit (4 KB default) |
| Public output | Copied as-is to dist/ root |
| Public reference | Absolute path from root (/file.png) |
| Public use cases | robots.txt, favicon.ico, manifest, service worker |
| Missing import | Build failure |
| Missing public asset | Runtime 404 |
| Cache busting | Automatic with import; manual with public |
Key takeaways:
- Import is the default;
publicis the exception. Vite’s documentation states this explicitly: “In general, prefer importing assets unless you specifically need the guarantees provided by thepublicdirectory” . Imports get hashed, optimized, and validated at build time.publicassets get none of that. - Imported assets get content hashing for cache busting. The filename changes when the content changes, so the browser downloads the new version and caches it indefinitely. A
publicasset keeps its name, so the browser may serve a stale cached version after an update . - Imported assets fail the build if missing. A typo in an import path stops the build. A typo in a
publicreference produces a 404 in the browser, which may not be noticed until a user reports a broken image . publicis for files that must keep their exact name.robots.txt,favicon.ico, PWA manifests, service workers, and Open Graph images that are shared by URL all need stable paths. Hashing would break them .- Never import from
public. The import bypasses the asset pipeline and may produce a path that does not survive the build.publicassets are referenced by absolute root path, not by import statement . - The
?url,?raw, and?inlinesuffixes control the import behavior.?urlforces URL treatment for files outside the default list.?rawimports the file as a string.?inlineforces base64 inlining regardless of size .
Remember: The public directory is not a convenience for “files I don’t want to import.” It is a specific mechanism for files that must be served at a fixed path and never processed. Everything else goes through the import pipeline, where Vite hashes it, optimizes it, and fails the build if it is missing. The one rule that prevents the most problems: do not import from public. Reference public assets by absolute path, and import assets from src/ or wherever the application code lives.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!