Referencing JSON in Vite
Two Approaches
JSON, as the standard format of structured data in web development, basically has two approaches to be consumed in Vite:
- JS Module
- Static Asset
JSON as JS Module
This way, like the JSON modules in Node, parses the JSON and load the object directly into our module.
In comparison to the default-only import in Node, Vite even allows named imports for each top-level property in the JSON object, which helps with tree-shaking.
// JSON module in Node
import foo from './foo.json' with { type: 'json' }
console.log(foo)
// JSON module in Vite
import { baz } from './bar.json'
console.log(baz)
Under the hood, Vite converts .json
files to ES6 modules via:
- esbuild built-in JSON loader in dev server
@rollup/plugin-json
at build time
In this approach, JSON files are treated as JS modules and will be included when bundled, just like any other JS files.
JSON as Static Asset
Meanwhile, JSON files can be treated as static assets, which are served as separated files and should be referenced by the resolved public URL.
The static asset URL differs by the parent directory of the JSON file in Vite:
- Public directory: URL stays unchanged
- Non-public directory: URL will be suffixed with the file hash
And for JSON files, you need to use the ?url
or ?raw
suffix to bypass the JSON loader, since Vite does not recognize JSON as assets by default.
// Public directory
// stays `/foo.json`
import foo from '/foo.json?url'
// Non-public directory
// becomes `/assets/foo.2d8efhg.json`
import foo from './foo.json?url'
To utilize static JSON assets, you typically need to fetch and parse them at runtime.
const foo = await fetch('/foo.json').then((res) => res.json())
Which One to Choose?
Most of the time, I personally go with the JSON as module approach for simplicity. But of course, it always depends on the use case.
In my opinion, the usage of JSON in frontend development usually scales from module to assets.
It tends to start as an abstraction of static data for better separation of concerns. For example, a table-of-contents configuration for a documentation site. The configuration is often unique and critical at the build time to the project, so importing this file as a module meets the requirement quite well and is very intuitive.
import toc from './toc.json'
// UI for navigation
<TOC toc={toc} />
// RSS feed
await writeFile('rss.xml', generateRSSFeed(toc))
// Other usages...
But things change when it comes to more scalable and dynamic scenarios. A typical example of this is dynamic configurations, for which I found Shiki’s theme preview is a great example.
Shiki is a syntax highlighter library, and its playground supports preview of different themes, which are written in JSON files.
In the playground, multiple JSON files may be needed as the user wants to preview different themes, and the files acquire quite some bandwidth. In this case, it’s better to keep different configurations in separated JSON files and load them on demand.
function loadTheme(name: string) {
return fetch(`/themes/${name}.json`).then((res) => res.json())
}
However, your JSON asset is not always located in the public directory, chances are you want to colocate it with your app code. To resolve the proper URL, you will need to construct the URL with import.meta.url
.
function loadTheme(name: string) {
const url = new URL(`./themes/${name}.json`, import.meta.url).href
return fetch(url).then((res) => res.json())
}
This way, we avoid the bundle size exploding with huge amounts of JSONs and can dynamically access them, which is similar with accessing database with REST APIs.
Preload JSON Assets
Like any other static assets, JSON files could be huge thus harms the use experience when loading.
For resources that your page will need very soon, the rel=preload
primitive is great for improving the loading performance.
As we will load JSON assets via fetch
, we need to specify the correct as
attribute. In addition to this, proper crossorigin
attribute is also required for the preloaded resource to be accessible to our fetch
calls.
<link rel="preload" as="fetch" src="/foo.json" crossorigin />
This way, the JSON file will be preloaded and often immediately available for the first fetch
call when the interaction happens. But in real scenarios, you may want to provide more granular cache strategies for the asset, i.e. stale-while-revalidate
, according to your specific use case.