Most developers use Vite and trust it. It's fast, well-designed, and gets out of the way.
But there's a fundamental assumption baked into Vite's architecture — and every other major build tool — that we decided to challenge when building Ionify.
The assumption: every build starts from zero.
This post is a deep look at what that means in practice, what we built instead, and the architectural tradeoffs we made along the way.
The Stateless Build Problem
When you run vite build, here's roughly what happens:
- Vite spins up esbuild/Rollup (now powered by Rolldown + Oxc instead of pure Rollup/Babel in many cases)
- Each file goes through its plugin chain independently
-
main.tsx→ TS plugin → JSX plugin → output -
utils.ts→ TS plugin → output -
index.css→ CSS plugin → output - Everything gets bundled
The next time you run it? Same thing. All of it.
There's no memory of what already ran. The TS plugin doesn't know the JSX plugin already processed a file. The bundler doesn't know which files actually changed since last time. The entire pipeline is stateless by design.
This isn't a bug — it's a deliberate architectural choice that keeps the tool simple and predictable. But it has a real cost at scale.
How Ionify Thinks About This Differently
Instead of transforming files, Ionify addresses them.
Every module gets a content hash:
SHA-256(source content + config version) → CAS key
The result lives in a Content-Addressable Store (CAS):
.ionify/
cas/
<configHash>/
<moduleHash>/
transformed.js
transformed.js.map
Same content + same config = same hash = skip the transform entirely.
Not faster transforms. No transforms at all — for everything that hasn't changed.
The Persistent Graph
The other half of the equation is the dependency graph.
Vite reconstructs its module graph on every dev server start. Ionify persists it.
.ionify/
graph.db ← sled-backed embedded KV store (Rust)
When a file changes, Ionify runs a BFS over the reverse dependency index to find exactly which modules are affected:
This means on a 500-module project, changing one utility file might only invalidate 12 modules — not all 500.
Version Isolation: Config Changes Invalidate Everything
One subtle problem with persistent caches: what if your config changes?
Ionify solves this with a deterministic version hash computed from the config:
The Four-Tier CAS Architecture
As the system evolved, we ended up with four distinct caching layers, each solving a different problem:
Tier 1 — Module Transform Cache
Tier 2 — Deps Artifact Store
Tier 3 — Compression CAS
Pre-compressed build outputs. Brotli-11 + gzip-9 computed once, served forever.
Tier 4 — Chunk-Output CAS
Real Config: Migrating from Vite
Here's an actual ionify.config.ts from a production project that migrated from Vite:
import { defineConfig } from 'ionify'
export default defineConfig({
entry: '/src/main.tsx',
server: {
https: true,
},
resolve: {
// Same aliases as tsconfig — Ionify reads these natively
// Note: if your tsconfig.json is JSONC (comments/trailing commas),
// auto-alias parsing currently fails — specify manually here
alias: {
'@@': '/',
'@@/Core': '/Core/src',
'@': '/src',
},
extensions: ['.mjs', '.js', '.mts', '.ts', '.jsx', '.tsx', '.json'],
conditions: ['import', 'module', 'browser', 'default'],
mainFields: ['module', 'jsnext:main', 'jsnext', 'main'],
},
optimizeDeps: {
include: ['moment-hijri'], // pre-warm known-problem deps
sharedChunks: 'auto',
packSlimming: 'auto',
vendorPacks: 'auto',
},
build: {
target: 'esnext',
},
})
This project has more than +10K module and the results of build time at vite roll-down was 3.7 seconds every time after migrating to Ionify. The time was reduced to 2.2 seconds on warm build, and 3.2 seconds for cold build
The Fundamental Difference
Vite is built around the assumption that each build is independent.
Ionify is built around the assumption that most of the work you did last time is still valid.
Both assumptions are reasonable. Vite's leads to a simpler, more predictable system. Ours leads to a system where the second build, the tenth build, and the CI build all benefit from everything that happened before.
The "With Ionify" side of the transform diagram is mostly empty. Not because we're hiding complexity — but because most of the transforms simply don't run.
Ionify is currently in production use. → ionify.cloud
Top comments (0)