Notes From Migrating a Large App to Vite
There is no shortage of articles about Vite. Most of them cover the happy path. This one is about what happened after the happy path ended.
When we finally measured, the numbers were worse than anyone guessed. Around 28% of the time spent on a typical change went into working around design tokens rather than building anything.
What was going wrong
Most of the complexity was accidental. Over the years we'd layered three different approaches to design tokens on top of each other, and each one made sense at the time.
We had good intentions. We even had a document describing how design tokens should work. The trouble was that the document and the code had quietly gone their separate ways.
A simpler approach
The key insight was to treat design tokens as part of the interface rather than an implementation detail. Once we did that, most of the other decisions followed naturally.
The core of it looks like this:
export function debounce<T extends (...args: any[]) => void>(fn: T, wait = 200) {
let timer: ReturnType<typeof setTimeout> | undefined;
return (...args: Parameters<T>) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), wait);
};
}
This alone let us delete a surprising amount of code elsewhere in the project.
A few things that helped along the way:
- Make the default the safe choice, not just the documented one.
- Agree on what "done" means before the first pull request.
- Keep the old path around until the new one has survived a real incident.
- Measure before and after, even if it's just a rough number in a spreadsheet.
What it cost us
We still haven't solved Core Web Vitals. That's a post for another day, and honestly, I'm not sure we'll get it right the first time either.
There are cases where this falls apart. If your team is very small or your traffic is very spiky, the extra moving parts may not pay for themselves.
Closing thoughts
Six months in, nobody on the team wants to go back. That's about the best review a change like this can get.