| | |

esbuild 14 ⚡ Loaders — Handling images, fonts, and JSON

⏱️ Estimated read: 11 min | |

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

ExtensionLoaderBehavior
.jsjsJavaScript
.jsxjsxJSX
.tstsTypeScript
.tsxtsxTSX
.csscssGlobal CSS
.module.csslocal-cssScoped CSS
.jsonjsonParsed object

Asset loaders

LoaderImport returnsUse case
dataurldata: URL stringSmall images, inline assets
filePath to copied fileLarge images, fonts
copyPath to copied fileAssets that must not be transformed
textStringShaders, text files
binaryUint8ArrayBinary data
base64Base64 stringEncoded binary

The loader configuration

MethodExample
CLI--loader:.png=dataurl
JavaScript APIloader: { '.png': 'dataurl' }
Import attributewith { type: 'json' }

The error

ErrorCauseFix
No loader is configured for ".png"No loader for the extensionAdd a loader entry
No loader is configured for ".gif"CSS url() referenceConfigure .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

PitfallWhy It HappensFix
Build fails on .png importNo loader configuredAdd loader: { '.png': 'file' }
CSS url() not resolvedNo loader for the referenced extensionConfigure the extension in the loader
Bundle is hugedataurl used for a large assetUse file instead
JSON tree-shaking does not workDefault export usedUse a named export
Uint8Array is not a Bufferbinary loader in NodeConvert 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

ItemValue
Default loadersjs, jsx, ts, tsx, css, json
JSON default exportEntire object
JSON named exportTree-shaken field
Asset loaderdataurl (inline) or file (copy)
Text loadertext (string)
Binary loaderbinary (Uint8Array)
Copy loadercopy (path, no transform)
CSS asset referenceRequires 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, and json loaders are enabled by default. A .png or .woff2 import requires an explicit loader entry, and the build fails with a clear error if the entry is missing .
  • The json loader provides tree-shaking through named exports. An import of { version } from package.json 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 dataurl loader inlines the asset, and the file loader copies it. The dataurl loader returns a data: URL string, which eliminates the network request but increases the bundle size. The file loader 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 references url('./loader.gif') is bundled only if a loader is configured for .gif. Without it, the build fails with No loader is configured for ".gif" files .
  • The text, binary, and base64 loaders expose file contents as values. The text loader returns a string, the binary loader returns a Uint8Array, and the base64 loader 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.

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!