The long path to vite

The long path to vite

The long path to vite

This is a post-mortem of our choice to switch from create-react-app to vite for our frontend tooling, and what you can expect from a similar move.

The big questions

First, we need to ask ourselves why go to the trouble of swapping one for the other. Is it worth the effort? The short answer is 'yes'! To understand the answer, we need to look at what create-react-app does for us.

The History

New web developers might not understand the purpose of these complex build tools we have now. Let's look at some brief history.

Long ago, there was Browserify. You could write JavaScript code that used node/CommonJS require(), and Browserify would bundle it up for you into one package. Those who you needed more complex tasks started using gulp and friends to write workflows. And then came the great JavaScript updates. With ES2015 and above, JavaScript gained new, exciting features that developers wanted to take advantage of, but with the slow moving browser-usage landscape, backwards compatibility was a risk. Babel emerged as a champion to ward off these fears and compile newer JavaScript to ES5 and below.

At this point, there were too many things to do before you could chuck some code into production. To make things worse, SPA libraries had picked up steam, and the amount of JavaScript you'd write for an average website exploded, with more logic happening client-side, similar to a real 'application'. You wanted to use node modules, Sass, JSX, Babel, TypeScript, bundle, minify the resultant CSS and JS, and much more. The demand for more manageability resulted in the creation of Webpack.

Webpack's promise was great. It handled all your build logic in one place, and was going to make our lives easier. And to be fair, it did. But it never got rid of all the complexity we'd built on top of, just made it a tad bit more manageable. It was still complex enough that we joked that configuring Webpack for a new React app was a project in of itself. Sure, template repositories came up, and you could clone one and get started, but at some point you still likely needed to mess with the configuration, which means you needed to understand Webpack.

create-react-app

Here's where create-react-app makes its entry. As an abstraction-over-an-abstraction in our pyramid of tooling, CRA preconfigured a 'standard' Webpack configuration that'd make most React developers happy. Then it hid it behind a node module so you never have to look at it again. Coming from developers of React itself, it was to be kept updated with what the community needed, and it made everyone happy. If you were not comfortable with this arrangement, you could 'eject' it and deal with the underlying Webpack, Babel and friends' configurations yourself.

This made life simpler, but it was bloated and slow, and lots of developers were still left unhappy. The vuejs team had gone down a similar route with vue-cli, although they made their configurations extensible without having to eject. Other alternatives like Parcel had come around, offering even 'simpler' tooling.

vite

Evan You, the developer of vuejs, was not happy with the way things were. With his team, he started working on “the next generation of front end tooling” that solved all our problems.

XKCD: Standards

Mandatory XKCD.

However, vite, unlike our previous contendors, promised to undo some of the complexity that had built up. Although initially designed for use with vuejs, and as a potential replacement for vue-cli some day, vite opened up its borders, and all frontend developers could take advantage of it. Instead of Webpack, Babel, and TypeScript, it used Rollup, and esbuild. While CRA served its dependencies as-is, resulting in a deeply nested devDependency tree that spanned over 35,000 files, vite bundled much of itself, leaving a lean installation. There are arguments on both sides of the channel here, whether vite distributing a bundle instead of npm dependencies was the right thing™ or not, but the benefits to the end user were absolute.

Like before, vite comes preconfigured with almost everything a frontend developer might need, but it's also quite easy to extend, since it uses Rollup's plugin and configuration format.

But the biggest advantage by far is the dev server and build time. After migrating to vite, our devserver launch dropped from almost a minute to mere milliseconds. It takes just about 300-500ms to launch, since vite does not bundle during dev. During production builds we have even higher benefits, as our Netlify build time dropped from 3:45 minutes to 1:15 minutes. Most of the remaining time is Netlify setting up the environment.

Add to this that vite transparently supports much of your existing tooling. Need Sass? Just install Sass, and vite supports it. Need CSS modules? Vite supports it. Need postcss? Just install postcss. Need TypeScript? Vite supports it. Since it uses esbuild at dev time, it skips typechecking altogether, and relies on your editor/IDE to provide that for you. After all, why duplicate the effort? Full typechecking is done during production builds. If you need anything more, you're a simple plugin away.

You can even start incrementally migrating your existing project to TypeScript with very little friction, by simply renaming your files to TS and hacking away at it. Sure, you could do this before, but the development setup cost was unreasonable. Now you can have an optional tsconfig, and you're good to go. Devserver and production builds all supported!

So we switched to vite, what then?

The vite flow is different from what you'd generally observe with Webpack and friends. Instead of your JavaScript being the entrypoint, vite treats your HTML as the entrypoint to your application. From here you can import your JS with <script>, import, whatever. You can even import JSX, TypeScript, TSX, and vite takes care of it for you. In fact, because of this feature, vite can actually be used for developing static sites as well! Even if you don't use React, Vue, Svelte, and the like, vite can still help you with hot-reloading, bundling, etc.

There are some things that break while migrating to vite, much in part because of how loosely strict our old tooling was. If your dependencies have cyclic dependencies, or if they rely on implicit global pollution, and so on, you might run into problems. This isn't a fault of vite itself, but it is what it is.

Something more to keep in mind is that react-app does more than just building. It also sets up 'best practices' ESLint config, testing, and so on. While we were tempted to keep react-app as a devDependency purely for its ESLint config, we decided against it, and assumed responsibility for it ourselves, in exchange for dropping a huge number of implicit devDependencies. By pulling eslint-config-react-app as a direct devDependency, we had to assume its peerDependencies as well. This resulted in us having to bring in the following:

  • eslint-plugin-flowtype
  • eslint-plugin-import
  • eslint-plugin-jsx-a11y
  • eslint-plugin-react
  • eslint-plugin-react-hooks
  • babel-preset-react-app
  • babel-eslint
  • @typescript-eslint/eslint-plugin
  • @typescript-eslint/parser

You might notice that eslint-plugin-flowtype is unnecessary for us, as we don't use Flow, but eslint-config-react-app needs it, and hence will error out unless we satisfy its peerDependencies. This was one of the unattractive bits of the migration, but overall, it was well worth it.

vite was part 1/2 of our developer environment revamp, and as part of this, we also switched from yarn to pnpm for our package management. Next time, we'll see a breakdown of the points that led to that decision, and how you could benefit from it.