7.2 · difficulty 2/4 · 15 min read
Asset Pipeline
Images, fonts, JSON, WASM — how Vite handles static assets. What the ?url, ?raw, and ?inline suffixes mean, and what changes after Rolldown takes over.
Default asset handling
Images / fonts / video
// Large files (> 4KB): copied to dist/assets/ and returned as a hashed URL
import logo from "./logo.png"
// logo is a string: "/assets/logo-HASH.png"
// Small files (≤ 4KB): inlined directly as a base64 data URL
import icon from "./small-icon.svg"
// icon is a string: "data:image/svg+xml;base64,..."The 4KB threshold is controlled by build.assetsInlineLimit:
export default defineConfig({
build: {
assetsInlineLimit: 8192, // change to 8KB
// set to 0 to never inline (all assets copied to assets/)
},
})Suffix modifiers
?url — force a URL return
Regardless of file size, always returns a URL string (no inlining):
import logoUrl from "./logo.png?url"
// logoUrl === "/assets/logo-HASH.png" (even if the file is small)
// Useful when you need to control file size, or when you need a string URL
const img = new Image()
img.src = logoUrl?raw — return the raw file content as a string
import svgContent from "./icon.svg?raw"
// svgContent === "<svg xmlns='...'><path .../></svg>"
// Useful when you need to manipulate the SVG XML directly, or read a text file's content
document.querySelector("#icon").innerHTML = svgContent?inline — force inline as base64
Regardless of file size, always inlines:
import logoDataUrl from "./logo.png?inline"
// logoDataUrl === "data:image/png;base64,iVBOR..."
// Useful when you need a data URL and don't want a network request
css: `background-image: url(${logoDataUrl})`JSON imports
JSON files can be imported directly — Vite parses them automatically:
import pkg from "./package.json"
console.log(pkg.version) // "1.0.0"
// Import by field (tree-shaking friendly)
import { version, name } from "./package.json"TypeScript / JSON types
// JSON files have automatic type inference
import data from "./data.json"
// data's type is inferred from the JSON structure
// For stricter business types, constrain usage at the call site
interface AppConfig {
apiBase: string
featureFlags: Record<string, boolean>
}
const config = data as AppConfigWASM imports
// Standard import
import init, { add } from "./math.wasm"
await init()
console.log(add(1, 2)) // 3
// Explicit URL import (for manual instantiation)
import wasmUrl from "./math.wasm?url"
const response = await fetch(wasmUrl)
const wasm = await WebAssembly.instantiate(await response.arrayBuffer())Worker imports
// Launch a Web Worker from the main thread
import MyWorker from "./worker.ts?worker"
const worker = new MyWorker()
// Inline Worker (bundled as a blob URL, no separate file needed)
import InlineWorker from "./worker.ts?worker&inline"
// Shared Worker
import SharedWorker from "./shared.ts?sharedworker"The public/ directory
Files in public/ are not processed by Vite:
- Copied directly to the
dist/root - Not hash-renamed
- Cannot be referenced via
import(access them with URL strings)
public/
favicon.ico → dist/favicon.ico
robots.txt → dist/robots.txt
manifest.json → dist/manifest.jsonSuitable for: special files the browser requests directly (favicon, manifest, sitemap, etc.).
Do not put every imported image, font, or SVG in public/. That bypasses hashing, dependency tracking, path rewriting, and build-time processing.
| Location | Good for | Access |
|---|---|---|
src/assets/ | resources imported by components, CSS, or JS | import logo from "./logo.png" |
public/ | fixed browser-requested files | "/favicon.ico" |
When deploying under a sub path, public assets still need the base path:
const favicon = `${import.meta.env.BASE_URL}favicon.ico`Hard-coding "/favicon.ico" requests the domain root, which is wrong for deployments like /docs/.
CSS url() enters the asset pipeline
Relative URLs in CSS are processed too:
.card {
background-image: url("./card-bg.png");
}Build output points to a hashed file:
.card {
background-image: url("/assets/card-bg-HASH.png");
}Avoid hard-coded public paths in CSS unless you intentionally need a fixed URL.
assetsInclude
Vite recognizes common images, fonts, media files, and WASM by default. For custom asset extensions such as .gltf, .glb, or .hdr, use assetsInclude:
import { defineConfig } from "vite"
export default defineConfig({
assetsInclude: ["**/*.gltf", "**/*.glb", "**/*.hdr"],
})Then imports return asset URLs:
import modelUrl from "./scene.gltf"If you only need a URL, use assetsInclude. If you need to parse and transform content, write a plugin.
Inline limits are not always better when larger
assetsInlineLimit balances request count against JS/CSS size:
export default defineConfig({
build: {
assetsInlineLimit: 4096,
},
})Large inline limits can increase JS/CSS size, reduce independent browser caching, hurt sourcemap readability, and require CSP rules for data:.
Inline tiny, module-local, low-reuse assets. Keep large images, fonts, videos, and shared logos as separate files.
Git LFS placeholders
With Git LFS, a missing real asset may appear as a small pointer file:
version https://git-lfs.github.com/spec/v1
oid sha256:...
size 123456If you see this content, fetch the LFS assets before debugging Vite configuration.
Common mistakes
| Symptom | Cause | Fix |
|---|---|---|
| image 404 after deploy | base does not match deployment path | configure base |
| SVG component expected, string returned | default asset import returns URL | use an SVG loader/plugin |
| large PNG bundled into JS | ?inline or high inline limit | use default import / ?url |
| public asset fails under sub path | root path hard-coded | use BASE_URL |
| custom model import fails | extension not recognized | configure assetsInclude |
Self-check
- A 3KB SVG file — will Vite inline it or copy it by default? How do you force it to remain as a file?
- What is the difference between
?rawand?url? What type of value does each return? - How do the build outputs differ between files in
public/and files insrc/assets/? - If you need a base64 data URL for a PNG image in CSS, which modifier should you use?
- When should you use
assetsIncludeinstead of a custom plugin?
// Choose the appropriate import method for each scenario:
// Scenario A: Embed an SVG icon directly into the page HTML (inline SVG)
// Scenario B: Get the URL of an image file for use in <img src>
// Scenario C: Read the contents of an HTML template file
// Scenario D: Load a WASM module and instantiate it manually
// Scenario E: Force a large PNG image to be base64-inlined at build time
// For each scenario:
// 1. Choose an import method (import ... from "...?XXX")
// 2. Explain your reasoning
// Example:
// Scenario A: import svgContent from "./icon.svg?raw"
// Reason: need the SVG XML string to insert directly into innerHTML
// TODO: complete B / C / D / E