Webpack Fast Refresh vs Vite
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
| Term | Definition |
|---|---|
| HMR | Swaps changed code into a running app without a full reload. |
| Lazy route / code‑splitting | Loading route code only when the route is visited. |
| Vendor chunk | A bundle of shared third‑party deps cached across routes. |
| Eager pre‑bundling | Bundling 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 ESLint | ESLint 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:
- 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. - Large dependency surface –
react-scriptspulled 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.projectorprojectService) 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
splitChunksconfiguration. The rest of the webpack config (loaders, plugins, devServer, etc.) remains unchanged.
Why This Setup Felt Faster End‑to‑End for Our Team
- 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.
SplitChunksmakes this predictable. - React Fast Refresh ergonomics – Solid state preservation on edits, reliable error recovery, and overlays we like.
- 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.