esbuild 14 ⚡ Loaders — Handling images, fonts, and JSON
esbuild’s loaders are the mechanism that tells the bundler how to interpret a file that is imported or referenced. The loader is selected by file extension, and the selection is configured through the loader option. A .json file uses the json loader by default, which parses the file into a JavaScript object and exports it. A .png file has no default loader, so importing it without configuration produces the error No loader is configured for ".png" files. The dataurl and file loaders are the two mechanisms that handle the binary assets, and the choice between them is a decision about whether the asset is inlined into the bundle or emitted as a separate file .
This chapter covers the three loader categories that matter for most projects: the JSON loader, the asset loaders (dataurl and file), and the text/binary loaders that expose file contents as values.
Key point: esbuild has a default loader for .js, .jsx, .ts, .tsx, .css, and .json. Every other extension requires an explicit loader configuration. Without it, the build fails with a message that names the extension and the file.
Why loaders are explicit
The default-loader problem. esbuild enables a small set of loaders by default, and the list is deliberate. The JavaScript, TypeScript, JSX, and CSS loaders are the languages the bundler is designed for. The json loader is enabled because JSON is a data format that is frequently imported from JavaScript. The image and font formats are not enabled because there is no single correct handling: a small icon should be inlined, a large font should be copied, and a project might want either. The explicit loader configuration is the mechanism that lets the project decide .
The error problem. The error message is the first encounter with the loader system for many projects. A CSS file that references url('./loader.gif') fails with No loader is configured for ".gif" files because the CSS bundling resolves the url() reference and then tries to load the .gif with no loader configured for that extension . The fix is a loader entry for .gif, not a change to the CSS file.
The tree-shaking problem. The json loader is not just a parser. It provides named exports for the top-level properties of the JSON object, and esbuild uses the named export to perform tree-shaking. An import of { version } from package.json includes only the version field in the bundle, not the entire file . The default export includes the entire object. The choice between the two is a bundle-size decision.
a. The JSON loader
The json loader is enabled by default for .json files. It parses the file at build time and exports the object as the default export. The named exports correspond to the top-level properties.
// Default export: the entire object
import pkg from './package.json'
console.log(pkg.version)
// Named export: only the version field is bundled
import { version } from './package.json'
console.log(version)
The named export is the tree-shaking mechanism. The version import includes only the version field. The default import includes the entire object, because esbuild cannot know which fields the code will access at runtime.
The json loader can also be forced for a file with a different extension by using the import attribute syntax. A file named data.custom can be imported with with { type: 'json' }, which tells esbuild to parse it as JSON without a configuration change .
import data from './data.custom' with { type: 'json' }
The import attribute is part of the JavaScript JSON modules proposal, and it is supported by other tools, which makes the code more portable.
b. The dataurl and file loaders
The dataurl loader inlines the file as a base64 data URL. The import returns a string that starts with data:. The file loader copies the file to the output directory and returns the path to the copied file. The choice between them is a trade-off between the number of network requests and the size of the bundle.
// esbuild.build.js
await esbuild.build({
loader: {
'.png': 'dataurl',
'.woff2': 'file',
},
})
With the dataurl loader, an import of icon.png returns a string like data:image/png;base64,iVBORw0.... The image is embedded in the JavaScript or CSS, and the browser does not make a separate request for it. This is appropriate for a small icon, where the request overhead is larger than the file itself .
With the file loader, the import returns the path to the copied file, such as ./icon-ABCD1234.png. The file is written to the output directory with a hash in the name. This is appropriate for a large image or a font, where inlining would bloat the bundle and prevent the browser from caching the asset separately .
The same loaders apply to the url() references in a CSS file. A CSS file that references url('./bg.png') is bundled only if a loader is configured for .png. The dataurl loader inlines the image into the CSS, and the file loader copies the image and rewrites the URL .
c. The text and binary loaders
The text loader reads the file as a string. The binary loader reads the file as a Uint8Array. The base64 loader reads the file and returns a base64-encoded string. These loaders are for files that are not images or fonts: a .glsl shader, a .txt file, or a custom binary format.
await esbuild.build({
loader: {
'.glsl': 'text',
'.bin': 'binary',
},
})
An import of a .glsl file with the text loader returns the shader source as a string, which can be passed to WebGLRenderingContext.shaderSource. An import of a .bin file with the binary loader returns a Uint8Array, which can be used directly or converted to a Buffer in Node.js .
The copy loader is a variant of the file loader. It copies the file to the output directory and rewrites the import path, but it does not transform the file into a module. It is used when the file is already a complete asset that should be served as-is, and the import is only a reference to the path.
Complete Example Session
// ============================================
// PART 1: THE JSON DEFAULT EXPORT
// ============================================
import pkg from './package.json'
console.log(pkg.version)
// The entire object is bundled.
// ============================================
// PART 2: THE JSON NAMED EXPORT
// ============================================
import { version } from './package.json'
console.log(version)
// Only the version field is bundled.
// ============================================
// PART 3: THE JSON IMPORT ATTRIBUTE
// ============================================
import data from './data.custom' with { type: 'json' }
// Parsed as JSON without a loader config.
// ============================================
// PART 4: THE DATAURL LOADER
// ============================================
await esbuild.build({
loader: { '.png': 'dataurl' },
})
import icon from './icon.png'
// icon is a data URL string.
// ============================================
// PART 5: THE FILE LOADER
// ============================================
await esbuild.build({
loader: { '.woff2': 'file' },
})
import font from './font.woff2'
// font is the path to the copied file.
/* ============================================
PART 6: THE CSS ASSET REFERENCE
============================================ */
.loading {
background-image: url('./loader.gif');
}
/* Requires a loader for .gif. */
// ============================================
// PART 7: THE CSS ASSET CONFIGURATION
// ============================================
await esbuild.build({
loader: { '.gif': 'dataurl' },
})
// The gif is inlined into the CSS.
// ============================================
// PART 8: THE TEXT LOADER
// ============================================
await esbuild.build({
loader: { '.glsl': 'text' },
})
import shader from './shader.glsl'
// shader is a string.
// ============================================
// PART 9: THE BINARY LOADER
// ============================================
await esbuild.build({
loader: { '.bin': 'binary' },
})
import data from './data.bin'
// data is a Uint8Array.
// ============================================
// PART 10: THE COPY LOADER
// ============================================
await esbuild.build({
loader: { '.pdf': 'copy' },
})
import pdfPath from './manual.pdf'
// The file is copied, the import is the path.
The ten parts covered the JSON default export, the JSON named export, the JSON import attribute, the dataurl loader, the file loader, the CSS asset reference, the CSS asset configuration, the text loader, the binary loader, and the copy loader.
Quick Reference
Default loaders
| Extension | Loader | Behavior |
|---|---|---|
.js | js | JavaScript |
.jsx | jsx | JSX |
.ts | ts | TypeScript |
.tsx | tsx | TSX |
.css | css | Global CSS |
.module.css | local-css | Scoped CSS |
.json | json | Parsed object |
Asset loaders
| Loader | Import returns | Use case |
|---|---|---|
dataurl | data: URL string | Small images, inline assets |
file | Path to copied file | Large images, fonts |
copy | Path to copied file | Assets that must not be transformed |
text | String | Shaders, text files |
binary | Uint8Array | Binary data |
base64 | Base64 string | Encoded binary |
The loader configuration
| Method | Example |
|---|---|
| CLI | --loader:.png=dataurl |
| JavaScript API | loader: { '.png': 'dataurl' } |
| Import attribute | with { type: 'json' } |
The error
| Error | Cause | Fix |
|---|---|---|
No loader is configured for ".png" | No loader for the extension | Add a loader entry |
No loader is configured for ".gif" | CSS url() reference | Configure .gif |
Best Practices
✅ Do This:
// Use named exports for tree-shaking
import { version } from './package.json' // ✅
// Use dataurl for small assets
loader: { '.png': 'dataurl' } // ✅
// Use file for fonts and large images
loader: { '.woff2': 'file' } // ✅
// Use text for shaders
loader: { '.glsl': 'text' } // ✅
❌ Don’t Do This:
// Import an image without a loader
import icon from './icon.png' // ❌ (build error)
// Inline a large font with dataurl
loader: { '.woff2': 'dataurl' } // ❌ (bloats bundle)
// Use the default JSON import when only one field is needed
import pkg from './package.json' // ❌ (bundles everything)
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
Build fails on .png import | No loader configured | Add loader: { '.png': 'file' } |
CSS url() not resolved | No loader for the referenced extension | Configure the extension in the loader |
| Bundle is huge | dataurl used for a large asset | Use file instead |
| JSON tree-shaking does not work | Default export used | Use a named export |
Uint8Array is not a Buffer | binary loader in Node | Convert with Buffer.from(data) |
Real-World Examples
1. JSON named export
import { version } from './package.json'
2. JSON import attribute
import data from './data.custom' with { type: 'json' }
3. dataurl loader
loader: { '.png': 'dataurl' }
4. file loader
loader: { '.woff2': 'file' }
5. CSS asset
background: url('./bg.png');
6. CSS loader config
loader: { '.png': 'file' }
7. text loader
loader: { '.glsl': 'text' }
8. binary loader
loader: { '.bin': 'binary' }
9. copy loader
loader: { '.pdf': 'copy' }
10. The error
No loader is configured for ".gif" files
Visual
The loader decision
┌─────────────────────────────────────────────────────────────┐
│ File extension │
│ │ │
│ ├── Default loader? ──► use it │
│ │ │
│ └── No default ──► loader config required │
│ │ │
│ ├── dataurl ──► inline │
│ ├── file ──► copy + path │
│ ├── text ──► string │
│ └── binary ──► Uint8Array │
└─────────────────────────────────────────────────────────────┘
The dataurl vs file
┌─────────────────────────────────────────────────────────────┐
│ dataurl: │
│ import icon from './icon.png' │
│ icon === 'data:image/png;base64,...' │
│ No separate request. Larger bundle. │
│ │
│ file: │
│ import font from './font.woff2' │
│ font === './font-ABCD1234.woff2' │
│ Separate request. Smaller bundle. │
└─────────────────────────────────────────────────────────────┘
The JSON tree-shaking
┌─────────────────────────────────────────────────────────────┐
│ Default export: │
│ import pkg from './package.json' │
│ → entire object bundled │
│ │
│ Named export: │
│ import { version } from './package.json' │
│ → only version field bundled │
└─────────────────────────────────────────────────────────────┘
The CSS asset pipeline
┌─────────────────────────────────────────────────────────────┐
│ .loading { background: url('./loader.gif'); } │
│ │ │
│ ▼ │
│ No loader for .gif ──► build error │
│ │ │
│ ▼ │
│ loader: { '.gif': 'dataurl' } │
│ │ │
│ ▼ │
│ background: url('data:image/gif;base64,...') │
└─────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Default loaders | js, jsx, ts, tsx, css, json |
| JSON default export | Entire object |
| JSON named export | Tree-shaken field |
| Asset loader | dataurl (inline) or file (copy) |
| Text loader | text (string) |
| Binary loader | binary (Uint8Array) |
| Copy loader | copy (path, no transform) |
| CSS asset reference | Requires a loader for the extension |
Key takeaways:
- esbuild has a default loader for the common languages, but not for images and fonts. The
js,jsx,ts,tsx,css, andjsonloaders are enabled by default. A.pngor.woff2import requires an explicitloaderentry, and the build fails with a clear error if the entry is missing . - The
jsonloader provides tree-shaking through named exports. An import of{ version }frompackage.jsonincludes only theversionfield. The default import includes the entire object, because esbuild cannot know which fields the code will access at runtime . - The
dataurlloader inlines the asset, and thefileloader copies it. Thedataurlloader returns adata:URL string, which eliminates the network request but increases the bundle size. Thefileloader returns the path to the copied file, which keeps the bundle small and allows the browser to cache the asset separately . - The CSS
url()references require the same loader configuration. A CSS file that referencesurl('./loader.gif')is bundled only if a loader is configured for.gif. Without it, the build fails withNo loader is configured for ".gif" files. - The
text,binary, andbase64loaders expose file contents as values. Thetextloader returns a string, thebinaryloader returns aUint8Array, and thebase64loader returns an encoded string. These are for files that are not images or fonts: shaders, custom data formats, or text assets . - The import attribute
with { type: 'json' }forces the JSON loader without configuration. A file with a non-standard extension can be parsed as JSON by adding the attribute to the import statement. This is part of the JSON modules proposal and is supported by other tools .
Remember: Loaders are the mechanism that tells esbuild how to interpret a file. The default loaders cover the languages and JSON; the images, fonts, and custom formats require an explicit loader configuration. The dataurl and file loaders are the two choices for binary assets, and the decision is between inlining and copying. The json loader provides tree-shaking through named exports. And the error message No loader is configured for ".ext" is the signal that a loader entry is missing.
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!