THE DEV SPECTRUM

Back to Spectrum

Why Do We Build React Apps for Production Instead of Running npm run dev?

8 min read #Frontend PK Pramod Krishna

Have you ever wondered why we develop a React application using:

npm run dev

but when it is time to deploy the exact same application, everyone suddenly tells us to run:

npm run build

Why can't we simply install Node.js on the production server, run npm run dev, and call it a day?

After all, the application works perfectly fine on our laptop.

This is a very reasonable question, especially when you're starting to learn how frontend applications are deployed.

The short answer is:

You can run npm run dev in production. But you usually shouldn't.

To understand why, we need to look at what actually happens when we develop and build a modern frontend application.


What Actually Happens When You Run npm run dev?

Let's take a typical React application created using Vite.

Your project might look something like this:

my-react-app/
├── public/
├── src/
│   ├── components/
│   ├── App.jsx
│   └── main.jsx
├── package.json
└── vite.config.js

During development, you probably run:

npm run dev

Your package.json might contain:

{
  "scripts": {
    "dev": "vite"
  }
}

When you execute this command, Vite starts a development server.

Conceptually, the architecture looks like this:

Browser

   │ HTTP

┌──────────────────────┐
│ Vite Development     │
│ Server               │
│                      │
│ • Transforms JSX     │
│ • Resolves modules   │
│ • Generates maps     │
│ • Watches files      │
│ • Provides HMR       │
└──────────┬───────────┘


       src/*.jsx

This development server does quite a lot of work for us.

For example, browsers don't directly work with our development environment in exactly the same way that we write it.

We may be using:

  • JSX
  • TypeScript
  • npm packages
  • environment variables
  • imported CSS
  • React components
  • development source maps

Vite sits between our source code and the browser and provides the development experience around all of this.


The Development Server Is Built for Developers

There's another important feature you probably use every day without thinking much about it.

Suppose you change:

<h1>Hello World</h1>

to:

<h1>Hello Universe</h1>

and save the file.

The browser updates almost immediately.

You didn't rebuild your application.

You didn't restart the server.

You probably didn't even refresh the browser.

That's because tools such as Vite provide Hot Module Replacement, commonly called HMR.

The development server watches your files:

You edit App.jsx


Vite detects the change


Transforms the changed module


Sends the update to the browser


Browser updates the application

This is fantastic while developing.

But think about production for a moment.

Are your users editing App.jsx?

No.

Do production users need HMR?

No.

Does production need to continuously watch your source files?

Usually, no.

So we're running functionality that exists primarily to make developers productive, not to serve production users efficiently.


So What Does npm run build Do?

Now let's run:

npm run build

With Vite, this usually produces a directory called:

dist/

For example:

dist/
├── index.html
└── assets/
    ├── index-Bx73a91.js
    ├── index-D2x91s.css
    └── logo-C91ab3.svg

Something very important has happened here.

Our development source code has been converted into production-ready assets.

Conceptually:

React source code



┌──────────────────────┐
│ Production Build     │
│                      │
│ JSX transformation   │
│ Bundling             │
│ Tree shaking         │
│ Minification         │
│ Code splitting       │
│ Asset processing     │
│ Content hashing      │
└──────────┬───────────┘


       dist/

           ├── index.html
           ├── *.js
           ├── *.css
           └── images/assets

The work that the development tooling was doing repeatedly during development has now largely been done once, ahead of time.


Your React Application Becomes Static Files

This is probably the most important concept in this entire article.

A typical client-side React application does not need Node.js to execute React in production.

After building the application, you essentially have files such as:

index.html
app.js
styles.css
logo.svg

These are just static files.

A browser can request them over HTTP:

GET /index.html
GET /assets/index-Bx73a91.js
GET /assets/index-D2x91s.css

The server doesn't need to understand React.

It doesn't need to understand JSX.

It doesn't even need to know that the application was originally built using React.

Its job is simply:

"Someone requested this file. Send them this file."

The browser downloads the JavaScript and executes your React application.

That distinction is extremely important.

Server

   │ sends HTML/JS/CSS

Browser

   │ executes JavaScript

React application runs

React is primarily running in the user's browser, not on your web server.


This Is Why React Can Be Hosted on S3

Once you understand that a production React build is mostly static files, something else starts making sense.

Why can we host React applications using object storage such as Amazon S3?

Because S3 doesn't need to execute React.

Imagine our CI/CD pipeline runs:

npm ci
npm run build

We now have:

dist/
├── index.html
└── assets/
    ├── index-Bx73a91.js
    └── index-D2x91s.css

We upload those files to S3.

Our architecture can then look like:

Developer

    │ git push

CI/CD Pipeline

    │ npm ci
    │ npm run build

dist/

    │ upload

┌───────────────┐
│ Amazon S3     │
│               │
│ index.html    │
│ *.js          │
│ *.css         │
│ images        │
└───────┬───────┘


   CloudFront


      Users

Notice what is missing.

There is no:

npm run dev

There may not even be a Node.js server.

There is no continuously running frontend application process that we need to manage.

Node.js was useful during the build process.

Once the build is finished, our hosting layer primarily needs to serve the generated files.


But Couldn't We Just Run npm run dev on an EC2 Server?

Yes.

Technically, you could do something like:

git clone <repository>
cd frontend

npm install

npm run dev -- --host 0.0.0.0

You could then place that server behind a load balancer:

Internet


Load Balancer


EC2


Node.js


Vite Dev Server


React source code

And yes, your application may work.

But now ask yourself:

Why are we running all of this infrastructure?

Our production environment now needs to care about:

Node.js runtime
      +
npm dependencies
      +
Vite
      +
development server
      +
filesystem watchers
      +
HMR infrastructure
      +
source transformation

All we ultimately wanted to deliver was:

HTML
CSS
JavaScript
Images

That's unnecessary complexity for a frontend that can be served statically.


Think About 10,000 Users

The difference becomes easier to understand when we think about scale.

Suppose 10,000 users visit our application.

If we're relying on a running application server, the architecture might look something like:

10,000 users


Load Balancer


Application Servers


Node.js Processes


Frontend Assets

Now we potentially need to think about:

  • CPU
  • memory
  • server availability
  • process crashes
  • health checks
  • scaling
  • Node.js versions
  • dependency vulnerabilities
  • container security
  • operating system patching

Compare that with static hosting:

10,000 users


    CDN
  ┌───┼───┐
  ▼   ▼   ▼
Edge Edge Edge
Cache Cache Cache

  └───────┐

     Object Storage

A CDN such as CloudFront can cache your JavaScript, CSS, images, and other assets at edge locations.

Many user requests can therefore be served directly from the CDN cache.

Your origin may not even receive most of those requests.


Production Builds Also Help With Browser Caching

You might have noticed filenames like this after running a build:

index-Bx73a91.js

Why doesn't the build simply generate:

index.js

One reason is cache busting.

Imagine version 1 of your application produces:

index-Bx73a91.js

A browser or CDN can cache this file for a long time.

Now you deploy a new version.

The build might produce:

index-Xa82b14.js

Because the contents changed, the generated hash changed.

The browser sees a completely different URL:

/assets/index-Bx73a91.js

versus:

/assets/index-Xa82b14.js

Therefore, we can aggressively cache static assets without worrying that users will keep receiving an old version forever.

A simplified deployment might look like:

Version 1

index.html

    └── index-Bx73a91.js


Version 2

index.html

    └── index-Xa82b14.js

This is one of the reasons production builds and CDNs work so well together.


What About Nginx?

You may have seen production Dockerfiles that do something like:

FROM node:22 AS build

WORKDIR /app

COPY package*.json ./
RUN npm ci

COPY . .
RUN npm run build


FROM nginx:alpine

COPY --from=build /app/dist /usr/share/nginx/html

At first glance, this might be confusing.

If React doesn't need a server, why are we running Nginx?

Because something still needs to speak HTTP and deliver the files.

Nginx isn't executing React.

It's simply doing something like:

Browser

   │ GET /

Nginx


index.html


Browser

   │ GET /assets/app.js

Nginx


app.js

So these are two very different architectures:

Development

Browser


Vite Development Server


Source Code

and:

Production

Browser


Nginx


Pre-built Static Files

Nginx is acting as a highly optimized static web server.

The same role could instead be handled by something such as S3 + CloudFront.


Does npm run build Always Mean Static Files?

No, and this is an important distinction.

Everything we've discussed so far applies primarily to a client-side Single Page Application, such as a typical React application built using Vite.

Other frameworks may work differently.

For example, a framework that performs server-side rendering may require:

Source Code


npm run build


Production Application


Node.js Server


Users

In that situation, the server is doing actual work at runtime.

It may be:

  • rendering HTML
  • executing server-side code
  • handling API routes
  • performing authentication
  • running middleware

So we should distinguish between three different concepts.

Development server

npm run dev


Development tooling


Fast development experience

Static production application

npm run build


HTML + CSS + JavaScript


S3 / CDN / Nginx

Server-rendered production application

npm run build


Production application


npm start


Node.js / Container / Server

The exact deployment architecture therefore depends on the framework and how the application is rendered.


Development and Production Have Different Goals

This is ultimately the reason we have separate development and production commands.

A development environment optimizes for developer experience.

We want:

  • fast startup
  • hot reload
  • useful errors
  • source maps
  • file watching
  • easy debugging

Production optimizes for users and operational efficiency.

We want:

  • smaller assets
  • fast downloads
  • caching
  • reliability
  • security
  • scalability
  • predictable deployments

Those are different requirements.

So it makes sense that we use different tooling for each environment.


A Simple Mental Model

If you remember only one thing from this article, remember this:

npm run dev prepares your application continuously while you're developing it.

While:

npm run build prepares the application once so production doesn't have to.

For a typical React/Vite SPA, the deployment process therefore looks like:

Git Repository

      │ git push

CI/CD Pipeline

      ├── npm ci

      └── npm run build


    dist/


Object Storage


     CDN


   Browser


React executes here

Once you understand this, a lot of frontend deployment concepts suddenly become easier to reason about.

Why can React be hosted on S3?

Because the production output is static files.

Why put CloudFront in front of it?

Because those static files are excellent candidates for CDN caching.

Why don't we need Node.js running permanently?

Because Node.js was primarily needed to build the client-side application, not execute it for every user.

And why shouldn't we normally run npm run dev in production?

Not because it is impossible.

But because the development server is solving a problem that production no longer has.


Where Do We Go From Here?

There is one interesting question left:

If React can simply be hosted as static files, why do we so often see React applications packaged into Docker containers and served through Nginx on ECS or Kubernetes?

That deserves its own explanation.

Because once we understand the difference between:

React
Node.js
Nginx
Docker
S3
CloudFront

and the role each one actually plays, choosing the right frontend deployment architecture becomes much easier.

Continue Reading

Read S3 + CloudFront vs Nginx + Docker: Where Should You Host a React App?
#DevOps

S3 + CloudFront vs Nginx + Docker: Where Should You Host a React App?

If a React application becomes static files after npm run build, why do teams still package it inside Docker and serve it through Nginx? Let's compare S3 + CloudFront with Nginx + Docker and understand when each architecture makes sense.

Read post ->