0%

1. Intro


Create React App (CRA) was a good friend. Back in 2018.


It took the pain out of Webpack configuration (you had two options - npx create-react-app my-app or set up Webpack yourself), gave you a reliable build that launched with a single command - npm start - and you just worked. But in 2025, that friend became the kind who drops by for an hour and stays for a week. Slow startup. Slow Hot Module Replacement (HMR). Outdated build tools (Webpack 5 + Babel). And the feeling that you're pulling a cart by hand instead of driving.


We migrated a six-year-old production React project to Vite. Not for the hype - to stop staring at a spinner on localhost every time you hit Cmd/Ctrl+S.


Here's what we ran into along the way - and what we got at the end.


2. Why leave CRA at all?


To put the scale in perspective:



The frog in boiling water. Every new feature makes the build a little slower, but only by fractions of a second.


It's not a catastrophe. It's just slow. And when you do it 200 times a day - start counting the hours of your life disappearing into nothing.


CRA no longer gets active support. Under the hood there's a pile of decisions (Webpack 5, Babel) that nobody's in a hurry to update. The React ecosystem has long been moving toward Vite, and ignoring that is getting harder and harder.


Decision made - migrate. Sounds simple. Turned out - not even close.


3. Problems that we faced down the road


3.1 Environment variables: REACT_APP_* → VITE_*


The first thing to break was .env files. Completely.

CRA only reads variables with the REACT_APP_ prefix and exposes them through process.env. Vite works differently: it needs the VITE_ prefix, and access goes through import.meta.env.



If you have dozens of variables located across the project - get ready for find-and-replace. Plus all your .env, .env.production, .env.staging files need renaming. It can be automated with a script, but first you have to spend time and realize that:



3.2 SCSS import issue


Vite doesn't understand CRA-specific paths like @import './src/_mixins'. It doesn't know how to resolve that path and just crashes with an error.

Solution is to configure additionalData or includePaths in vite.config.js:


Or replace all the problematic imports with relative paths. The second option is cleaner, but the first one is faster if you have a lot of imports.


3.3 Deprecated SASS syntax: @import and if()


SASS deprecated @import in favor of @use and @forward a long time ago (https://sass-lang.com/documentation/breaking-changes/import/). CRA still let it slide - warnings showed up at build time, but everything worked. With the updated SASS in Vite, they start getting in the way of the build.


Same story with the if() function - the syntax changed, and old constructs throw deprecation warnings.



You can suppress the errors via silenceDeprecations in the config - a quick fix actually. But it's better to go through and migrate properly: SASS provides sass-migrator (https://www.npmjs.com/package/sass-migrator) for automatically replacing @import with @use.


3.4 react-pdf and pdfjs-dist: Webpack specific internals


This was the most unpleasant problem. react-pdf internally uses entry.webpack - a path that only exists in a Webpack environment. In Vite it simply doesn't resolve.



The web worker needs to either be copied into public/ with an explicit path, or use import.meta.url for a dynamic reference. The react-pdf docs for Vite exist, but finding them on the first try is its another side quest (https://docs.react-pdf-kit.dev/introduction/basic-usage/#vite).


3.5 SVG import errors


In CRA, SVGs could be imported two ways simultaneously:


In Vite, any .svg is just a URL by default. For component imports you need vite-plugin-svgr (https://www.npmjs.com/package/vite-plugin-svgr). But there's a catch - even with the plugin, you need to configure it properly so both formats work:



3.6 Styled components


If the project used styled-components/macro - they don't work in Vite directly, since macros are a Webpack/babel-macro mechanism.


In most cases a simple import swap fixes it. If you used the css helper from macro - same deal. For more complex cases there's babel-plugin-styled-components via @vitejs/plugin-react with the babel option (https://www.npmjs.com/package/babel-plugin-styled-components).


3.7 Path aliases: @/components


Paths like @/components/Button don't work out of the box in Vite, ESLint, or VSCode. You need to configure three places in sync:


Three files. One alias. Miss even one - it'll work, but with red underlines or ESLint errors.


3.8 require() → import()


Vite runs on standard ES modules. Dynamic require() is not its language.



Static require() is solved by a simple swap to import. Dynamic ones - to import() with await. The hardest part is when require() shows up in legacy utilities or third-party packages - you either need to patch them or find an alternative.


3.9 Replacing Jest with Vitest


Jest and Vite don't play well together. Jest runs on CommonJS, Vite on ES modules. You can configure Jest to work with a Vite project, but it's a pain with transformers and configs.


Vitest (https://vitest.dev/) is the native solution for Vite. The API is nearly identical to Jest, and migration mostly comes down to this:


And swapping imports in tests:


Most tests will just work. Exceptions: snapshot tests and Jest-specific matchers - check such things separately.


4. What we got in the end


Once everything was running, it was time to look at the numbers and they speak for themselves.


4.1 Cold start:

CRA: ~35 seconds

Vite: ~1.5 seconds

HMR (update after Cmd/Ctrl+S):

CRA: 3-8 seconds

Vite: ~100-300ms - the browser updates faster than you can take a sip of coffee.


4.2 Bundle size: roughly the same, but Vite does better tree-shaking by default.


4.3 Developer experience:

  1. Errors show up right in the browser with a readable call stack
  2. HMR is surgical - only the module you changed is updated, not the entire dependency graph
  3. Config is readable and compact - vite.config.ts at 40 lines vs webpack.config.js at 300
  4. TypeScript works out of the box, no extra plugins needed
  5. import.meta is a standard that works everywhere, not just in Webpack


4.4 Ecosystem: Vite today is the standard for new React projects. Remix, SvelteKit, SolidStart, Astro - they all use Vite, which means more documentation, more plugins, and faster bug fixes.


5. Is it worth it?


Yes. But honestly - this isn't a weekend task.


If the project is small with no heavy dependencies, you'll probably get it done in a day or two. If you've got PDFs, SVG components, macros, complex SCSS structures, and a big test suite - plan a week and test thoroughly.


The main thing: don't migrate and develop new features at the same time. Migration first, then stabilize and debug, and then new functionality. Otherwise you'll have no idea what broke - Vite or your new code.


Every of these problems is solvable. Some in 5 minutes, some in half a day. But when everything is working and you hit Save and see the change in the browser in 200 milliseconds - you know it was worth every minute spent.


CRA gave us a solid start. Vite gives us the speed to work today.

Web
Development
Webpack
Vite
Jest
Vitest
React
CRA
Babel
Outsourcing
Outstaffing
Migration

Vadim Sinichenko

-

Related articles

ALL ARTICLES

UI testing & Storybook

UI testing & Storybook

Storybook + Tests = Forced Isolation as a System

Web
Development
2 min. read
February 14, 2026

Such a Different Agile

Such a Different Agile

A case from fintech when you built a car, but the business just wanted to honk the horn louder (or the story of one “slavery” from our lead frontend d

Development
2 min. read
February 24, 2026