Webpack Fast Refresh vs Vite

Published: (December 18, 2025 at 02:54 PM EST)
6 min read
Source: Dev.to

Source: Dev.to

Overview

This article shares what felt fastest in the day‑to‑day development of ilert‑ui, a large React + TypeScript app with many lazy routes. We first moved off Create React App (CRA) toward modern tooling, trialed Vite for local development, and ultimately landed on webpack‑dev‑server + React Fast Refresh.

This article was first published on the ilert blog, and you can find the full version here.

Scope: Local development only. Our production builds remain on Webpack. For context, the React team officially sunset CRA on February 14 2025, and recommends migrating to a framework or a modern build tool such as Vite, Parcel, or RSBuild.

Qualitative field notes from ilert‑ui: We didn’t run formal benchmarks; this is our day‑to‑day experience in a large route‑split app.

Helpful Terms

TermDefinition
HMRSwaps changed code into a running app without a full reload.
Lazy route / code‑splittingLoading route code only when the route is visited.
Vendor chunkA bundle of shared third‑party deps cached across routes.
Eager pre‑bundlingBundling common deps up front to avoid many small requests later.
Dependency optimizer (Vite)Pre‑bundles bare imports; may re‑run if new deps are discovered at runtime.
Type‑aware ESLintESLint that uses TypeScript type info – more accurate, heavier.

Why CRA No Longer Fit ilert‑ui

ilert‑ui outgrew CRA’s convenience defaults as the app matured. The main reasons we moved away from CRA were:

  1. Customization friction – Advanced webpack tweaks (custom loaders, tighter split‑chunks strategy, Babel settings for react-refresh) required ejecting or patching, which slowed iteration on a production‑scale app.
  2. Large dependency surfacereact-scripts pulled in many transitive packages. Install times grew, and security noise increased without clear benefits for us.

Goals for the Next Steps

  • Keep React + TS.
  • Improve time‑to‑interactive after server start.
  • Preserve state on edits (Fast Refresh behavior) and keep HMR snappy.
  • Maintain predictable first‑visit latency when navigating across many lazy routes.

Vite: First Impressions

During development, Vite serves your source as native ESM and pre‑bundles bare imports from node_modules using esbuild. This usually yields very fast cold starts and responsive HMR.

What We Loved Immediately

  • Cold starts – Noticeably faster than our CRA baseline.
  • Minimal config, clean DX – Sensible defaults and readable errors.
  • Great HMR in touched areas – Editing within routes already visited felt excellent.

Where the Model Rubbed Against Our Size

  • Methodology – Qualitative observations from daily development in ilert‑ui.
  • Repo shape – Dozens of lazy routes, several heavy sections pulling in many modules; hundreds of shared files and deep store imports across features.

What We Noticed

  • First‑time heavy routes – Opening a dependency‑rich route often triggered many ESM requests and sometimes a dep‑optimizer re‑run. Cross‑route exploration across untouched routes felt slower than our webpack setup that eagerly pre‑bundles shared vendors.
  • Typed ESLint overhead – Running type‑aware ESLint (with parserOptions.project or projectService) in‑process with the dev server added latency during typing. Moving linting out‑of‑process helped, but didn’t fully offset the cost at our scale – an expected trade‑off with typed linting.

TL;DR for our codebase: Vite was fantastic once a route had been touched in the session, but the first visits across many lazy routes were less predictable.

What We Run in Production

// webpack.config.js
module.exports = {
  optimization: {
    minimize: false,
    runtimeChunk: "single",
    splitChunks: {
      chunks: "all",
      cacheGroups: {
        "react-vendor": {
          test: /[\\/]node_modules[\\/](react|react-dom|react-router-dom)[\\/]/,
          name: "react-vendor",
          chunks: "all",
          priority: 30,
        },
        "mui-vendor": {
          test: /[\\/]node_modules[\\/](@mui\\/material|@mui\\/icons-material|@mui\\/lab|@mui\\/x-date-pickers)[\\/]/,
          name: "mui-vendor",
          chunks: "all",
          priority: 25,
        },
        "mobx-vendor": {
          test: /[\\/]node_modules[\\/](mobx|mobx-react|mobx-utils)[\\/]/,
          name: "mobx-vendor",
          chunks: "all",
          priority: 24,
        },
        "utils-vendor": {
          test: /[\\/]node_modules[\\/](axios|moment|lodash\\.debounce|lodash\\.isequal)[\\/]/,
          name: "utils-vendor",
          chunks: "all",
          priority: 23,
        },
        "ui-vendor": {
          test: /[\\/]node_modules[\\/](@loadable\\/component|react-transition-group|react-window)[\\/]/,
          name: "ui-vendor",
          chunks: "all",
          priority: 22,
        },
        "charts-vendor": {
          test: /[\\/]node_modules[\\/](recharts|reactflow)[\\/]/,
          name: "charts-vendor",
          chunks: "all",
          priority: 21,
        },
        "editor-vendor": {
          test: /[\\/]node_modules[\\/](@monaco-editor\\/react|monaco-editor)[\\/]/,
          name: "editor-vendor",
          chunks: "all",
          priority: 20,
        },
        "calendar-vendor": {
          test: /[\\/]node_modules[\\/](some-calendar-lib)[\\/]/,
          name: "calendar-vendor",
          chunks: "all",
          priority: 19,
        },
        // …additional vendor groups as needed
      },
    },
  },
  // other webpack config (loaders, plugins, devServer, etc.)
};

Note: The snippet above shows the relevant splitChunks configuration. The rest of the webpack config (loaders, plugins, devServer, etc.) remains unchanged.

Why This Setup Felt Faster End‑to‑End for Our Team

  1. Eager vendor pre‑bundling – We explicitly pre‑bundle vendor chunks (React, MUI, MobX, charts, editor, calendar, etc.). The very first load is a bit heavier, but first‑time visits to other routes are faster because shared deps are already cached. SplitChunks makes this predictable.
  2. React Fast Refresh ergonomics – Solid state preservation on edits, reliable error recovery, and overlays we like.
  3. Non‑blocking linting – Typed ESLint runs outside the dev server process, so HMR stays responsive even during large type checks.

Takeaways

  • Vite shines for rapid iteration on already‑touched routes, thanks to its native ESM serving and lightning‑fast HMR.
  • Webpack with eager vendor splitting provides more predictable latency for first‑time navigation across many lazy routes in a large codebase.
  • Separating heavy tooling (type‑aware ESLint, type checking) from the dev server preserves a snappy developer experience regardless of the bundler you choose.

If you’re evaluating a migration path for a similarly sized React + TS application, consider the trade‑offs highlighted above and choose the toolchain that aligns best with your team’s workflow and performance priorities.

// webpack.config.js (excerpt)
{
  // …
  splitChunks: {
    cacheGroups: {
      "calendar-vendor": {
        test: /[\/\\]node_modules[\/\\](@fullcalendar\/core|@fullcalendar\/react|@fullcalendar\/daygrid)[\/\\]/,
        name: "calendar-vendor",
        chunks: "all",
        priority: 19,
      },
      vendor: {
        test: /[\/\\]node_modules[\/\\]/,
        name: "vendor",
        chunks: "all",
        priority: 10,
      },
    },
  },
  // …
}
// vite.config.ts – Vite `optimizeDeps` includes we tried
export default defineConfig({
  optimizeDeps: {
    include: [
      "react",
      "react-dom",
      "react-router-dom",
      "@mui/material",
      "@mui/icons-material",
      "@mui/lab",
      "@mui/x-date-pickers",
      "mobx",
      "mobx-react",
      "mobx-utils",
      "axios",
      "moment",
      "lodash.debounce",
      "lodash.isequal",
      "@loadable/component",
      "react-transition-group",
      "react-window",
      "recharts",
      "reactflow",
      "@monaco-editor/react",
      "monaco-editor",
      "@fullcalendar/core",
      "@fullcalendar/react",
      "@fullcalendar/daygrid",
    ],
    // Force pre‑bundling of these dependencies
    force: true,
  },
});

Vite – Pros

  • Blazing cold starts and lightweight config.
  • Excellent HMR within already‑touched routes.
  • Strong plugin ecosystem and modern ESM defaults.

Vite – Cons

  • Dependency optimizer re‑runs can interrupt flow during first‑time navigation across many lazy routes.
  • Requires careful setup in large monorepos and with linked packages.
  • Typed ESLint running in‑process can hurt responsiveness on large projects; better out‑of‑process.

Webpack + Fast Refresh – Pros

  • Predictable first‑visit latency across many routes via eager vendor chunks.
  • Fine‑grained control over loaders, plugins, and output.
  • Fast Refresh preserves state and has mature error overlays.

Webpack + Fast Refresh – Cons

  • Heavier initial load than Vite’s cold start.
  • More configuration surface to maintain.
  • Historical complexity (mitigated by modern config patterns and caching).

Choose Vite if:

  • Cold starts dominate your workflow.
  • Your module graph isn’t huge or fragmented into many lazy routes.
  • Plugins – especially typed ESLint – are light or run out‑of‑process.

Choose Webpack + Fast Refresh if:

  • Your app benefits from eager vendor pre‑bundling and predictable first‑visit latency across many routes.
  • You want precise control over loaders/plugins and build output.
  • You like Fast Refresh’s state preservation and overlays.

Learn more in the iLert blog.

Back to Blog

Related posts

Read more »