4 Things Making Your Vite Dev Server Slow (And How to Fix Them)

Vite promised lightning-fast development. So why is your dev server taking 30 seconds to start? Why are production bundles hitting 2MB?

The culprits are almost always the same four patterns—and they're all fixable in under an hour. This guide shows you exactly what's slowing down your Vite React app and how to fix it.

The four things killing your Vite performance

Every slow Vite project I've profiled had at least one of these issues:

  1. Barrel file imports - Re-exporting from index files forces Vite to process thousands of modules
  2. Babel transforms - The default React plugin uses Babel, which is slow
  3. SVG-as-components - Converting SVGs to React components adds build overhead
  4. Large production chunks - Without manual chunking, everything lands in one file

Let's fix each one.

Fix dev server speed

Switch to SWC (5-minute fix, 10x faster HMR)

The single biggest dev server speedup is replacing Babel with SWC—a Rust-based compiler that's dramatically faster.

npm uninstall @vitejs/plugin-react
npm install @vitejs/plugin-react-swc
// vite.config.js
import react from '@vitejs/plugin-react-swc';

export default {
  plugins: [react()],
};

That's it. HMR that took 3-4 seconds can drop to under 500ms.

Important: If you're using the standard @vitejs/plugin-react, don't configure Babel options unless you absolutely need them. Custom Babel config prevents esbuild from being used during builds.

Stop using barrel files

Barrel files are the index.js files that re-export everything:

// components/index.js (barrel file)
export { Button } from './Button';
export { Card } from './Card';
export { Modal } from './Modal';
// ... 50 more exports

When you import from a barrel:

// Slow - processes ALL exports even though you only want Button
import { Button } from '@/components';

Vite has to fetch and transform every file the barrel exports. For large codebases, this means thousands of modules on startup.

Fix: Import directly from source files:

// Fast - only processes Button
import { Button } from '@/components/Button';

Yes, it's more verbose. Your dev server will thank you.

Don't transform SVGs into components

Popular plugins like vite-plugin-svgr convert SVGs to React components:

// Slow - SVG becomes a full React component
import Logo from './logo.svg?react';

Each SVG gets transformed, adding build overhead. For a few icons it's fine. For 50+ icons, it adds up.

Better approach: Import as URLs and use <img> tags:

// Fast - no transform needed
import logoUrl from './logo.svg';

function Header() {
  return <img src={logoUrl} alt="Logo" width={120} height={40} />;
}

For icons that need styling, consider:

  • CSS mask-image for color changes
  • Icon fonts
  • A small set of inline SVG components for critical icons only

Use server warmup

Vite can pre-transform frequently used files on startup:

// vite.config.js
export default {
  server: {
    warmup: {
      clientFiles: [
        './src/main.tsx',
        './src/App.tsx',
        './src/components/Layout.tsx',
      ],
    },
  },
};

Using --open or server.open: true also helps - Vite automatically warms up your entry point.

Check your browser (often the real culprit)

What feels like a Vite problem is often a browser problem. I've seen teams spend hours debugging Vite configs when a single browser checkbox was the issue.

1. The "Disable cache" trap:

When dev tools are open, Chrome has a "Disable cache" checkbox in the Network tab. If checked, the browser re-fetches every module on every page load instead of using Vite's optimized caching.

Dev tools → Network tab → Uncheck "Disable cache"

This single setting can make HMR feel 10x slower. Only enable it when actively debugging caching issues.

2. Browser extensions:

Extensions like React DevTools, Redux DevTools, or ad blockers intercept network requests. Each interception adds latency. In a Vite app with 500+ modules, this compounds quickly.

Test in incognito mode or a clean browser profile:

# Chrome with clean profile
google-chrome --user-data-dir=/tmp/clean-chrome

# Or just use incognito
google-chrome --incognito

If Vite is dramatically faster in incognito, start disabling extensions one by one to find the culprit.

3. Firefox has known issues:

Firefox's ES module handling has documented bugs that make Vite's unbundled development approach slower. The Vite team recommends using Chrome or Safari for development.

If you must use Firefox, enable the network.http.max-persistent-connections-per-server setting in about:config and increase it from 6 to 10+.

4. Hardware acceleration:

Disabled GPU acceleration forces the browser to use software rendering, which impacts both the dev server and your app's performance. Check:

Chrome → Settings → System → Use hardware acceleration

Fix production bundle size

Enable manual chunking (most impactful change)

By default, Vite puts all vendor code in one massive chunk. For large apps, this single file blocks page load and kills caching—when any dependency updates, users re-download everything.

Split it up with manualChunks:

// vite.config.js
export default {
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          // React core in its own chunk
          'vendor-react': ['react', 'react-dom'],

          // Router separate
          'vendor-router': ['react-router-dom'],

          // Heavy libraries isolated
          'vendor-charts': ['recharts', 'd3'],

          // UI library
          'vendor-ui': ['@radix-ui/react-dialog', '@radix-ui/react-dropdown-menu'],
        },
      },
    },
  },
};

This lets the browser cache stable vendor chunks separately from your frequently-changing app code.

Dynamic chunking for large apps

For more control, use a function:

// vite.config.js
export default {
  build: {
    rollupOptions: {
      output: {
        manualChunks(id) {
          if (id.includes('node_modules')) {
            // Group all @radix-ui packages
            if (id.includes('@radix-ui')) {
              return 'vendor-radix';
            }
            // Group charting libraries
            if (id.includes('recharts') || id.includes('d3')) {
              return 'vendor-charts';
            }
            // React ecosystem together
            if (id.includes('react')) {
              return 'vendor-react';
            }
            // Everything else
            return 'vendor';
          }
        },
      },
    },
  },
};

Lazy load routes

Don't load every route upfront. Use React.lazy for route-based code splitting:

import { lazy, Suspense } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';

const Home = lazy(() => import('./pages/Home'));
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));

function App() {
  return (
    <BrowserRouter>
      <Suspense fallback={<PageSkeleton />}>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/dashboard" element={<Dashboard />} />
          <Route path="/settings" element={<Settings />} />
        </Routes>
      </Suspense>
    </BrowserRouter>
  );
}

Each route becomes its own chunk, loaded only when visited. This directly improves LCP.

Enable compression

Compress your output for smaller transfer sizes:

npm install vite-plugin-compression
// vite.config.js
import compression from 'vite-plugin-compression';

export default {
  plugins: [
    compression({
      algorithm: 'gzip',
    }),
    compression({
      algorithm: 'brotliCompress',
    }),
  ],
};

This generates .gz and .br files. Configure your server to serve them.

Analyze your bundle

Before optimizing, see what's actually in your bundle:

npm install rollup-plugin-visualizer
// vite.config.js
import { visualizer } from 'rollup-plugin-visualizer';

export default {
  plugins: [
    visualizer({
      open: true,
      gzipSize: true,
      filename: 'bundle-analysis.html',
    }),
  ],
};

Run npm run build and a treemap opens showing exactly where your bytes go.

Common findings:

  • Moment.js or date-fns with all locales (switch to day.js or tree-shake)
  • Full lodash instead of individual imports
  • Multiple versions of the same package
  • Unused exports from large libraries

Profile dev server bottlenecks

If your dev server is still slow after the basics, profile it:

vite --profile

Interact with your app, then press p to save a CPU profile. Upload it to speedscope.app to see exactly what's taking time.

Look for:

  • Long-running plugin hooks (buildStart, config, configResolved)
  • Slow transforms on specific file types
  • Dependencies that should be pre-bundled

Force pre-bundling

Vite pre-bundles dependencies to convert CommonJS to ESM. Sometimes it misses things:

// vite.config.js
export default {
  optimizeDeps: {
    include: [
      'some-cjs-package',
      'deeply-nested-dependency',
    ],
  },
};

Vite and Core Web Vitals

Vite's build output directly affects your production Core Web Vitals. Here's how each metric is impacted and specific Vite configurations to improve them.

LCP impact

LCP measures when the largest content element becomes visible. Large JavaScript bundles delay LCP because the browser must download, parse, and execute JS before React can render anything.

How Vite affects LCP:

  • Single large chunk blocks initial render
  • Unoptimized vendor bundles delay hydration
  • Missing preload hints slow critical resource loading

Fix it with Vite config:

  1. Split vendor chunks strategically:
// vite.config.js
export default {
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          // React loads first - it's critical
          'vendor-react': ['react', 'react-dom'],
          // Everything else loads after
          'vendor-other': ['axios', 'date-fns', 'lodash-es'],
        },
      },
    },
  },
};
  1. Lazy load below-fold components:
// Components below the fold shouldn't block LCP
const Comments = lazy(() => import('./Comments'));
const RelatedPosts = lazy(() => import('./RelatedPosts'));
const Footer = lazy(() => import('./Footer'));
  1. Use modulepreload for critical chunks:

Vite automatically adds <link rel="modulepreload"> for entry points. For other critical chunks, add them manually:

<!-- In your index.html -->
<link rel="modulepreload" href="/assets/vendor-react.js" />
  1. Optimize the critical rendering path:
// vite.config.js
export default {
  build: {
    // Inline small chunks to reduce round trips
    assetsInlineLimit: 4096, // 4kb
    // Generate smaller chunks for better caching
    chunkSizeWarningLimit: 500,
  },
};

CLS impact

CLS measures visual stability. Vite itself doesn't cause CLS, but how you handle lazy-loaded components does.

Common CLS causes in Vite apps:

  1. Lazy components without reserved space:
// Bad - layout shifts when component loads
const Chart = lazy(() => import('./Chart'));

function Dashboard() {
  return (
    <Suspense fallback={<p>Loading...</p>}>
      <Chart /> {/* Height unknown until JS loads */}
    </Suspense>
  );
}

// Good - skeleton matches final dimensions
function Dashboard() {
  return (
    <Suspense fallback={<div style={{ height: 400, background: '#f0f0f0' }} />}>
      <Chart />
    </Suspense>
  );
}
  1. Fonts loaded via CSS-in-JS: If your styling solution loads fonts dynamically, text can reflow when fonts load. Use Vite's CSS handling instead:
// vite.config.js
export default {
  css: {
    preprocessorOptions: {
      scss: {
        additionalData: `@import "@/styles/fonts.scss";`,
      },
    },
  },
};
  1. Images without dimensions:
// Bad - image size unknown
<img src={imageUrl} alt="Product" />

// Good - dimensions prevent CLS
<img src={imageUrl} alt="Product" width={400} height={300} />

INP impact

INP measures interaction responsiveness. Chunk size directly affects INP because parsing large JavaScript files blocks the main thread.

How Vite chunk size affects INP:

When a user clicks a button that triggers a lazy import, the browser must:

  1. Fetch the chunk
  2. Parse the JavaScript
  3. Execute the code
  4. Run the click handler

Large chunks make step 2 and 3 slow, causing perceptible lag.

Fix it:

  1. Keep interaction-critical chunks small:
// vite.config.js
export default {
  build: {
    rollupOptions: {
      output: {
        manualChunks(id) {
          // Keep interactive UI components in small chunks
          if (id.includes('/components/interactive/')) {
            return 'ui-interactive';
          }
        },
      },
    },
  },
};
  1. Prefetch likely interactions:
// Prefetch the settings page chunk when user hovers the menu
function NavMenu() {
  const prefetchSettings = () => {
    import('./pages/Settings'); // Starts loading immediately
  };

  return (
    <nav>
      <Link to="/settings" onMouseEnter={prefetchSettings}>
        Settings
      </Link>
    </nav>
  );
}
  1. Use web workers for heavy computation:
// vite.config.js - enable worker bundling
export default {
  worker: {
    format: 'es',
  },
};

// Then in your code
const worker = new Worker(new URL('./heavy-calc.worker.js', import.meta.url));
worker.postMessage(data);
worker.onmessage = (e) => setResult(e.data);
  1. Monitor chunk sizes in CI:
// vite.config.js
export default {
  build: {
    // Fail build if any chunk exceeds 500kb
    chunkSizeWarningLimit: 500,
  },
};

Quick wins checklist

Dev server speed

  • Switch to @vitejs/plugin-react-swc
  • Replace barrel imports with direct file imports
  • Import SVGs as URLs, not components
  • Add critical files to server.warmup
  • Test in Chrome/Safari instead of Firefox
  • Ensure "Disable cache" is unchecked in dev tools

Production bundle size

  • Configure manualChunks for vendor splitting
  • Lazy load routes with React.lazy
  • Add vite-plugin-compression for gzip/brotli
  • Run rollup-plugin-visualizer to find bloat
  • Remove unused dependencies

Build speed

  • Don't configure Babel options in the React plugin
  • Use optimizeDeps.include for problematic dependencies
  • Profile with vite --profile if still slow

Frequently Asked Questions

Is Vite faster than Webpack?

For dev server, significantly yes. Vite serves native ES modules without bundling during development. For production builds, they're comparable since both use bundlers (Rollup for Vite, Webpack for Webpack).

Should I migrate from Create React App to Vite?

Yes. CRA is deprecated and Vite offers faster development and modern tooling. The migration is usually straightforward - update config files and import paths.

Why is my Vite build slower than expected?

Check for: custom Babel config (prevents esbuild), slow plugins, large source maps, or TypeScript type checking in the build. Run vite build --profile to identify bottlenecks.

Does Vite work with older browsers?

Yes. Use @vitejs/plugin-legacy to generate fallback bundles for browsers that don't support native ES modules. This adds build time but ensures compatibility.

How do I debug Vite performance issues?

Start with vite --profile to generate a CPU profile. Upload to speedscope.app to visualize. Also check Vite's --debug flag for detailed logging about transforms and dependency resolution.

What's next

Your 30-minute action plan:

  1. Swap to SWC (5 min) — One npm install, one config change
  2. Find barrel imports (10 min) — Search for from '@/components' or similar index imports
  3. Add manual chunks (10 min) — Split React, router, and heavy libraries
  4. Run bundle analyzer (5 min) — See exactly what's bloating your build

Most Vite performance problems disappear after these four changes.

Related guides:

Still losing points after optimizing? Run your site through PageSpeedFix to identify exactly what's hurting your Core Web Vitals—sometimes it's not Vite at all.