Why (and how) we migrated from Create React App to Vite

In this article:
Subscribe to our blog:

We migrated our SPA from Create React App to Vite and Jest to Vitest to escape security vulnerabilities, dependency constraints, and slow builds

Every software project built on a solid foundation eventually feels the ground start to shift.

For years, Create React App (CRA) was our reliable bedrock. But over time it became a source of technical debt, security concerns, and developer friction.

We were stuck with outdated dependencies, accumulating security vulnerabilities, and a build process that was slowing us down.

It was time to tear out the old foundation and build something new, escape a deprecated toolchain, and build a more secure approach for evolving the Codacy platform.

TL;DR

We migrated Codacy's Single Page Application from the deprecated Create React App to Vite and Jest to Vitest, which allowed us to:

  • Eliminate critical security vulnerabilities from outdated webpack dependencies
  • Unblock library upgrades that were previously impossible
  • 80% faster CI workflows (15 min → 3 min) and improved test performance
  • Measurable code quality improvements: 5 issues fixed, 1550 complexity reduction

This post details our migration strategy, the challenges we faced, and why moving away from deprecated tooling was crucial for our long-term success.


Why we made the switch


The ticking clock of technical debt

The pain points of our CRA setup were no longer minor annoyances; they were active risks and roadblocks:

  • Deprecation and abandonment: With the React team officially stepping back from maintenance, CRA was no longer actively evolving. We were building on a toolchain with no clear future.
  • Accumulating vulnerabilities: We were forced to patch our package.json with 11 forced dependency resolutions just to mitigate security vulnerabilities in legacy dependencies like webpack. This was a constant, reactive battle.
  • Development gridlock: These patches and outdated dependencies blocked us from upgrading other libraries. We couldn’t adopt modern React features or leverage the performance of modern build tools, effectively freezing our ability to innovate.

What Vite brings to the table

Vite represented a clear path forward, directly addressing our biggest concerns and future-proofing our development environment:

  • Security and maintenance: As an actively maintained project with a modern dependency tree, Vite immediately eliminated the legacy webpack vulnerabilities that had plagued us.
  • Developer experience: The difference was night and day. Dev server startups dropped from ~15 seconds to under 3, and Hot Module Replacement (HMR) became nearly instantaneous.
  • A solid foundation: Vite’s architecture, built on modern tools like esbuild and Rollup, gave us the stable, forward-looking foundation we needed.

How Vite compares to CRA:

Aspect
CRA
Vite

Dev server startup

~15 seconds

~3 seconds

HMR speed

1-3 seconds

<100ms

Maintenance status

🔴 Deprecated

🟢 Active

Dependencies

Legacy webpack

Modern esbuild + Rollup


Our migration strategy


Phase 1: Dependencies and security

The goal was to clean up the dependency tree and establish a solid foundation. The most significant change was updating our package.json:

Key wins:

  • Eliminated critical security vulnerabilities by removing deprecated webpack dependencies
  • Reduced forced resolutions from 11 to 5
  • Unblocked library upgrades that were previously impossible with CRA
  • Modernized to ES modules with "type": "module"
  • Future-proofed our build system for upcoming React features

Phase 2: Build configuration revolution

The goal was to replace CRA's hidden webpack configuration with a visible, customizable Vite setup.

Core Vite configuration
HTML template migration

One of the more straightforward parts was migrating from CRA's public/index.html to Vite's root-level approach:

Key changes:

  • File location: public/index.html → index.html (project root)
  • Environment variables:
    • %REACT_APP_*% → %VITE_*%
    • %PUBLIC_URL% → / (absolute paths)
  • Add explicitly <script type="module"> tag

Phase 3: Testing infrastructure overhaul

The goal was to migrate from Jest to Vitest while maintaining test reliability. Key changes and benefits included:

  • Mocking syntax: All jest.mock() calls became vi.mock()
  • Global imports: jest globals replaced with vi from 'vitest'
  • Dramatically faster CI/CD - our test workflow dropped from 15 minutes to 3 minutes
Mocking strategy transformation

The biggest challenge was rewriting our mocking approach. Vitest uses a different mocking philosophy:

Why this took time:

  • 200+ test files needed updates across the project
  • Complex mocks required complete rewrites
  • Test utilities needed significant updates
  • Timing differences: Vitest's async behavior required adding many async/await and Promise handling that Jest didn't need

The challenges we overcame


1. Environment variables: from implicit to explicit

CRA automatically injects all REACT_APP_* environment variables, but Vite requires explicit configuration.

We created a comprehensive mapping system that handles both legacy process.env and modern import.meta.env patterns:

Migration Path:

  1. Support both process.env.* and import.meta.env.*
  2. Gradually replace process.env.* usage
  3. Remove legacy process.env.* definitions

2. SVG handling: from built-in to plugin-based

CRA has built-in SVG support, but Vite requires explicit configuration. We tackled this by creating different SVG configurations for different environments:


3. Export patterns: export * caused bundle issues

Vite's tree-shaking is more aggressive than webpack's, causing issues with barrel exports.

As such, we used explicit, named exports for better tree-shaking:

Why this mattered:

  • Bundle size: Prevented unused hooks from being included
  • Performance: Better code splitting and lazy loading

4. Test infrastructure: a complete rewrite

Our extensive Jest setup didn't translate directly to Vitest, so we rebuilt our testing infrastructure from the ground up:


Now we're faster, safer, and future-proof

This migration was about more than just speed. The benefits touched every part of our development process:

  • Security and Maintainability: We can now keep our dependencies current without workarounds, backed by a toolchain that evolves with the modern web.
  • Developer Productivity: A lightning-fast development server and an 80% reduction in CI wait times means our team spends more time building and less time waiting.
  • Code Quality and Freedom: We're no longer blocked from adopting React 18+ features. Better tree-shaking from Vite has helped us identify and remove unused code, and we now have the freedom to adopt new libraries as they become available.

Moving from CRA to Vite wasn't just a simple upgrade; it was an escape from a deprecated ecosystem that was actively creating security risks and holding us back.

The journey was challenging, but the result is a faster, safer, and more maintainable application, with a development team empowered to build the future.

The payoff is worth it. 🚀

RELATED
BLOG POSTS

Github Integration: Issues & Comments
In order to improve your code, GitHub integration has been a key step. We want to make it easier for you to integrate our service into your development...
Create Jira Tickets for Issues in Codacy
An oft-asked-for feature has finally landed in Codacy; now, you can create Jira tickets for one or multiple Codacy issues from within the Codacy UI...
Adding Collaboration
This is a blog post of our Code Reading Wednesdays from Codacy (http://www.codacy.com): we make code reviews easier and automatic. During the last...

Automate code
reviews on your commits and pull request

Group 13