Back to AI guides

OJ: Lovable's Rust Rewrite of the Vite Dev Server

Stanley Ulili
Updated on September 29, 2026

Lovable runs roughly one million short-lived preview sandboxes every day. Each one has to start a Vite development server, wait for it to become ready, serve the preview, and then shut down again.

At small scale, that startup cycle is easy to ignore. At Lovable’s scale, it becomes expensive. Every sandbox brings the overhead of starting Node.js, loading the toolchain, and allocating memory, even though many of those environments exist only briefly.

That is the problem OJ, short for Orange Juice, was built to solve. Created by Lovable engineer Raphael Amorim, OJ is a Rust-based development server designed specifically to reduce the cost of these short-lived preview environments.

Lovable reports 4x lower memory usage and 2x faster cold starts compared with its previous setup. Those numbers are achievable in the workload OJ was designed for, where startup time and memory are multiplied across enormous numbers of ephemeral environments.

On a typical developer-sized project, however, the difference is much less dramatic. The interesting part is understanding why OJ produces such large savings at Lovable’s scale, and how much of that advantage carries over to ordinary local development.

That distinction is what this article explores.

What OJ is and isn't

OJ is not a complete rewrite of Vite. It's a targeted rewrite of one specific layer: the dev server. The file watcher, module graph, HMR logic, React Fast Refresh, and WebSocket handling all run as Rust. The bundler and parser are still Rolldown and Oxc, the same Rust-based tools that Vite itself uses, maintained by VoidZero (Evan You's team).

The OJ GitHub repository page, showing the project's orange juice carton logo and its tagline as a "Rust-native build tool for React apps."

OJ reads your existing vite.config.ts without requiring a separate config file. It runs your existing Vite plugins through a compatibility bridge: a small Node process handles JavaScript-based plugins while the Rust core takes care of everything else. The result is that it works as a drop-in for most React projects. Excalidraw and Twenty, a CRM frontend with around 15,000 modules, both run on OJ without modification.

The project is at github.com/lovablelabs/oj under Lovable's GitHub organization.

Side-by-side on a typical project

Installing OJ and running it alongside Vite takes three steps. Add it to your project:

 
npm install -D @lovable/oj

Add a script to package.json:

package.json
"scripts": {
  "dev": "vite",
  "dev:oj": "oj dev"
}

Then compare:

 
npm run dev
 
npm run dev:oj

On a moderately complex React project with TanStack Start (SSR, file-based routing, server functions), the two servers produce near-identical behavior:

A side-by-side view of a complex TanStack application. The left window is served by OJ, and the right by Vite. Both appear and function identically at first glance.

SSR, client-side hydration, server functions, Tailwind CSS, asset imports, environment variables, file-based routing, and dynamic routes all work out of the box.

Where compatibility diverges: Fast Refresh state

The first notable difference is Fast Refresh behavior. Vite correctly preserves component state when you edit and save a file: edit a counter component's text, and the counter value stays where it was. OJ resets state on every file change. For editing workflows, this is a meaningful difference. It's a known limitation that Raphael Amorim has flagged as a near-term fix.

Memory on a typical project

On this size of project, memory usage at rest:

  • Vite: ~331 MB across 2 processes
  • OJ: ~297 MB across 2 processes

A split-screen showing the memory consumption stats. The Vite server on the right reports 331 MB, while the OJ server on the left reports 297 MB.

A 10-15% improvement is real but not dramatic. For a single local developer, this doesn't matter much. For Lovable running thousands of concurrent instances, it starts to.

Where the performance claims actually hold: large scale

OJ's real advantage appears at scale and with large applications. On a synthetic 5,000-component React app, benchmarking cold start to fully painted page:

Setup Cold start Memory
Vite 8.3 (default) 2.39s 913 MB
OJ 0.2.2 (normal) 1.40s (1.7x faster) 221 MB (4.1x less)
Vite 8.3 (bundled dev mode) 1.18s 993 MB
OJ 0.2.2 (bundle mode) 0.89s 217 MB

A bar chart displaying the benchmark results for a large application, clearly showing OJ's significant lead in both lower cold start time and reduced memory usage.

OJ's normal mode uses a quarter of the memory of standard Vite. Vite's bundled dev mode closes the cold start gap but uses even more memory. OJ's bundle mode delivers the best of both.

One important caveat Evan You raised: the Vite cold start numbers in these benchmarks include vite-plugin-checker, which runs tsc in a background worker for browser type-error overlays. OJ doesn't run that plugin, so the speed comparison is not apples-to-apples on cold start. The memory comparison is more straightforward and is where You concedes OJ's advantage is clear.

In production at Lovable, the numbers are more striking: median sandbox acquisition time dropped from 14.5 seconds to 3 seconds, and the dev server process runs on roughly 6.5x less memory than Node/Vite before the app even loads.

Agent-aware update batching

OJ's design includes one feature that's specifically relevant for AI coding agents. A human developer edits one file, saves, and waits. An AI agent might write or modify ten files in a rapid burst.

Vite's watcher sees ten separate save events and triggers ten partial updates in quick succession, potentially producing flickering intermediate states.

An animated diagram showing that 10 file saves from an "agent" trigger 10 separate updates in Vite, but are coalesced into a single, efficient update in OJ.

OJ coalesces a burst of file changes into a single atomic update, applying one coherent change once the burst settles. It also exposes a /flush endpoint: an agent can hold updates by not flushing, then POST to /flush when all its file changes are complete. This gives the agent explicit control over when the preview updates, ensuring it never shows a partially valid state.

Evan You's response

Vite's creator posted a measured analysis of OJ. His points are worth reading in full since they frame the project correctly:

A screenshot of the full, multi-point tweet from Evan You (@youyuxi) titled "Thoughts on oj."

He calls OJ "impressive" and correctly identifies that it's not a Vite competitor but a specialized projection of Vite's own future direction. He acknowledges the memory advantage is real and says it's "surely something we should aim to improve in Vite itself."

He also raises the benchmark caveat about vite-plugin-checker and notes that with full bundle mode, Vite's cold start metrics are much closer to OJ's.

His most forward-looking point is about what OJ signals for open source. With the cost of re-implementation falling because of AI, it's becoming easier for teams to build what he calls "tailored projections" of open-source dependencies. Rather than sending pull requests to maintain a generalist tool, a team can fork and maintain a version optimized for their exact constraints. He isn't sure if this fragmentation is good or bad, but he thinks it's the direction the ecosystem is heading.

When to use OJ

For a single developer working on a typical project, standard Vite is still the safer default. OJ is broadly compatible, but not perfectly so, and issues such as Fast Refresh state resets can become noticeable in day-to-day development. At that scale, the performance gains are usually not large enough to justify the extra edge cases.

OJ makes more sense when your workload starts to resemble Lovable’s. Its biggest advantages show up when you are running many short-lived development environments, where every second of startup time and every megabyte of memory has a direct infrastructure cost.

It is also a stronger fit for large React applications, where the memory savings become more meaningful, and for AI-driven development environments where agents are changing multiple files at once. In those cases, OJ’s atomic update batching can reduce preview flicker and make the feedback loop feel more stable.

So OJ is worth evaluating if you are building an AI coding platform, running sandboxed previews at scale, or trying to minimize memory usage in a Vite-compatible development server. For ordinary local development, though, Vite remains the more practical choice.

Got an article suggestion? Let us know
Licensed under CC-BY-NC-SA

This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.