| | |

Vite 07 ⚡ Static Assets — public directory vs. imported assets

⏱️ Estimated read: 10 min | |

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

PropertyImportpublic directory
Build processingYes (hash, optimize, inline)No (copied as-is)
FilenameContent-hashedExact name preserved
Reference syntaximport x from './path'/path (absolute root)
Missing fileBuild failsRuntime 404
Cache bustingAutomatic (hash changes)Manual (rename or version query)
InliningYes, under assetsInlineLimitNever
Best forComponent assets, images, fontsrobots.txt, favicon.ico, manifest

Suffixes

SuffixEffect
?urlForce URL import (for files not in the default list)
?rawImport file contents as string
?inlineForce base64 inline
?no-inlineForce separate file

Decision rule

ConditionUse
Referenced from source codeImport
Needs cache bustingImport
Must keep exact namepublic
Never referenced from sourcepublic
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

PitfallWhy It HappensFix
Image cached after updatepublic asset has no hashImport the asset or add a version query
Build does not fail on missing assetAsset is in publicImport it so the build validates it
Asset path breaks in productionImported from publicReference public by absolute path, import only from source
SVG not inlinedFile over assetsInlineLimitUse ?inline suffix
TypeScript error on importMissing vite/client typesAdd 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

ItemValue
Default choiceImport
Import outputHashed filename in dist/assets/
Import inliningUnder assetsInlineLimit (4 KB default)
Public outputCopied as-is to dist/ root
Public referenceAbsolute path from root (/file.png)
Public use casesrobots.txt, favicon.ico, manifest, service worker
Missing importBuild failure
Missing public assetRuntime 404
Cache bustingAutomatic with import; manual with public

Key takeaways:

  • Import is the default; public is the exception. Vite’s documentation states this explicitly: “In general, prefer importing assets unless you specifically need the guarantees provided by the public directory” . Imports get hashed, optimized, and validated at build time. public assets 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 public asset 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 public reference produces a 404 in the browser, which may not be noticed until a user reports a broken image .
  • public is 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. public assets are referenced by absolute root path, not by import statement .
  • The ?url, ?raw, and ?inline suffixes control the import behavior. ?url forces URL treatment for files outside the default list. ?raw imports the file as a string. ?inline forces 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.

Was this helpful?


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!