From Parcel to Vite: A short story of a 100K LOC migration
We've migrated our three frontend projects from Parcel to Vite, and the process was... smooth.
We've migrated our three frontend projects from Parcel to Vite, and the process was... smooth.
We have three main frontend projects at Logto: the sign-in experience, the Console, and the live preview. These projects are all in TypeScript, React, and SASS modules; in total, they have around 100K lines of code.
We loved Parcel for its simplicity and zero-config setup. I can still remember the day when I was shocked by how easy it was to set up a new project with Parcel. You can just run parcel index.html and boom, all necessary dependencies are installed and the project is running. If you are an "experienced" developer, you may feel the same way comparing it to the old days of setting up with Gulp and Webpack. Parcel is like a magic wand.
The simplicity of Parcel was the main reason we stuck with it for so long, even though it could be moody sometimes. For example:
So why did we decide to migrate to something else?
Someone even opened a discussion to ask if Parcel is dead. The official answer is no, Parcel is still alive, but it's in a we-are-working-on-a-large-refactor-and-no-time-for-minor-releases state. To us, it's like a "duck death": The latest version we can use is from more than a year ago, and we don't know when the next version will be released. It looks like it's dead, it acts like it's dead, so it's dead to us.

We knew Vite from Vitest. Several months ago, we were tired of Jest's ESM support (in testing) and wanted to try something new. Vitest won our hearts with the native ESM support and the Jest compatibility. It has an amazing developer experience, and it's powered by Vite.
You may have different settings in your project, but usually you will find plugin replacements as the Vite ecosystem is blooming. Here are our setups at the moment of migration:
We started the migration by creating a new Vite project and playing around with it to see how it works. The process was smooth and the real migration only took a few days.
Vite has out-of-the-box support for monorepo, ESM, TypeScript, React, and SASS. We only needed to install the necessary plugins and configurations to make it work.
Vite has built-in support for path aliases, for example, in our tsconfig.json:
We only needed to add the same resolution in our vite.config.ts:
Note the replacement path should be an absolute path, while it is relative to the project root. Alternatively, you can use the vite-tsconfig-paths plugin to read the path aliases from the tsconfig.json.
Although Vite has built-in support for HMR, it is required to install a plugin to enable React Fast Refresh. We used the @vitejs/plugin-react plugin which is provided by the Vite team and has great support for React features like Fast Refresh:
We use the vite-plugin-svgr plugin to convert SVGs to React components. It's as simple as adding the plugin to the Vite config:
However, we didn't specify on which condition the SVGs should be converted to React components, so all the imports were converted. The plugin offers a better default configuration: only convert the SVGs that are imported with the .svg?react extension. We updated our imports accordingly.
Although Vite has built-in support for SASS modules, there's one thing we need to care about: how the class names are formatted. It may be troublesome for users and our integration tests if the class names are not formatted consistently. The one-line configuration in the vite.config.ts can solve the problem:
Bt the way, Parcel and Vite have different flavors of importing SASS files:
The * as syntax, however, works in Vite, but it will cause the loss of modularized class names when you use dynamic keys to access the styles object. For example:
Since Vite leverages Rollup under the hood, we can use the official @mdx-js/rollup plugin to support MDX as well as its plugins. The configuration looks like this:
The remarkGfm plugin is used to support GitHub Flavored Markdown, and the rehypeMdxCodeProps plugin is used to pass the props to the code blocks in the MDX files like what Docusaurus does.
We would like to use Mermaid diagrams in our MDX files as other programming languages. The usage should be as simple as other code blocks:
Should be rendered as:
Since our app supports light and dark themes, we coded a little bit to make the Mermaid diagrams work with the dark theme. A React component is created:
useTheme is a custom hook to get the current theme from the context. The mermaid library is imported asynchronously to reduce the loading size for the initial page load.
For the code block in the MDX file, we have a unified component to do the job:
Finally we define the MDX provider as follows:
This isn't a Vite-specific thing, it's still worth mentioning since we updated our pages to use lazy loading during the migration, and nothing broke afterward.
React has a built-in React.lazy function to lazy load components. However, it may cause some issues when you are iterating fast. We crafted a tiny library called react-safe-lazy to solve the issues. It's a drop-in replacement for React.lazy and a detailed explanation can be found in this blog post.
There's a neat plugin called vite-plugin-compression to produce compressed assets. It supports both gzip and brotli compression. The configuration is simple:
One great feature of Vite (or the underlying Rollup) is the manual chunks. While React.lazy is used for lazy loading components, we can have more control over the chunks by specifying the manual chunks to decide which components or modules should be bundled together.
For example, we can first use vite-bundle-visualizer to analyze the bundle size and dependencies. Then we can write a proper function to split the chunks:
Unlike the production build, Vite will NOT bundle your source code in the dev mode (including linked dependencies in the same monorepo) and treat every module as a file. For us, the browser will load hundreds of modules for the first time, which looks crazy but it's actually fine in most cases. You can see the discussion here.
If it's a thing for you, an alternative-but-not-perfect solution is to list the linked dependencies in the optimizeDeps option of the vite.config.ts:
This will "pre-bundle" the linked dependencies and make the dev server faster. The pitfall is that HMR may not work as expected for the linked dependencies.
Additionally, we use a proxy which serves the static files in production and proxies the requests to the Vitest server in development. We have some specific ports configured to avoid conflicts, and it's also easy to set up in the vite.config.ts:
Unlike Parcel, Vite uses a modern approach to handle environment variables by using import.meta.env. It will automatically load the .env files and replace the variables in the code. However, it requires all the environment variables to be prefixed with VITE_ (configurable).
While we were using Parcel, it simply replaced the process.env variables without checking the prefix. So we have come up with a workaround using the define field to make the migration easier:
This allows us to gradually add the prefix to the environment variables and remove the define field.
That's it! We've successfully migrated our three frontend projects from Parcel to Vite, and hope this short story can help you with your migration. Here's what the configuration looks like in the end: