JAKEARCHIBALD

Importing vs fetching JSON_JAKEARCHIBALD:A496D7960199E35FDA15EAD6611BA94F

Description

This year, JSON module imports became baseline 'newly available', meaning they're implemented across browser engines.


import data from './data.json' with { type: 'json' };

// And…

const { default: data } = await import('./data.json', {
with: { type: 'json' },
});

I'm glad JavaScript has this feature, but I can't see myself using it in a browser environment, other than small demos. It comes down to the behaviour differences with this:


const response = await fetch('./data.json');
const data = await response.json();

Here's why…

## Error handling

With a static import:


import data from './data.json' with { type: 'json' };

If the above fails, it takes the whole module graph down with it. Because of this, I'd never use this pattern with some third-party JSON, as I'd want to be able to provide a fallback if the third-party service fails.

But `import()` allows exactly that:


try {
const { default: data } = await import(url, {
with: { type: 'json' },
});
} catch (error) {
// Fallback logic
}

This is pretty good. Although the `fetch()` alternative:


try {
const response = await fetch('./data.json');
const data = await response.json();
} catch (error) {
// Fallback logic
}

…allows much more introspection in event of a failure. There's `response.status`, or you can use `response.text()`, meaning you still have the source if JSON parsing fails. Maybe that doesn't matter in all cases.

I think the bigger issue is…

## Caching and garbage collection

When you import a module (be it JS, WASM, CSS, or JSON), it's cached for the lifetime of the environment (e.g. a page or worker), even if the result is a network or parsing failure. All imports for a given specifier & type return the same module.

This is generally a good thing, as it means your JS module returns the same objects every time, and state can be shared across all importers. But if you're doing something like:


const { default: results } = await import('/api/search?q=whatever', {
with: { type: 'json' },
});

…then you have a memory leak, because each set of search results will live in the module graph for the life of the page. That isn't the case with `fetch()`, where returned objects can be garbage collected once they're out of reference.

The same applies to cases like:


let { default: largeData } = await import('/large-data.json', {
with: { type: 'json' },
});

const someSmallPart = largeData.slice(0, 10);
largeData = null;

If the above used `fetch()`, then the data other than `someSmallPart` could be garbage collected. But with `import()`, the whole `largeData` object remains in memory for the life of the page.

## When should JSON modules be used?

It makes sense to use JSON module imports for local static JSON resources where you need all/most of the data within. Particularly since bundlers can understand JSON imports, and bundle the object with other modules. That isn't possible with `fetch()`, unless you use some pretty hacky plugins.

In server code, I might import `package.json` to get the version number. However, I wouldn't do this with frontend code, as it's wasteful to bundle all of `package.json` just to get a single value – bundlers don't perform tree-shaking of individual object keys.

**Update:** Jed points out that esbuild has an option at allows you to import JSON as if each top level key is individually exported, and in this case it will tree-shake. You could make a fairly trivial plugin to make the same work for Rollup/Vite. It still requires you to use the right kind of import, though.

Generally, I'd write a Vite/Rollup plugin to extract just the data I needed at build time (with Vite, the define option is handy).

I'm glad this feature exists, but it should be used with care, and not as a blanket replacement for `fetch()`ing JSON.
Visit Original Source

Basic Information

ID JAKEARCHIBALD:A496D7960199E35FDA15EAD6611BA94F
Published Oct 22, 2025 at 01:00

💭 Join the Security Discussion

🔒 Your email address will not be published. Required fields are marked *

⚠️ Please be respectful and constructive in your comments. Security discussions should remain professional.