Currently, many people learning React use tools like Create React App (CRA) or Vite to easily create projects and develop Single Page Application (SPA) based applications. While these tools allow you to quickly start React projects, React internally has very complex configurations and structures, and directly configuring these settings with JavaScript is more complex and difficult than you might think.
CRA and Vite serve the role of automating these complex environment configurations. CRA is a tool released by Facebook in 2016 that automatically generates the basic structure and configuration for React projects. However, recently it has been officially announced that it will no longer be updated and will maintain its current state. On the other hand, Vite is a build tool that appeared in 2020, gaining popularity by highlighting fast development speed as its strength.
Why are these two tools always compared?
CRA and Vite are frequently compared because their transpilers and bundlers, which are the most core internal structures for running React in browsers, are different, resulting in very different purposes and performance. To understand what transpilers and bundlers are, we first need to briefly look at how React projects work.
We usually write JSX or TSX files when creating project components and configure these components to run in browsers. However, this process is not simply about writing and executing code. To make React work properly in browsers, two key components are needed: transpilers and bundlers. How these two elements work and how CRA and Vite handle them differently affects the development experience and performance.
Transpiling
Can code written in JSX or TSX be executed directly in browsers? No, it cannot be executed directly. Since browsers basically don't understand JSX or TSX syntax, it must be converted to JavaScript that browsers can understand, and this process is called transpiling.
Bundling
Bundling refers to the process of combining multiple JS files into one so that browsers can receive them all at once. However, if JSX or TSX has been converted to JavaScript through a transpiler, this code can be executed directly in browsers. Then, why is bundling necessary? The answer to this question is related to the characteristics of SPA (Single Page Application), which React mainly uses, and the HTTP protocol. SPA refers to an application configured to switch between multiple screens within one page, allowing various content to be displayed without page refresh. To do this, it's common to download all necessary JavaScript files in advance when users first load the app. This is because it enables smooth operation during page transitions.
HTTP is basically a TCP-based protocol, and certain connection/disconnection costs occur each time data is exchanged. It's not simply that smaller files are faster; when requests increase, communication overhead increases accordingly.
For example, suppose you need to send 3 messages to a friend.
And suppose it takes the following time each time you send a message:
Basic transmission time: 1 second
Transmission time based on message length: Varies according to message content length
If you send messages one by one separately,
1st message: Basic 1 second + length-based time 1 second = 2 seconds
2nd message: Basic 1 second + length-based time 1 second = 2 seconds
3rd message: Basic 1 second + length-based time 1 second = 2 seconds
→ Total 6 seconds
But if you bundle and send all 3 messages at once
Basic 1 second + length-based time 3 seconds = Total 4 seconds
In other words, even small data can be slower when sent in multiple parts, and this is the core reason for bundling.
The same applies to JavaScript files. When transmitted in multiple parts, overhead occurs with each request, reducing network efficiency. Therefore, bundlers combine multiple JS files into one so browsers can receive them all at once. This can provide benefits such as improved load speed, initial rendering optimization, and traffic reduction.
CRA and Vite use different tools to run React projects, and the procedures in Dev Mode and Production Mode are also different.
As shown in the diagram, CRA uses babel and Webpack for transpiling and bundling, while Vite uses esbuild and rollup for transpiling and bundling. Additionally, CRA and Vite show differences in the procedures for rendering projects to browsers in both Dev Mode and Production Mode.


Among these, esbuild is a high-performance build tool written in Go language, which is much faster than Babel in terms of speed. This gives Vite a significant advantage in build performance, and also, Vite skips bundling in development mode. Instead, it processes code in module units and uses a method of loading only necessary modules when needed (HMR-based, HMR explanation is introduced below). This relates to the necessity of bundling explained earlier and the reason why complete bundling is not necessary in development environments.
As a result, because Vite skips bundling in development mode, it provides much faster execution speed than CRA, and also, due to performance differences in the transpilers themselves (esbuild vs Babel), not only are there differences in processing steps, but there are also noticeable performance differences in initial loading time and HMR response speed.
esbuild and Babel show distinct differences in transpiling performance because their development purposes and internal structures are different from the start. The reason why CRA and Vite chose different transpilers also stems from differences in tool design philosophy and performance orientation.
Babel
Babel is a relatively early transpiler, developed in JavaScript and structured based on CommonJS. While it has advantages of flexible structure and a broad ecosystem, due to JS's inherent limitations like single-thread processing and runtime performance limits, its transpiling speed is relatively slow.
esbuild
esbuild is a high-performance transpiler written in Go that appeared relatively recently. Based on Go's high-speed compilation characteristics and multi-thread parallel processing capabilities, it boasts much faster build speeds than Babel. Because it appeared relatively recently, while it has performance advantages, it has the disadvantage that its ecosystem is not as broad as Babel's.
Webpack and Rollup show fundamental philosophical differences in how they handle bundling tasks. Webpack provides granular control through a role-separated configuration system, while Rollup integrates all functions into a single plugin-centered system.
Loaders: File Transformation Engine
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/
}
]
}
Loaders work in a pipeline manner. When they detect specific file patterns, they transform those files into a form that JavaScript can understand. They handle tasks like converting TypeScript to JavaScript, SCSS to CSS, and images to Base64.
Vendor Splitting: Caching Optimization Strategy
optimization: {
splitChunks: {
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendor',
chunks: 'all'
}
}
}
}
Vendor splitting aims to minimize cache invalidation. It separates external libraries that don't change frequently (React, Lodash, etc.) into separate chunks, so library caches remain even when application code changes.
Plugins: Build Process Extension
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html'
}),
new MiniCssExtractPlugin({
filename: '[name].[contenthash].css'
})
]
Plugins intervene at specific points in the build lifecycle to perform additional tasks. They handle all auxiliary functions not covered by loaders, such as HTML file generation, CSS extraction, and bundle analysis.
Actually, when you create a basic React project with CRA and check webpack.config.js with the npm run eject command, you can see this 3-stage structure clearly separated. Looking at CRA's webpack.config.js, while there's a lot of code, you can confirm that loader, optimization, and plugin are clearly separated within the exported function. This structure shows clear separation of concerns and clearly demonstrates that each configuration area has independent responsibilities. File transformation is handled by loaders, bundle optimization by optimization, and build process extension by plugins.
This allows developers to granularly control complex build processes step by step, and when modifying or extending specific functions, they can focus intensively on the relevant area only.
Unlike CRA's Webpack, Rollup integrates all functions into plugins, handling everything plugin-centrically at once.
export default {
input: 'src/index.js',
output: {
file: 'dist/bundle.js',
format: 'esm'
},
plugins: [
// File transformation (Webpack's loader role)
typescript(),
babel({
babelHelpers: 'bundled'
}),
// External dependency handling (Webpack's vendor role)
nodeResolve({
browser: true
}),
commonjs(),
// Build optimization (Webpack's plugin role)
terser(),
serve({
contentBase: 'dist',
port: 3000
})
]
}
Webpack's advantages:
Rollup's advantages:
Ultimately, the difference in design philosophy where Webpack targets complex applications while Rollup targets libraries or simple apps creates these architectural differences. Webpack's 3-stage separation system is for the granular control required in large-scale projects, while Rollup's plugin-centered structure reflects a philosophy pursuing efficiency and simplicity.
Both Webpack and Rollup are 'bundling tools' that bundle source code into JavaScript files. However, many module systems exist in JavaScript, and Webpack bundles source code based on Runtime Module System, while Rollup bundles based on ES6 module system.
In other words, even with the same source code, the results of bundling with Webpack and bundling with Rollup are different.
Simple source code exists, and we compared the results after bundling with webpack and rollup.
//math.js
export function add(a, b) { return a + b; }
export function multiply(a, b) { return a * b; }
export function unused() { return 'unused'; }
// main.js
import { add } from './math.js';
console.log(add(2, 3));
webpack result (you can just skim through this with your eyes):
/******/ (() => { // webpackBootstrap
/******/ "use strict";
/******/ var __webpack_modules__ = ({
/***/ "./math.js":
/*!*****************!*\
!*** ./math.js ***!
\*****************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
__webpack_require__.r(__webpack_exports__);
__webpack_require__.d(__webpack_exports__, {
"add": () => (/* binding */ add),
"multiply": () => (/* binding */ multiply),
"unused": () => (/* binding */ unused)
});
function add(a, b) { return a + b; }
function multiply(a, b) { return a * b; }
function unused() { return 'unused'; }
/***/ }),
/***/ "./main.js":
/*!*****************!*\
!*** ./main.js ***!
\*****************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
__webpack_require__.r(__webpack_exports__);
var _math_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./math.js */ "./math.js");
console.log((0,_math_js__WEBPACK_IMPORTED_MODULE_0__.add)(2, 3));
/***/ })
/******/ });
/************************************************************************/
/******/ // Runtime code (module loader system)
/******/ var __webpack_require__ = {};
/******/
/******/ __webpack_require__.d = (exports, definition) => {
/******/ for(var key in definition) {
/******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
/******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
/******/ }
/******/ }
/******/ };
/******/
/******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
/******/
/******/ __webpack_require__.r = (exports) => {
/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
/******/ }
/******/ Object.defineProperty(exports, '__esModule', { value: true });
/******/ };
/******/
/******/ // Module execution start
/******/ __webpack_require__("./main.js");
/******/ })();
rollup result:
function add(a, b) { return a + b; }
console.log(add(2, 3));
As mentioned earlier, because Webpack and Rollup have different module systems and development philosophies, you can confirm that the results are completely different even when bundling the same code.
Runtime Module System has the advantage of enabling dynamic imports and code splitting to load only necessary modules at runtime, and has good compatibility with legacy code because it's CommonJS-based. On the other hand, the ES6 module system can generate smaller bundles by removing unnecessary code at build time through static analysis.
Specific differences in output
Code structure:
Runtime overhead:
Tree shaking:
Looking at the comparison above, you can see that in performance aspects like speed, file size, and build procedures, Webpack shows some weakness compared to Vite. This is somewhat natural since Vite was developed relatively recently compared to Webpack and incorporates more modern technologies.
Nevertheless, Webpack continues to be used in many projects as it has been consistently used for a long time. This long history means more than just the passage of time - it has significant meaning in that stability has been proven through numerous tests and extensibility is excellent with a vast plugin ecosystem. Also, the existence of diverse communities and contributors serves as a major advantage. Having stability, flexibility, and a community to discuss problems with during development are considered very important factors, so I think projects based on Webpack continue to be developed.
For this reason, especially in financial sectors or large corporations where stability is the top priority and complex requirements must be handled frequently, Webpack is still often preferred. On the other hand, in startups where deployments happen multiple times a day and fast development speed is important, or companies working on relatively simple structured projects that don't require complex build configurations, Vite is increasingly being used.
Of course, just because Webpack is more stable and has a larger community doesn't mean Vite is unstable or lacks community support. Each tool has its own advantages, and the important thing seems to be choosing the build tool that fits the nature and requirements of the current project.
In this article, we compared two representative transpilers and bundlers. Understanding internal operating methods helps not only user experience but also greatly improves developer experience. Knowing the characteristics of various tools enables better tool selection according to project situations.
Just because you're familiar with a currently used tool doesn't mean you don't need to know other tools at all. Having basic understanding of competing tools allows you to make better technical decisions. It's worthwhile enough in that it broadens the scope of technology choices.
I hope this article helped you feel more familiar with build tools. I hope it provided an opportunity to reflect on what roles the tools you usually use have played, and I hope it helps you decide what criteria to use when choosing tools for new projects in the future.