vite-mastery

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.

Vite 8.1Stable

Default asset handling

Images / fonts / video

ts
// 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:

ts
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):

ts
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

ts
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:

ts
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:

ts
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

ts
// 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 AppConfig

WASM imports

ts
// 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

ts
// 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)
text
public/
  favicon.ico    → dist/favicon.ico
  robots.txt     → dist/robots.txt
  manifest.json  → dist/manifest.json

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

LocationGood forAccess
src/assets/resources imported by components, CSS, or JSimport logo from "./logo.png"
public/fixed browser-requested files"/favicon.ico"

When deploying under a sub path, public assets still need the base path:

ts
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:

css
.card {
  background-image: url("./card-bg.png");
}

Build output points to a hashed file:

css
.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:

ts
import { defineConfig } from "vite"

export default defineConfig({
  assetsInclude: ["**/*.gltf", "**/*.glb", "**/*.hdr"],
})

Then imports return asset URLs:

ts
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:

ts
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:

text
version https://git-lfs.github.com/spec/v1
oid sha256:...
size 123456

If you see this content, fetch the LFS assets before debugging Vite configuration.

Common mistakes

SymptomCauseFix
image 404 after deploybase does not match deployment pathconfigure base
SVG component expected, string returneddefault asset import returns URLuse an SVG loader/plugin
large PNG bundled into JS?inline or high inline limituse default import / ?url
public asset fails under sub pathroot path hard-codeduse BASE_URL
custom model import failsextension not recognizedconfigure assetsInclude

Self-check

  1. A 3KB SVG file — will Vite inline it or copy it by default? How do you force it to remain as a file?
  2. What is the difference between ?raw and ?url? What type of value does each return?
  3. How do the build outputs differ between files in public/ and files in src/assets/?
  4. If you need a base64 data URL for a PNG image in CSS, which modifier should you use?
  5. When should you use assetsInclude instead of a custom plugin?
ts
// 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