Previous state
Create-react-app application takes around 3 minutes to build, requiring around 3GB of RAM.
Why Vite
- We wanted a quick frictionless migration (so picking a framework like Next is out of scope);
- We wanted to avoid low level tools. We want something well-mantained with a good preset out of the box;
- It looks like Vite achieved these goals, other similar tooling might have too;
Migration tweaks
This might change a bit depending on what kind of stuff you have in your project. Here's what we had:
Initial setup
- Vite's docs doesn't have any article about "migrating from an existing project"; So I have launched a starter project and copied the following files:
vite.config.ts
-
tsconfig.json
(adapt accordingly) tsconfig.node.json
- Review the
package.json
and remove anything related to Create React App, Babel or Webpack. For instance:react-scripts
- Also replace the package.json's scripts accordingly. Ex:
"vite": "vite",
"start": "vite",
"build": "vite build",
"preview": "vite preview",
"test": "vitest --run",
"test:watch": "vitest",
- Add Vite (
yarn add vite
). Update TS to the latest version since you don't have CRA locking you to an ancient version anymore;
React plugin
One of the first things to add is the React plugin in the Vite Config. (@vitejs/plugin/react
).
Below is shown the final version of the vite config:
/// <reference types="vitest" />
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { resolve, parse } from 'path';
import * as fs from 'fs';
import svgr from 'vite-plugin-svgr';
const rootPaths = fs.readdirSync('src').reduce((out, item) => {
const parsed = parse(item);
return { ...out, [parsed.name]: resolve('src', item) };
}, {});
// https://vitejs.dev/config/
export default defineConfig({
plugins: [svgr(), react()],
resolve: {
alias: rootPaths,
},
envPrefix: 'REACT_APP',
test: {
globals: true,
environment: 'happy-dom',
},
});
Path mapping
In CRA, folders at the source root can be acessed as absolute paths.
- I.e.
/src/ListComponent/Somefile.ts
can be imported as import Somefile from 'ListComponent/Somefile'
This special handling doesn't exist on Vite. I have then manually stitched this mapping on vite config's resolve.alias
setting.
const rootPaths = fs.readdirSync('src').reduce((out, item) => {
const parsed = parse(item);
return { ...out, [parsed.name]: resolve('src', item) };
}, {});
export default defineConfig({
// ..
resolve: {
alias: rootPaths,
},
});
SVG Imports
Create React App embeds the "SVGR" library. If you use any import like...
import { ReactComponent as MySvg } from './file.svg'
...then this won't work anymore.
A frictionless fix was to add the vite-plugin-svgr
shown above (found in a Stack Overflow reply).
Vite is based on Rollup, and SVGR provides its own Rollup plugin,
@svgr/rollup
. Unfortunately this seemed not to work properly here because Vite already embeds a static file URL plugin, which takes higher precedence. Even following the provided steps to use it alongside the URL plugin didn't work well.
Environment variables
Vite doesnt read environment variables from process.env
, but rather from import.meta.env
; Also, the NODE_ENV
variable is found on the import.meta.env.mode
, which is set according to the build environment used (Vite dev server, Vite build or vitest);
Some badly tasting environment variables like BROWSER=none
or PORT
won't be needed anymore (Vite's server accepts a --port
argument like 99% of other software in the world).
The default environment variable safe prefix is VITE_APP
instead of REACT_APP
. This can be changed on the envPrefix
setting (as shown above), as to avoid some refactoring.
Type defs
If you previously had written a strictly typed process.env
, you may need to move those types to the corresponding global interfaces ImportMetaEnv
and ImportMeta
, as shown on the environment variable docs.;
We also need to replace the build tool types. On react.app-env.d.ts, replace:
- /// <reference types="react-scripts" />
+ /// <reference types="vite/client" />
The index.html
The index.html
now lives in the root folder. It also requires a new script tag on its body, pointing to the project root:
<body>
<div id="root"></div>
<script type="module" src="/src/index.tsx"></script>
</body>
Also, any %PUBLIC_URL%
tags must be removed.
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
Refactor sync require()'s
On webpack you could still get away with writing a synchronous CommonJS require()
anywhere. On Vite this simply won't work out (unless maybe with a plugin);
Default build folder
The default build folder on Vite is dist
instead of build
. This can be tweaked with build.outDir
.
Testing
The quickest way around testing is probably swithing to Vitest, as the Jest test runner kinda relies on Babel/Webpack;
We still kept Jest on the project, we're just not using its test runner anymore. Other parts of Jest like assertions or mocks are still there.
Vitest reads from the same config file (vite.config.ts
). You need to add its type directive for TS not to complain:
// on vite.config.ts:
/// <reference types="vitest" />
As shown before, we needed a couple extra settings on the "test" key.
test: {
globals: true,
environment: 'happy-dom',
},
-
globals adds the mocha-like globals (
describe
,test
, etc) to the context; - environment allows you to enable JSDOM or other;
- When you set an environment, the CLI will suggest you to separately install it.
ESLint
Many ESLint plugins that were previously bundled with CRA had to be manually installed and added.
- @typescript-eslint/eslint-plugin
- @typescript-eslint/parser
- eslint-plugin-jsx-a11y
- eslint-plugin-react
- eslint-plugin-react-hooks
We ended up with something like this on the eslint.config
:
{
"root": true,
"parser": "@typescript-eslint/parser",
"plugins": [
"@typescript-eslint",
"jsx-a11y"
],
"extends": [
"plugin:react/recommended",
"plugin:react-hooks/recommended",
"plugin:import/recommended",
"plugin:import/typescript",
],
"settings": {
"react": {
"version": "17.0"
}
}
}
Build and development
The Vite Dev server does not automatically include TS checking. It suggests you to run tsc
on the build task (tsc && vite build
). The tsconfig is already suggested with a noEmit
.
While you may probably add tsc
to the build through a plugin, in the end I think it is better not to, since VSCode already runs its own TS Server. Running tsc
on the development server creates a duplicate TS Server.
In case you'd like to check errors project-wide:
- you may still run
tsc -w
- or use a VS Code Task: F1 > Run Build Task > tsc - watch
Since type checking and building are now separate tasks, you may run them on parallel on the CI.
Performance feelings
Build time went to around 25 seconds down from 3 min (could be lower if I hadn't disabled SMT on my processor); While Webpack uses only a single core during most of the build, Vite displays some average activity on all cores.
Peak memory usage went to 1,2GB, down from ~3GB.
- The development server starts right away, since it actually didn't compile anything. Pages are compiled as you load them (similar to what happens on Next.js). The development mode may not feel THAT fast on a first page load, since every dependency is served individually. If you look at the requests pane, you can see an enormous number of files being served;
- Nonetheless, it is orders faster than Webpacks 3-minute build-of-everything;
- Only the files required by a specific page are compiled and served;
- This also means that when performing HMR, only the changed files are re-served. HMR feels more responsive;
- This may also mean that once the first load is done, the browser may leverage caching of individual files on its side;
- On the production mode, the files are bundled more like it happens on other traditional tools. Development and production builds are considerably different from each other. The differences are explained right on the first page of the docs.
Top comments (0)