Understanding how Vite deals with your node_modules
This is an article about Vite and its features which are related to the node_modules of your projects. Background As a Vite user so far, I guess there might be some questions in your head like: Have you ever been confused about all the Vite configs for your project dependencies? Have you ever wondered how Vite deals with it? Have you ever tried to build a tool based on Vite and found it difficult to deal with the node_modules directory? Have you ever seen a long error message in the browser or terminal that you have no idea what it means? Does a CJS (CommonJS) dependency work in all the Vite cases? Does a dependency in the normal SPA (Single Page Application) mode and the SSR (Server Side Rendering) mode work the same way? In this article, we will explore how Vite deals with them in different modes and scenarios. What is Vite? Vite is a modern frontend build tool that provides a fast and efficient development experience. It uses native ES modules in the browser for development, and bundles your code by Rollup for production. Today, Vite has dominated the frontend world as the most popular build tool. Even It has become the de facto standard for modern frontend development. "Every frontend project has a messy node_modules folder" One of the most magical things about Vite is that it can work with your node_modules folder pretty well. We all know that in modern JavaScript ecosystem, the node_modules folder is a big trouble. It's large, messy, and difficult to manage. It contains all kinds of dependencies, including CJS (CommonJS), ESM (ECMAScript Modules), UMD (Universal Module Definition), and even some non-JavaScript files like CSS, images, etc. It's a nightmare for a build tool to deal with. And one of its biggest challenges is to deal with the CJS code. We all know that Vite is a ESM-first build tool. The whole idea of Vite is to use native ES modules in the browser for development. However, in the real world, CJS is still everywhere, and it's not supported in the browser. However, Vite can handle all the challenges above. Not only handling it precisely, but also performantly. In most of the time, you don't even have to think about it. It just works by default. But I have to say. It's just most of the time. There are still some edge cases that you have to take care of manually. And this is what this article is discussing. How did this article come about? As part of my job, recently I'm working on integrating Vite (also Vitest) into a dev tool called Bit, which originally uses webpack in most of the cases. Basically, Bit is a component-driven development tool for various frontend frameworks and Node.js. In Bit, everything is a component and eventually consumed as an npm package. So technically, you would deal with all kinds of components as packages in your node_modules folder, whatever they are in CJS or ESM, need to be further transformed or not. During the integration, I've met a lot of cases which need extra attentions. I've also had a lot of discussions with the Vite/Vitest team via GitHub issues/PRs/discussions and a little more Discord chats. I'm glad most of them have been eventually figured out. And I also learned a lot of interesting details. I'm thinking, maybe I can write something down, to help more people who also have the same issues, or just want to know more about Vite. So here we are. To be noticed #1 that all the demos have been snapshotted step-by-step on this GitHub repo Jinjiang/reproduction - branch: vite-deps-demo-2025. You can always check it out there. At this moment, the latest Vite version is v6.2.0. To be noticed #2 that in the future Vite will integrate Rolldown as its new core. Technically their config would be different, more precisely saying, simpler. Let's begin. Basic usage Setup Let's start from the official guide to create a React project: BTW, feel free to choose your favorite package manager. Here we use pnpm as an example. $ pnpm create vite │ ◇ Project name: │ vite-project │ ◇ Select a framework: │ React │ ◇ Select a variant: │ TypeScript │ ◇ Scaffolding project in /home/jinjiang/Developer/vite-project... │ └ Done. Now run: cd vite-project pnpm install pnpm run dev Snapshot of the demo project #1 - basic setup After the installation, we can see the node_modules folder is created with all the dependencies you need: cd vite-project pnpm install Let's run the project: $ pnpm run dev VITE v6.2.1 ready in 180 ms ➜ Local: http://localhost:5173/ ➜ Network: use --host to expose ➜ press h + enter to show help Cool, now you can see the React project is running on http://localhost:5173/. To better understand how it works, let's introduce 2 common inspection skills: node_modules/.vite folder and the "Network" panel in browser DevTools. How to inspect 1: the node_modules/.vite/deps folder First, you may have found there

This is an article about Vite and its features which are related to the node_modules
of your projects.
Background
As a Vite user so far, I guess there might be some questions in your head like:
- Have you ever been confused about all the Vite configs for your project dependencies?
- Have you ever wondered how Vite deals with it?
- Have you ever tried to build a tool based on Vite and found it difficult to deal with the
node_modules
directory? - Have you ever seen a long error message in the browser or terminal that you have no idea what it means?
- Does a CJS (CommonJS) dependency work in all the Vite cases?
- Does a dependency in the normal SPA (Single Page Application) mode and the SSR (Server Side Rendering) mode work the same way?
In this article, we will explore how Vite deals with them in different modes and scenarios.
What is Vite?
Vite is a modern frontend build tool that provides a fast and efficient development experience. It uses native ES modules in the browser for development, and bundles your code by Rollup for production. Today, Vite has dominated the frontend world as the most popular build tool. Even It has become the de facto standard for modern frontend development.
"Every frontend project has a messy node_modules
folder"
One of the most magical things about Vite is that it can work with your node_modules
folder pretty well.
We all know that in modern JavaScript ecosystem, the node_modules
folder is a big trouble. It's large, messy, and difficult to manage. It contains all kinds of dependencies, including CJS (CommonJS), ESM (ECMAScript Modules), UMD (Universal Module Definition), and even some non-JavaScript files like CSS, images, etc. It's a nightmare for a build tool to deal with.
And one of its biggest challenges is to deal with the CJS code. We all know that Vite is a ESM-first build tool. The whole idea of Vite is to use native ES modules in the browser for development. However, in the real world, CJS is still everywhere, and it's not supported in the browser.
However, Vite can handle all the challenges above. Not only handling it precisely, but also performantly. In most of the time, you don't even have to think about it. It just works by default.
But I have to say. It's just most of the time. There are still some edge cases that you have to take care of manually. And this is what this article is discussing.
How did this article come about?
As part of my job, recently I'm working on integrating Vite (also Vitest) into a dev tool called Bit, which originally uses webpack in most of the cases. Basically, Bit is a component-driven development tool for various frontend frameworks and Node.js. In Bit, everything is a component and eventually consumed as an npm package. So technically, you would deal with all kinds of components as packages in your node_modules
folder, whatever they are in CJS or ESM, need to be further transformed or not.
During the integration, I've met a lot of cases which need extra attentions. I've also had a lot of discussions with the Vite/Vitest team via GitHub issues/PRs/discussions and a little more Discord chats. I'm glad most of them have been eventually figured out. And I also learned a lot of interesting details.
I'm thinking, maybe I can write something down, to help more people who also have the same issues, or just want to know more about Vite. So here we are.
To be noticed #1 that all the demos have been snapshotted step-by-step on this GitHub repo Jinjiang/reproduction - branch: vite-deps-demo-2025. You can always check it out there. At this moment, the latest Vite version is v6.2.0.
To be noticed #2 that in the future Vite will integrate Rolldown as its new core. Technically their config would be different, more precisely saying, simpler.
Let's begin.
Basic usage
Setup
Let's start from the official guide to create a React project:
BTW, feel free to choose your favorite package manager. Here we use
pnpm
as an example.
$ pnpm create vite
│
◇ Project name:
│ vite-project
│
◇ Select a framework:
│ React
│
◇ Select a variant:
│ TypeScript
│
◇ Scaffolding project in /home/jinjiang/Developer/vite-project...
│
└ Done. Now run:
cd vite-project
pnpm install
pnpm run dev
Snapshot of the demo project #1 - basic setup
After the installation, we can see the node_modules
folder is created with all the dependencies you need:
cd vite-project
pnpm install
Let's run the project:
$ pnpm run dev
VITE v6.2.1 ready in 180 ms
➜ Local: http://localhost:5173/
➜ Network: use --host to expose
➜ press h + enter to show help
Cool, now you can see the React project is running on http://localhost:5173/
.
To better understand how it works, let's introduce 2 common inspection skills: node_modules/.vite
folder and the "Network" panel in browser DevTools.
How to inspect 1: the node_modules/.vite/deps
folder
First, you may have found there is a node_modules/.vite
folder created with some files like this:
$ tree node_modules/.vite
node_modules/.vite
└── deps
├── _metadata.json
├── chunk-ENSPOGDT.js
├── chunk-ENSPOGDT.js.map
├── chunk-RO7GY43I.js
├── chunk-RO7GY43I.js.map
├── package.json
├── react-dom.js
├── react-dom.js.map
├── react-dom_client.js
├── react-dom_client.js.map
├── react.js
├── react.js.map
├── react_jsx-dev-runtime.js
├── react_jsx-dev-runtime.js.map
├── react_jsx-runtime.js
└── react_jsx-runtime.js.map
1 directory, 16 files
This folder is created by Vite to store some temporary files. And it's easy to guess that the deps
subfolder is used to store something related to the dependencies. We will jump into the details later.
How to inspect 2: the "Network" panel in browser DevTools
Second, open the browser DevTools and check the "Network" panel. You will see all the requests to the Vite dev server. And some of them point to this node_modules/.vite/deps
folder.
For example, the source file src/main.tsx
:
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.tsx'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
StrictMode>,
)
is eventually requested into the browser roughly like (after some beautification):
// 'react/jsx-dev-runtime' transformed from jsx
import react_jsxDevRuntime from "/node_modules/.vite/deps/react_jsx-dev-runtime.js?v= ";
// originally 'react'
import react from "/node_modules/.vite/deps/react.js?v= ";
// originally 'react-dom/client'
import reactDom_client from "/node_modules/.vite/deps/react-dom_client.js?v= ";
import "/src/index.css";
import App from "/src/App.tsx";
const jsxDEV = react_jsxDevRuntime["jsxDEV"];
const StrictMode = react["StrictMode"];
const createRoot = reactDom_client["createRoot"];
createRoot(document.getElementById("root")).render(
jsxDEV(StrictMode, {
children: jsxDEV(App, {}, void 0, false, {...}, this)
}, void 0, false, {...}, this)
);
As you can see, all the package imports like import React from 'react'
or import { createRoot } from 'react-dom/client'
are resolved into a path to node_modules/.vite/deps/xxx
.
A small recap
- To inspect the dependencies, you can either check the code in
node_modules/.vite/deps
folder, or check the "Network" panel in browser DevTools. They usually just match each other.
This is the first Vite-magical thing we'd like to discover. More magically, whatever the source of a package import is in CJS or ESM, the eventual requested content will always be in ESM.
A simple case with CJS and ESM
Here, both react
and react-dom
are "bad" examples which are still CJS in 2025, but good examples to show how Vite deals with CJS package imports. To better understand how Vite deals with CJS and ESM package imports in a simpler way, let's create a new package:
mkdir node_modules/foo
touch node_modules/foo/package.json
touch node_modules/foo/foo-cjs.cjs
touch node_modules/foo/foo-esm.mjs
- In
node_modules/foo/package.json
:
{
"name": "foo"
}
- In
node_modules/foo/foo-cjs.cjs
:
exports.foo = 'foo-cjs'
- In
node_modules/foo/foo-esm.mjs
:
export const foo = 'foo-esm'
This will create a package named foo
with 2 files:
-
foo-cjs.cjs
in CJS and -
foo-esm.mjs
in ESM.
Then add 2 imports into src/main.tsx
:
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
// 2 new imports
import { foo as fooCjs } from 'foo/foo-cjs.cjs'
import { foo as fooEsm } from 'foo/foo-esm.mjs'
import './index.css'
import App from './App.tsx'
console.log({ fooCjs, fooEsm });
...
Snapshot of the demo project #2 - a case with both CJS and ESM
Restart the Vite dev server, then you will find these imports have also been resolved into a path like /node_modules/.vite/deps/foo-esm.js?v=
but in different ways:
// first new import
import foo_fooCjs from "/node_modules/.vite/deps/foo_foo-cjs__cjs.js?v= ";
const fooCjs = foo_fooCjs["foo"];
// second new import
import { foo as fooEsm } from "/node_modules/.vite/deps/foo_foo-esm__mjs.js?v= ";
At the same time, the eventual content of these 2 requested files are all in ESM:
-
node_modules/.vite/deps/foo_foo-cjs__cjs.js
:
import { __commonJS } from "/node_modules/.vite/deps/chunk-.js?v= ";
// node_modules/foo/foo-cjs.cjs
var require_foo_cjs = __commonJS({
"node_modules/foo/foo-cjs.cjs"(exports) {
exports.foo = "foo-cjs";
}
});
export default require_foo_cjs();
it imports a builtin helper __commonJS
to wrap the CJS source code into ESM.
-
node_modules/.vite/deps/foo_foo-esm__mjs.js
:
import "/node_modules/.vite/deps/chunk-.js?v= ";
// node_modules/foo/foo-esm.mjs
var foo = "foo-esm";
export {
foo
};
it's almost the same as the original ESM source code.
And the __commonJS()
helper looks like this:
-
node_modules/.vite/deps/chunk-
:.js
var __getOwnPropNames = Object.getOwnPropertyNames;
var __commonJS = (cb, mod) => function __require() {
return mod || (0,
cb[__getOwnPropNames(cb)[0]])((mod = {
exports: {}
}).exports, mod),
mod.exports;
}
;
export { __commonJS };
So overall:
- Vite can recognize whether a package import is in CJS or in ESM, not only per package but also per file, which quite makes sense.
- For ESM package import, it's straightforward, nothing special to do.
- For CJS package import (with
exports.xxx
in it), Vite will transform the package code (node_module/foo/foo-cjs.cjs
) into ESM via default export. And it will be eventually imported as default in the importer file (src/main.tsx
).
Please notice that, even though your CJS package import is in
exports.xxx
which is much closer to named exports in ESM, Vite will always transform it into default export in ESM. That is unituitive but makes sense because in CJS the exports can by complex and dynamic which are difficult to predict into static named imports, while putting all of them into a default export as a deal simplifies the transformation process. And later, we still have chance to distinguish betweenexports.foo = xxx
andmodule.exports = xxx
by a flag__esModule
.
The case with module.exports = xxx
in CJS
At last, you might also be curious about another kind of CJS package import module.exports = xxx
.
Great question! Let's try it now:
touch node_modules/foo/foo-cjs-module.cjs
- In
node_modules/foo/foo-cjs-module.cjs
:
module.exports = 'foo-cjs-module'
- And then add the import into
src/main.tsx
:
import fooCjsModule from 'foo/foo-cjs-module.cjs'
Snapshot of the demo project #3 - module.exports in CJS
Restart the Vite server, now you will find the importer in the eventual requested content:
import fooImport from "/node_modules/.vite/deps/foo_foo-cjs-module__cjs.js?v= ";
const fooCjsModule = fooImport.__esModule ? fooImport.default : fooImport;
That means, Vite will try to access the default
property of the CJS package import if there is a __esModule
flag in it. Otherwise, it will use the import itself directly.
and the imported file looks like:
-
node_modules/.vite/deps/foo_foo-cjs-module__cjs.js
:
import { __commonJS } from "/node_modules/.vite/deps/chunk-.js?v= ";
// node_modules/foo/foo-cjs-module.cjs
var require_foo_cjs_module = __commonJS({
"node_modules/foo/foo-cjs-module.cjs"(exports, module) {
module.exports = "foo-cjs-module";
}
});
export default require_foo_cjs_module();
Actually, even for the case you use exports.xxx
in CJS package import, the resolved importing code would be in the same logic:
import fooCjsAll from 'foo/foo-cjs.cjs'
will be resolved into:
import fooImport from "/node_modules/.vite/deps/foo_foo-cjs__cjs.js?v= ";
const fooCjs = fooImport.__esModule ? fooImport.default : fooImport;
// `fooCjs.foo` will be 'foo-cjs'
which also works as expected.
Snapshot of the demo project #4 - default import with exports.xxx in CJS
A small recap
- All the package imports will be resolved into imports to a path like
/node_modules/.vite/deps/xxx
. - For ESM package import, it's more straightforward, almost nothing extra.
- For CJS package import, Vite will transform the package code into ESM with default export. And eventually, the consuming import code will be pointed to the default import as well.
The code inside node_modules/.vite/deps/
is generated by Vite which we call "dependency pre-bundling" or "dependency optimization". Next, let's dive into it and see how to customize it for your own needs.
Custom Dependency Pre-bundling
What is vite optimize
and optimizeDeps
config in Vite?
You may have noticed that, on Vite official docs, there is a vite optimize
command. The job is actually part of what Vite dev server does. It can figure out all the needed dependencies in node_modules
and then "pre-bundles" them into node_modules/.vite/deps/
. Then all the package imports like react
can be resolved to the new optimized imports like node_modules/.vite/deps/react.js
instead of the original one.
The rough steps of this "optimize" process are:
- Traverse all the source code from the entry file of your project (usually it's the
index.html
) and find out all the imports tonode_modules
. and then draw a "optimization boundary" around them.- e.g. in the project above, the analyzation prcess would be:
-
index.html
: importssrc/main.tsx
-
src/main.tsx
: importsreact/jsx-dev-runtime
(boundary),react
(boundary),react-dom/client
(boundary),./App.tsx
,./index.css
,foo/foo-cjs.cjs
(boundary),foo/foo-esm.mjs
(boundary),foo/foo-cjs-module.cjs
(boundary) -
./App.tsx
: importsreact/jsx-dev-runtime
(boundary),react
(boundary),./assets/react.svg
,/vite.svg
,./App.css
-
./index.css
: no more imports -
./App.css
: no more imports -
./assets/react.svg
: no more imports -
./vite.svg
: no more imports
-
- Then the full "optimization boundary" would be:
-
react/jsx-dev-runtime
(invisible from source code) -
react
(invisible from source code) react-dom/client
foo/foo-cjs.cjs
foo/foo-esm.mjs
foo/foo-cjs-module.cjs
-
- e.g. in the project above, the analyzation prcess would be:
- Pre-bundle all the dependencies on the "optimization boundary" list into
node_modules/.vite/deps/
folder. This step is done by esbuild.
After those, we are able to replace all the package imports into the optimized imports.
You usually don't have to do this because Vite dev server will do it automatically for you. Just in case you need to force it to be done or rebuilt, you can also run vite --force
to force the rebuild.
Why we need dependency optimization?
In short:
- To support CJS package imports in the browser.
- To reduce the number of requests to the server for better performance.
What is optimizeDeps
config in Vite?
Now, it's time to understand why we have a optimizeDeps
config in Vite. This config is designed to give you more control over the "optimization boundary" settings. For example, you can exclude some packages from the boundary, or include more packages into the boundary.
This is very useful when Vite somehow is not able to do the analyzation correctly, or you want to skip some packages from the pre-bundling process for special reasons. For example, you may want to change the content of a certain package in a special case. Then, it's not recommended to pre-bundle it. Because once you have changed the content, the whole pre-bundle needs to be rebuilt, not to mention Vite may not rebuild it immediately during the dev server running. Even when you restart it, Vite sometimes can't detect the changes. So you have to force the rebuild manually to make it right.
Back to the demo project, first of all, let's disable the automatic analyzation by setting optimizeDeps.noDiscovery
to true
in vite.config.ts
:
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vite.dev/config/
export default defineConfig({
optimizeDeps: {
noDiscovery: true,
},
plugins: [
react(),
],
})
Snapshot of the demo project #5 - optimizeDeps.noDiscovery
Then restart the Vite dev server by pnpm run dev --force
. You will see your web app not working anymore with a browser runtime error:
Uncaught SyntaxError: The requested module '/node_modules/.pnpm/react-dom@19.0.0_react@19.0.0/node_modules/react-dom/client.js?v=4eaecffa' does not provide an export named 'createRoot'
If you inspect the "Network" panel now, you will find the package imports like import { createRoot } from 'react-dom/client'
in src/main.tsx
are not resolved into node_modules/.vite/deps/xxx
anymore. They will be pointed to the original package imports:
import { createRoot } from "/node_modules/.pnpm/react-dom@19.0.0_react@19.0.0/node_modules/react-dom/client.js?v= ";
import { foo as fooCjs } from "/node_modules/foo/foo-cjs.cjs?import&v= ";
import { foo as fooEsm } from "/node_modules/foo/foo-esm.mjs?v= ";
import fooCjsAll from "/node_modules/foo/foo-cjs.cjs?import&v= ";
import fooCjsModule from "/node_modules/foo/foo-cjs-module.cjs?import&v= ";
And You can also see the eventual content of react-dom/client.js
is in CJS which doesn't work in browser. Now we know that Vite itself can't deal with CJS code directly without the "optimization".
To make the app works again, let's add all the package imports of the "optimization boundary" into the optimizeDeps.include
config:
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vite.dev/config/
export default defineConfig({
optimizeDeps: {
noDiscovery: true,
include: [
`react-dom/client`,
'foo/foo-cjs.cjs',
'foo/foo-esm.mjs',
'foo/foo-cjs-module.cjs',
],
},
plugins: [
react(),
],
})
Snapshot of the demo project #6 - fix with optimizeDeps.include
Now, all the package imports are resolved back to node_modules/.vite/deps
. And the web app is working again.
Among these optimizeDeps.include
items, you actually can remove the ESM package imports like foo/foo-esm.mjs
because the browser can work with them natively without the optimization:
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vite.dev/config/
export default defineConfig({
optimizeDeps: {
noDiscovery: true,
include: [
`react-dom/client`,
'foo/foo-cjs.cjs',
// 'foo/foo-esm.mjs',
'foo/foo-cjs-module.cjs',
],
},
plugins: [
react(),
],
})
Snapshot of the demo project #7 - remove ESM from optimizeDeps.include
Another way to achieve the same effect is to remove noDiscovery
and include
fields to turn all of them on but exclude the ESM package imports from the "optimization boundary" by setting optimizeDeps.exclude
:
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vite.dev/config/
export default defineConfig({
optimizeDeps: {
exclude: [
'foo/foo-esm.mjs',
],
},
plugins: [
react(),
],
})
Snapshot of the demo project #8 - equivalent optimizeDeps.exclude
This also means, in node_modules
, only ESM content can be technically modified without the pre-bundle rebuild. Otherwise, all the code changes in node_modules
need a forced pre-bundle rebuild to make it works. However, to support the advanced features like HMR (Hot Module Replacement), you need something else. This is framework-specific and won't be covered in this article.
A small recap:
- By default, Vite will automatically analyze the project and draw a "optimization boundary" around all the package imports. The "optimization boundary" is used to pre-bundle the dependencies into ESM and put them into
node_modules/.vite/deps
. - Option
optimizeDeps.noDiscovery
can be set to disable the default deps analyzation. - Option
optimizeDeps.include
andoptimizeDeps.exclude
are used to opt-in/opt-out certain package imports to/from the "optimization boundary". - Command
vite optimize
andvite --force
can be used to do the optimization manually.
And it's easy to get those tips below:
- Never exclude CJS package imports.
- If you want to include all the dependencies by default and exclude some exceptions, just use
optimizeDeps.exclude
. - If you want to manually list the "boundary" from zero, use
optimizeDeps.noDiscovery
+optimizeDeps.include
instead.
Advanced case: deep dependencies configuration
Now, let's think about an advanced case: what if I'd like to exclude a package import like foo/foo-esm.mjs
but include (optimize) one of its package imports to a deep dependency (in CJS)?
Let's create a deep dependency foo-dep-a
with a CJS file foo-dep-a-cjs.cjs
:
mkdir node_modules/foo/node_modules
mkdir node_modules/foo/node_modules/foo-dep-a
touch node_modules/foo/node_modules/foo-dep-a/package.json
touch node_modules/foo/node_modules/foo-dep-a/foo-dep-a-cjs.cjs
- In
node_modules/foo/node_modules/foo-dep-a/package.json
:
{
"name": "foo-dep-a"
}
- In
node_modules/foo/node_modules/foo-dep-a/foo-dep-a-cjs.cjs
:
exports.fooDepA = 'foo-dep-a-cjs'
- And then add the import into
node_modules/foo/foo-esm.mjs
:
import { fooDepA } from 'foo-dep-a/foo-dep-a-cjs.cjs'
export const foo = fooDepA
Snapshot of the demo project #9 - a case of deep deps
Restart the server, you will see the browser runtime error when you opt-out foo/foo-esm.mjs
since the new CJS file isn't optimized.
To fix it, we can use this syntax to include the deep dependency:
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vite.dev/config/
export default defineConfig({
optimizeDeps: {
include: [
'foo > foo-dep-a/foo-dep-a-cjs.cjs',
],
exclude: [
'foo/foo-esm.mjs',
],
},
plugins: [react()],
})
Snapshot of the demo project #10 - fix with deep optimizeDeps.include
For the optimizeDeps
config, besides noDiscovery
, include
, and exclude
, there are also some other options like entries
, esbuildOptions
, holdUntilCrawlEnd
, needsInterop
, etc. You can check the official docs for more details.
Also, to force the pre-bundle rebuild, there is also a config optimizeDeps.force
which does the same thing as vite --force
. It's very useful for debugging purposes. We will turn it on by default in the following sections.
I guess so far everything is still quite clear and straightforward. And nothing is uncovered from the official docs yet, right? Next, let's dive into the SSR (Server Side Rendering) world. I bet you will find something really interesting and surprising.
Basic SSR usage
The SSR mode will let your project run more logics on the server side, which is quite different from the default (SPA) mode. In short, comparing to SPA, SSR will additionally:
- render the app into a html string on the server, and
- run most of the code in the server environment like Node.js rather than only in the browser.
This brings more technical challenges, especially when it comes to the node_modules
in a mixed CJS and ESM world. Let's see how Vite deals with it.
Setup
First thing first, let's turn this project into a SSR one. For demonstration, we will do it in a minimal way:
- Create 2 entries:
src/client.tsx
andsrc/server.tsx
instead of the originalsrc/main.tsx
. - Update
index.html
to includesrc/client.tsx
instead ofsrc/main.tsx
. at the same time, add a placeholder inside theas
for the server-rendered content replacement.
- Add a
run.mjs
file to run the whole service. - Add a
dev:ssr
npm scripts inpackage.json
withnode ./run.mjs
. - And don't forget to install
express
andcompression
which are used inrun.mjs
.
Some particular file changes are:
-
src/client.tsx
:
import { createRoot, hydrateRoot } from 'react-dom/client';
import './index.css'
import App from './App';
const root = document.getElementById('root');
if (import.meta.env.SSR) {
hydrateRoot(root!, (<App />));
}
else {
createRoot(root!).render(<App />);
}
-
src/server.tsx
:
import { renderToString } from "react-dom/server";
import App from './App';
export const render = async () =>
renderToString(<App />);
-
run.mjs
:
import { readFileSync } from "node:fs";
import express from "express";
import compression from 'compression'
import { createServer } from "vite";
const PORT = 5173;
// create Vite dev server in middleware mode
const devServer = await createServer({
server: { middlewareMode: true },
appType: "custom",
});
// create the main server with an express app
const app = express();
app.use(compression());
app.use(devServer.middlewares);
// handle all the requests with the Vite dev server
app.use("/", async (req, res, next) => {
const url = req.originalUrl;
try {
// 1. get the index.html template
// 2. transform the template with some necessary setup
// 3. render the app into a string
// 4. replace the final html with the rendered app string
const template = readFileSync('./index.html', "utf-8");
const tranformedTemplate = await devServer.transformIndexHtml(url, template);
const { render } = await devServer.ssrLoadModule('./src/server.tsx');
const appHtml = await render();
const html = tranformedTemplate.replace(``, appHtml);
res.statusCode = 200;
res.setHeader("Content-Type", "text/html");
res.end(html);
} catch (error) {
next(error);
}
});
// listen to the port
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});
Snapshot of the demo project #11 - SSR setup
Now you can run the project with either pnpm run dev
or pnpm run dev:ssr
. It will run the web app on http://localhost:5173
in normal (SPA) or SSR mode. And you may already guess out in SSR mode, Vite deals with node_modules
far more differently from SPA mode.
What is ssr.external
and ssr.noExternal
config in Vite?
Imagine there could be all kinds of content in node_modules
which are:
- JavaScript files