Skip to content

Top mistakes to avoid when deploying react apps to production

By Adam Hultman · 10 min read

ReactDeployment
Top mistakes to avoid when deploying react apps to production cover image

Deploying a React app can feel like the finish line of a project, but it’s really just the start of a new phase. Once real users are in the app, things get a lot less predictable. People are on slower networks, older devices, different browsers, and sometimes they click things in ways you absolutely did not expect.

A good production deployment is not just about making sure the app works. It’s about making sure it works well, stays secure, loads quickly, and gives you enough visibility to fix issues when something goes wrong.

Here are some of the most common mistakes developers make when deploying React apps to production, plus a few practical ways to avoid them.


1. Not Handling Environment Variables Securely

Environment variables are one of those things that seem simple until they bite you.

They’re useful for storing configuration values like API URLs, feature flags, and environment-specific settings. But in frontend apps, you have to be careful. Some environment variables are bundled into the JavaScript that gets sent to the browser. That means they are not private.

In Create React App, variables starting with this are exposed to the client:

1 REACT_APP_

In Vite, variables starting with this are exposed:

1 VITE_

In Next.js, variables starting with this are exposed:

1 NEXT_PUBLIC_

That is fine for values like:

1 2 3 NEXT_PUBLIC_API_URL VITE_APP_ENV REACT_APP_SITE_NAME

It is not fine for secrets like:

1 2 3 4 DATABASE_URL STRIPE_SECRET_KEY JWT_SECRET AWS_SECRET_ACCESS_KEY

If a secret ends up in your client bundle, it is no longer a secret. Anyone can inspect the JavaScript and find it.

A good pattern is to keep sensitive values on the server and only expose values that are safe for the browser. You can also keep an example environment file in your repo so other developers know what values are needed:

1 2 3 .env.example .env.local .env.production.local

Then make sure your real local environment files are ignored:

1 2 3 # .gitignore .env.local .env.*.local

The main rule is simple: if the browser needs it, assume users can see it. If it is sensitive, keep it server-side.


2. Misconfiguring the Production Build

A common deployment mistake is assuming that if the app works locally, it is ready for production.

Development mode and production mode are not the same thing. Development builds are usually larger, slower, and include extra warnings or debugging behavior. Production builds are optimized for real users.

For most React apps, you want to run a proper production build before deploying:

1 npm run build

For Vite, that usually means:

1 vite build

For Next.js:

1 next build

These commands create optimized production output. They minify code, optimize assets, and remove development-only behavior.

It’s also worth testing the production build locally before shipping it. A surprising number of issues only show up after the app is built.

For Vite, you can do:

1 2 vite build vite preview

When testing the production build, check the boring but important stuff:

  1. Does the app load from a fresh browser session?
  2. Do routes work after refreshing the page?
  3. Are API calls going to the right environment?
  4. Are images, fonts, and static assets loading?
  5. Do protected pages still work?
  6. Are error tracking and analytics running correctly?

Production should not depend on someone remembering the right command from their laptop. Ideally, your CI/CD pipeline should run the build the same way every time.


3. Forgetting to Optimize Bundle Size

One of the fastest ways to make a React app feel slow is to ship too much JavaScript.

A big bundle means the browser has more code to download, parse, and execute before the user can really interact with the app. On a fast laptop, that might not seem like a big deal. On a mid-range phone with a weak connection, it can feel painfully slow. Minimize the bytes that are sent over the wire.

A few common causes of bundle bloat are:

  1. Importing entire libraries when you only need one function
  2. Loading heavy components on the initial page
  3. Including admin-only code for regular users
  4. Shipping large charting, map, editor, or analytics libraries everywhere
  5. Never checking what is actually inside the bundle

For example, avoid importing an entire library if you only need one part of it:

1 2 3 4 5 // Avoid this if you only need one function import * as _ from 'lodash'; // Better import debounce from 'lodash/debounce';

You can also split heavier parts of your app so they only load when needed:

1 2 3 4 5 6 7 8 9 10 11 import React, { Suspense, lazy } from 'react'; const HeavyComponent = lazy(() => import('./HeavyComponent')); export default function App() { return ( <Suspense fallback={<div>Loading...</div>}> <HeavyComponent /> </Suspense> ); }

In Next.js, you can use dynamic imports:

1 2 3 4 5 import dynamic from 'next/dynamic'; const HeavyComponent = dynamic(() => import('./HeavyComponent'), { loading: () => <p>Loading...</p>, });

Be a little careful with ssr: false in Next.js. It can be useful for browser-only components, but it should not be the default solution for every heavy component. Sometimes it can hurt performance or SEO.

The best approach is to measure. Use a bundle analyzer, look at what is actually being shipped, and clean up the biggest offenders first.


4. Skipping Error Monitoring and Logging

Once your app is live, things will go wrong. That is not pessimism. That is just production.

Users will run into edge cases you did not test. APIs will fail. Browsers will behave differently. A deployment will look fine at first, then one route will quietly break for 15 percent of users.

If you do not have error monitoring, you may not know until someone complains.

Sentry is a popular option for React apps:

1 2 3 4 5 6 import * as Sentry from '@sentry/react'; Sentry.init({ dsn: process.env.SENTRY_DSN, environment: process.env.NODE_ENV, });

Good error monitoring should tell you things like:

  1. What error happened?
  2. Which release introduced it?
  3. How many users are affected?
  4. What browser or device was involved?
  5. What route or action triggered it?

It is also helpful to track failed API requests, slow network calls, and important user actions. Tools like Datadog, LogRocket, CloudWatch, and Sentry can all help depending on your stack.

One small but important note: do not log secrets, tokens, passwords, or sensitive user data. Logs are useful, but they should not become a second accidental database of things you really did not want exposed.


5. Ignoring Security Best Practices

Security can feel like something you deal with later, but production is exactly where small mistakes start to matter.

A few basics go a long way.

First, make sure your app is served over HTTPS. Most platforms like Vercel, Netlify, Cloudflare, and Render handle this for you, but it is still worth confirming. Also make sure HTTP redirects to HTTPS.

Second, be careful with user-generated content. React escapes values by default, which helps protect against cross-site scripting. This is generally safe:

1 <p>{userInput}</p>

But rendering raw HTML is a different story:

1 <div dangerouslySetInnerHTML={{ __html: userInputHtml }} />

If you need to render user-provided HTML, sanitize it first:

1 2 3 import DOMPurify from 'dompurify'; const sanitizedHtml = DOMPurify.sanitize(userInputHtml);

Third, consider adding basic security headers. In a Next.js app, you can configure headers like this:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 module.exports = { async headers() { return [ { source: '/(.*)', headers: [ { key: 'Content-Security-Policy', value: "default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self';", }, { key: 'X-Frame-Options', value: 'DENY', }, { key: 'X-Content-Type-Options', value: 'nosniff', }, ], }, ]; }, };

A Content Security Policy can help reduce the damage from XSS by limiting which scripts and resources your app is allowed to load. Just make sure to test it carefully before rolling it out. A strict CSP is great until it accidentally blocks something your app actually needs.


6. Forgetting About Caching

Caching can make your app much faster, but bad caching can create confusing production bugs.

Static assets like JavaScript and CSS files are usually safe to cache for a long time when their filenames include a content hash. If the file changes, the filename changes too.

HTML and content pages are different. If those are cached too aggressively, users might load an old version of a page even after the content has been updated. This gets especially painful on publishing sites, where authors expect “publish” to mean “the page is live now.”

In reality, there might be multiple caches involved:

  1. Browser cache
  2. CDN cache
  3. Framework cache
  4. API cache
  5. CMS cache
  6. Server-side rendered page cache
  7. Service worker cache, if you use one

When something looks stale, it is not always obvious which layer is holding onto the old version. That can lead to the classic production debugging loop: the editor says the article is wrong, you refresh and see the new version, someone else still sees the old version, and now everyone is haunted.

This is where cache invalidation matters. For content-heavy apps, you usually want a reliable way to purge or revalidate specific pages when content changes. That might mean clearing CDN paths, using cache tags, calling a revalidation endpoint, or wiring your CMS publish event into your frontend cache strategy.

A healthy setup usually looks like this:

  1. Cache hashed JS and CSS assets aggressively
  2. Revalidate HTML and content pages more carefully
  3. Invalidate specific pages when content changes
  4. Avoid full-site cache purges unless you really need them
  5. Make cache status visible in headers or tooling
  6. Test service workers carefully if you use them
  7. Make sure rollbacks still work with cached clients

Caching is one of those things that feels simple until it crosses three systems and a confused author is asking why their typo fix only exists on half the internet.


7. Not Having a Rollback Plan

Every deployment should have an escape hatch.

Even with good testing, things can break. A bad build can go out. A feature flag can behave unexpectedly. An API change can break a page you forgot about.

Before deploying, ask:

  1. Can we roll this back quickly?
  2. Do we know which version is currently live?
  3. Are database migrations safe to reverse or tolerate?
  4. Are risky changes behind feature flags?
  5. Will someone notice if the deploy causes errors?

Platforms like Vercel, Netlify, and Cloudflare Pages make rollbacks pretty easy, but you still need to know how your team handles them.

The best rollback plan is the one you understand before everything is on fire.


8. Not Measuring Real User Performance

Lighthouse scores are useful, but they do not tell the whole story.

Your app might be fast on your machine and slow for actual users. Real users have different devices, networks, browsers, extensions, and cached states. Production performance needs to be measured in production.

Useful metrics to watch include:

  1. Largest Contentful Paint
  2. Interaction to Next Paint
  3. Cumulative Layout Shift
  4. API latency
  5. JavaScript errors
  6. Slow routes
  7. Performance by device type

Tools like Datadog RUM, Sentry Performance, Vercel Analytics, Google Analytics, and SpeedCurve can help you see what users are actually experiencing.

Performance is not something you optimize once and forget. It is more like keeping your apartment clean. Ignore it long enough and suddenly there is a chair covered in laundry.


Final Thoughts

Deploying a React app is more than running npm run build and calling it a day.

A good production deployment means thinking about configuration, bundle size, security, monitoring, caching, rollback plans, and real user performance. None of these things need to be overly complicated, but they do need to be handled intentionally.

The little things matter. Keeping secrets out of the client bundle, testing the production build, watching errors, and measuring performance can save you a lot of painful debugging later.

Ship the app, but ship it like real people are going to use it.

Because they are.

Keep reading


© 2026 Adam Hultman