English
  • parcel
  • vite
  • js
  • esbuild
  • bundler
  • monorepo

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.

Gao
Gao
Founder

The backstory

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:

  • Parcel sometimes failed to bundle the project because it couldn't find some chunk files that were actually there.
  • It needed some hacky configurations to make it work with our monorepo setup.
  • It doesn't support MDX 3 natively, so we had to create a custom transformer for it.
  • It doesn't support manual chunks (as of the time of writing, the manual chunks feature is still in the experimental stage), which is okay for most circumstances, but sometimes you need it.

So why did we decide to migrate to something else?

  1. We were stuck with Parcel 2.9.3, which was released in June 2023. Every time a new version was released after that, we tried to upgrade, but it always failed with build errors.
  2. The latest version of Parcel was 2.12.0, released in February 2024. Although it has nearly daily commits, no new release has been made since then.

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.

Parcel upgrade pull requests

Trust me, we've tried.

Why Vite?

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.

The status quo

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:

  • Monorepo: We use PNPM (v9) workspaces to manage our monorepo.
  • Module: We use ESM modules for all our projects.
  • TypeScript: We use TypeScript (v5.5.3) for all our projects with path aliases.
  • React: We use React (v18.3.1) for all our frontend projects.
  • Styling: We use SASS modules for styling.
  • SVG: We use SVGs as React components.
  • MDX: We have MDX with GitHub Flavored Markdown and Mermaid support.
  • Lazy loading: We need to lazy load some of our pages and components.
  • Compression: We produce compressed assets (gzip and brotli) for our production builds.

The 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.

Out-of-the-box support

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.

Path alias

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.

React Fast Refresh and HMR

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:

SVG as React component

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.

SASS modules

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:

MDX support

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.

Mermaid support within MDX

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:

Lazy loading

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.

Compression

There's a neat plugin called vite-plugin-compression to produce compressed assets. It supports both gzip and brotli compression. The configuration is simple:

Manual chunks

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:

Dev server

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:

Environment variables

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.

Conslusion

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: