Throw away your CRAPublished on 29 June 2022

From CRA to Vite, a case of (much) faster JS compilation.

Create React App.

Three words, so much meaning. Over with the babel and the webpack configs. You write one line - you’re ready to roll with a fully-featured React application. TypeScript? You got it! SVGs imported as React Components? Here you go. Don’t think about the under-the-hood minutiae.

It just runs.

And compiles… Not so fast. See, the biggest issue with CRA - in my humblest of opinions - are its monstrous compile times. Over at Aircall, one of our biggest and baddest front-ends took 93 seconds to compile. A minute and a half. A personal project of mine takes over 20 seconds.

In retrospect, those aren’t horrible numbers, but if you do frequent deploys per day, and in addition to that you run a “test build” pipeline on each branch to make sure your PR won’t break your builds, you might want to cue in that elevator music.

And that’s not even the tip of the iceberg.

The tip of the iceberg, the cherry on top of the cake is react-scripts’ memory utilisation (or “memory John-Wick’ing” - as I do get the impression that every time I build a CRA app, Keanu Reeves is popping holes in my RAM with his ballers).

I’d like us to sit around the fire and pass around a torchlight, so that everyone gets their turn in telling the same story of how they 2x or 4x’d or 6xxxl’d their Circle CI or Gitlab instances just to be able to build a CRA app, but we’ll go another route.

I have a little DigitalOcean droplet that I use as my lab-server to run side projects, experiments, and generally break in all the places. Before you ask, yes, it does run production stuff. Oh, how irresponsible of me.

Some of the projects are small and insignificant enough (and “code-to-learn” enough) that I push the code to a git repo on the droplet, where a bunch of git hooks run build shell scripts and deploy the app. You know, heroku-style deployment. It’s not the be-all end-all of deployment workflows, I’m aware, but I’m not going to spend hours of my time to set up the perfect-est, most bulletproof CI/CD pipeline for a bunch of html and css files (or, in this case, small-ish React projects). For most of them, even this git-hook-enabled workflow is overkill.

DigitalOcean offers a little dashboard for each of its droplets, where you can track its memory consumption. It’s a 2 Gb droplet. And yes, that’s enough to run most websites, people.

It’s not enough, apparently, to build a Create React App project with react-scripts. Here’s the memory consumption for one of my projects building on the droplet after a deploy:

You see that little spike there, on the 04/19, around 00:00? Before you ask me about my nightly coding habits, that’s 100% memory utilisation.

A hundred. Percent.

To build a bunch of html and css and js. (Apologies for the reductionist stance, but at the end of the day, that’s the result I’m looking for and why all those tools we use were created in the first place)

Jeez.

Such spikes can crash the droplet - which happened a bunch of times, rendering it unresponsive for a couple of minutes. That’s more my fault, I admit. Building a project on a production machine… Yeah, I’ll go to CI/CD hell for that.

“Ok Andrey, you’ve made your point”, you’ll say. “What’s the solution?"

I’m happy to say that there is a solution and it’s been gaining traction in the community as of late, and has become my go-to choice for frontend tooling.

Vite

Coming straight from the creator of Vue (Evan You) and Contributors, Vite is a nifty little project that uses Esbuild under the hood (do correct me if I’m wrong) and guarantees next-to-instant improvements to your build times.

I switched one of my own React projects over to Vite with a few config changes and pushed to my DigitalOcean droplet. Here’s the graph:

Memory use sits steadily at 26%. It didn’t even budge.

I’m inspired to call this graph the “Sea of Tranquility”. Which is not a bad name for a technical graph.

So how about it? Let’s switch a Create React App from react-scripts to Vite, shall we? It’s much easier and faster than I expected.

Step 1

Drop react-scripts from your project and add vite.

/* The SVGR plugins are optional, but if you're using SVGs, you're going to need it. */
yarn remove react-scripts
yarn add vite @vitejs/plugin-react vite-plugin-svgr

Goodbye, old friend. We had fun.

Step 2

Move the index.html file from the public directory to the root of your project (next to the src).

Remove all the %PUBLIC_URL% references inside. A search & replace should do it. Example:

<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<!-- The above should become: -->
<link rel="icon" href="/favicon.ico" />

Step 3

Add a vite.config.ts file to your project root. Here are the contents of my config, it should do for a classic CRA application:

import { defineConfig, loadEnv } from 'vite'
import react from '@vitejs/plugin-react'
import svgrPlugin from 'vite-plugin-svgr'

// https://vitejs.dev/config/
export default defineConfig(({ mode }) => {
  return {
    base: '/admin/',
    server: {
      port: 3000,
      hmr: {
        port: 8888, // vite@2.5.2 and newer: clientPort
      },
      proxy: {
        '/graphql': '',
      },
    },
    build: {
      outDir: 'build',
    },
    plugins: [
      htmlPlugin(loadEnv(mode, '.')),
      react(),
      svgrPlugin({
        svgrOptions: {
          icon: true,
          // ...svgr options (https://react-svgr.com/docs/options/)
        },
      }),
    ],
  }
})

Ok, let’s go through the file:

First up is the base option.

I’m serving my React app from a different url. This is akin to the PUBLIC_URL env variable of Create React App.

base: '/admin/'

Next up, the server section. This configures pretty self-explanatory things, such as the port you’re going to be serving your app from, the Hot Module Replacement (hmr) port, and in my case, a proxy, as I’m tapping into a graphql endpoint that runs locally.

server: {
  port: 3000,
  hmr: {
    port: 8888, // vite@2.5.2 and newer: clientPort
  },
  proxy: {
    '/graphql': '',
  },
}

The build section of the config specifies… You guessed it! The build folder. G celebrate, we’ve earned this one.

build: {
  outDir: 'build',
}

Finally, last but not least are the plugins. Seeing as I’m building a React application, I’m using the react plugin, as well as the svgr plugin, to properly bundle in SVGs.

plugins: [
  react(),
  svgrPlugin({
    svgrOptions: {
      icon: true
    },
  }),
]

Step 4

A final note on two things: environmental variables and Typescript.

If you’re using .env files in your Create React App, they need to be properly prefixed.

// Prefix env variables you're going to use in your app with VITE_
VITE_APP_NAME=App
VITE_VARIABLE=/whatever
// ...and so on and so forth.

Then, you need to change how you use those variables inside the application. Gone is process.env, Vite imports env vars a little differently.

// Vite environmental variables from your .env files.
import.meta.env.VITE_APP_NAME
import.meta.env.VITE_VARIABLE

Finally, on Typescript. Everyone’s case might be a little different, but I had to adapt my tsconfig.json a little. 99% of it is the same CRA config I had, but I changed target to ESNext and added vite package types into the types array.

{
  "compilerOptions": {
    "target": "ESNext",
    "lib": [
      "dom",
      "dom.iterable",
      "esnext"
    ],
    "allowJs": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noFallthroughCasesInSwitch": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx",
    "downlevelIteration": true,
    "types": ["vite/client", "vite-plugin-svgr/client"]
  },
  "include": [
    "src"
  ]
}

That’s all, folks!

At least for me, that’s all I had to do in order to switch from Create React App to Vite successfully, and benefit from improved build times both in development and production, and overall greater performance. It took me about a day to get this working, so I hope someone might stumble into this and save themselves some precious time.