Vite vs Webpack. When speed matters. 2023 update

Vite vs Webpack. When speed matters. 2023 update

What is Vite⚡?

Many developers heard about Vite, and almost everyone knows Webpack. Let's try to answer this question: What is really better in 2023: Vite vs Webpack?

Let's have a look at Vite first.

Vite is a Next Generation Frontend Tooling. It pronounced /vit/ , like "veet".

It has these fantastic features:

  • Instant Server Start

  • Lightning Fast HMR

  • Rich Features

  • Optimized Build

  • Universal Plugins

  • Fully Typed APIs

Vite Dev server

Vite dev server provides many enhancements over native ESM imports:

  • NPM Dependency Resolving and Pre-Bundling

  • Hot Module Replacement

  • TypeScript and TypeScript Compiler Options

  • Proxy

  • Vue/React/

  • JSX, CSS, JSON

  • Static Assets

  • Glob Import

NPM Dependency Resolving and Pre-Bundling

import { someMethod } from 'my-dep'

The above code will throw an error in the browser.

Vite does:

  • Pre-bundle them and convert CommonJS / UMD modules to ESM (ES module). The pre-bundling step is performed with esbuild, making Vite's cold start time significantly faster than any JavaScript-based bundler.

  • Rewrite the imports to valid URLs like /node_modules/.vite/my-dep.js?v=f3sf2ebd so the browser can import them correctly.

  • Cache dependencies via HTTP headers, so it will run pre-bundle only if some of these dependencies have been changed:

  • The dependencies list in your package.json.

  • Package manager lockfiles, e.g. package-lock.json, yarn.lock, or pnpm-lock.yaml.

  • Relevant fields in your vite.config.js, if present.

Hot Module Replacement (HMR)

Vite provides an HMR API over native ESM, which is much faster than Webpack.

  • Instant, precise updates without reloading the page or blowing away the application state.

  • First-party HMR integrations React Fast Refresh.

  • No configuration is needed.

  • It offers the best developer experience and fast page load

TypeScript

Vite supports TypeScript and TypeScript Compiler Options

  • Vite supports importing .ts files out of the box.

  • Vite does NOT perform type checking. You can use the TypeScript compiler to perform type checking. Usually, IDE takes care of it.

  • Vite uses esbuild to transpile TypeScript into JavaScript (about 20~30x faster than vanilla tsc, and HMR updates < 50ms).

  • Some configuration fields under compilerOptions in tsconfig.json require special attention.

  • isolatedModules should be set to true.

  • useDefineForClassFields should be set to true if the TypeScript target is ESNext.

JSX

JSX transpilation is also handled via esbuild and defaults to the React 16 flavour.

React 17 style JSX support in esbuild is tracked here.

CSS

###
@import Inlining and Rebasing

Vite is pre-configured to support CSS @import inlining via postcss-import.

It respects @import for CSS CSS url() references are always automatically rebased to ensure correctness.

PostCSS

If the project contains valid PostCSS config (any format supported by postcss-load-config, e.g. postcss.config.js), it will be automatically applied to all imported CSS.

###
CSS Modules

Any CSS file ending with .module.css is considered a CSS modules file. Importing such a file will return the corresponding module object:

 / example.module.css /
.red {
  color: red;
}
import classes from './example.module.css'
document.getElementById('foo').className = classes.red

SS modules behaviour can be configured via the css.modules option.

If css.modules.localsConvention is set to enable camelCase locals (e.g. localsConvention: 'camelCaseOnly'), you can also use named imports:

// .apply-color -> applyColor
import { applyColor } from './example.module.css'
document.getElementById('foo').className = applyColor

CSS Pre-processors

Vite does provide built-in support for .scss, .sass, .less, .styl and .stylus files. There is no need to install Vite-specific plugins for them, but the corresponding pre-processor itself must be installed:

# .scss and .sass
npm add -D sass
# .less
npm add -D less
# .styl and .stylus
npm add -D stylus

You can also use CSS modules combined with pre-processors by prepending .module to the file extension, for example, style.module.scss.

Static Assets

Importing a static asset will return the resolved public URL when it is served:

import imgUrl from './img.png'
document.getElementById('hero-img').src = imgUrl

Glob Import

Vite supports importing multiple modules from the file system via the special import.meta.glob function:

const modules = import.meta.glob('./dir/*.js')

The above will be transformed into the following:

// code produced by vite
const modules = {
  './dir/foo.js': () => import('./dir/foo.js'),  './dir/bar.js': () => import('./dir/bar.js')}

You can then iterate over the keys of the modules object to access the corresponding modules:

for (const path in modules) {
  modules[path]().then((mod) => {    console.log(path, mod)  })}

Matched files are by default lazy loaded via dynamic import and will be split into separate chunks during the build.

If you'd rather import all the modules directly (e.g. relying on side-effects in these modules to be applied first), you can use import.meta.globEager instead:

const modules = import.meta.globEager('./dir/*.js')

The above will be transformed into the following:

// code produced by vite
import * as __glob__0_0 from './dir/foo.js'
import * as __glob__0_1 from './dir/bar.js'
const modules = {
  './dir/foo.js': __glob__0_0,  './dir/bar.js': __glob__0_1}

Glob Import important info

  • This is a Vite-only feature and is not a web or ES standard.

  • The glob patterns are treated like import specifiers: they must be either relative (start with ./) or absolute (start with /, resolved relative to project root) or an alias path (see resolve.alias option).

  • The glob matching is done via fast-glob - check out its documentation for supported glob patterns.

  • You should also be aware that glob imports do not accept variables; you must pass the string pattern directly.

  • The glob patterns cannot contain the exact quote string (i.e. ', ", ) as outer quotes.

  • e.g. '/Tom's files/` , use "/Tom's files/" instead.

Optimized build with Vite.js

CSS Code Splitting

Vite automatically extracts the CSS used by modules in an async chunk and generates a separate file for it.

When the associated async chunk is loaded, the CSS file is automatically loaded via a tag. The async chunk is guaranteed to be evaluated after the CSS is loaded to avoid FOUC (Flash of unstyled content).

If you'd rather have all the CSS extracted into a single file, you can disable CSS code splitting by setting build.cssCodeSplit to false.

Preload Directives Generation

Vite automatically generates <link rel="modulepreload"> directives for entry chunks and their direct imports in the built HTML.

Async Chunk Loading Optimization (Code-splitting)

When we build the production application, Rollup often generates "common" chunks - code that is shared between two or more other chunks. Combined with dynamic imports, it is pretty common to have the following scenario:

In the non-optimized scenarios, when async chunk A is imported, the browser will have to request and parse A before it can figure out that it also needs the common chunk C. This results in an extra network roundtrip:

Entry ---> A ---> C

Vite automatically rewrites code-split dynamic import calls with a preload step so that when A is requested, C is fetched in parallel:

Entry ---> (A + C)

Vite Plugins

Vite can be extended using plugins, which are based on Rollup's well-designed plugin interface with a few extra Vite-specific options.

This means that Vite users can rely on the mature ecosystem of Rollup plugins, while also being able to extend the dev server and SSR functionality as needed.

How to find Vite plugins

Visit the official plugins page

Adding Plugin

To use a plugin, it needs to be added to the devDependencies of the project and included in the plugins array in the vite.config.js config file. For example, to provide support for legacy browsers, the official @vitejs/plugin-legacy can be used:

npm add -D @vitejs/plugin-legacy
// vite.config.js
import legacy from '@vitejs/plugin-legacy'
import { defineConfig } from 'vite'

export default defineConfig({
  plugins: [
   legacy({
    targets: ['defaults', 'not IE 11']
   })
  ]})

Plugins also accept presets, including several plugins as a single element.

Migration to Vite from Webpack (React Create App)

Reasons

  • Development speed

  • Less complex than Webpack

  • Good support and documentation

  • Growing ecosystem

  • Many frameworks adapt Vite (Laravel, Storybook)

Total number of downloads between 2021-01-01 and 2022-07-16: +31,000,000

Migration Steps

1. Start with Vite local dev server. Keep Webpack as a build tool.

2. Use Vite (Rollup) for the production build of your app.

3. Use Vitest to run tests.

How to use Vite dev server

Install Vite and Plugins

npm i vite vite-plugin-env-compatible vite-tsconfig-paths vite-plugin-svgr --save-dev
  • @vitejs/plugin-react plugin for React projects

  • vite-tsconfig-paths plugin gives vite the ability to resolve imports using TypeScript's path mapping.

  • vite-plugin-env-compatible plugin inject to process.env like vue-cli or create-react-app and also define client "prоcess.env.XXX" for you. By default, Vite exposes env variables on the special impоrt.meta.env object.

  • vite-plugin-svgr plugin to import SVG files as React components using svgr under the hood.

Setup index.html file

Vite treats index.html as source code and part of the module graph.

We need to add another index.html file to the root directory with a script entry for your app:

<script type="module" src="/src/index.ts"></script>

There's no need for special %PUBLIC_URL% placeholders unless you need injections.

In this case, you can use vite-plugin-html.

You can also use another folder than root if you specify in the config or as an option when you run

// package.json...
"dev": "vite serve some/sub/dir --mode dev",

Setup vite.config.js file

When running vite from the command line, Vite will automatically try to resolve a config file named vite.config.js inside the project root.

import { defineConfig, loadEnv } from 'vite'

export default defineConfig(({command, mode})=>{
  // Load env file based on `mode` in the current working directory.
  // Set the third parameter to '' to load all env regardless of the `VITE_` prefix.
  const env = loadEnv(mode, process.cwd(), '')
  return {
    // vite config
    define: {
      __APP_ENV__: env.APP_ENV
    }
  }
})

Enjoy the process: Vite vs Webpack benchmark

  • The first start (cold, no cache) can take from 1 to 5 sec (from my experience)

  • Other runs will take less than a second!

  • HMR will take 200-300ms!

My developer experience with Vite vs Webpack (JavaScript build tool)

Over the last 5 years, I have been working on more than 10 Vue projects. Initially, we used Webpack as a standard tool behind Vue CLI.

When Vite came out, we tried it on the new project and realised how fast it was.

We decided to move the biggest project to Vite next. This project had several thousand files (components, stores, pages, helpers).

With Webpack, even on my Mac with an M1 chip, it took 140+ seconds to start the app locally. After we switched to Vite, the cold start became about 3 seconds. The hot start became less than a second.

That was a really impressive result. We finished migration for the rest of our projects in a couple of sprints. Every new project since this time has always been powered by Vite.

What Issues have we encountered during the migration

  • Some libraries did not have proper ESM builds. We had to use early versions from contributors or contribute ourselves.

  • Webpack folder imports with require.context. We had to switch the implementation to use Glob Import from Vite.

How quick we migrated a project

It took about one month of work for 1 developer to migrate the biggest project, including experimenting and testing.

Related articles

Ruslan Osipov
Written by author: Ruslan Osipov