Actually, I should clarify – I tried migrating our main dashboard to Vite last Tuesday. But then I hit the plugin ecosystem wall. Again.
The reality is, if you’re building a standard React app in 2026, you probably don’t need Webpack. But if you’re dealing with a sprawling enterprise monolith that relies on custom module federation, obscure asset handling, or legacy browser support (yes, some banks still use ancient Chromium forks), Webpack isn’t just an option. It’s the only thing that doesn’t break.
I’ve spent the last three days re-configuring a TypeScript Webpack setup from scratch. It was painful. It was verbose. But it works exactly how I told it to, which is more than I can say for the “zero-config” magic that failed me earlier this week.
The “Modern” Webpack Stack (Yes, It Exists)

Forget ts-loader. Seriously. If you are still using ts-loader in 2026, you are voluntarily choosing to waste your life waiting for builds. The biggest shift in the last two years hasn’t been abandoning Webpack, but gutting its internals to use faster transpilers. The SWC (Speedy Web Compiler) is a popular choice that can significantly speed up your Webpack builds.
I’m running this setup on Node 24.1.0, and the difference is night and day. By swapping out the default TypeScript compiler for SWC within the Webpack pipeline, I cut our cold start time from 42 seconds down to about 8 seconds. It’s not instant like Vite, but it’s fast enough that I don’t lose my train of thought.
The Config That Actually Works
Pay attention to the module rules. That’s where the magic happens.
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
// 'production' for your build pipeline, 'development' for local
mode: 'development',
// The entry point. I stick to src/index.ts usually.
entry: './src/index.ts',
// Where the bundled mess goes
output: {
filename: 'bundle.[contenthash].js',
path: path.resolve(__dirname, 'dist'),
clean: true, // Cleans the dist folder before build. Lifesaver.
},
resolve: {
extensions: ['.ts', '.js'], // Crucial. Don't forget .js or imports break.
},
module: {
rules: [
{
test: /\.ts$/,
exclude: /node_modules/,
use: {
// using swc-loader instead of ts-loader
// results: 4x faster builds on my M3 Macbook
loader: 'swc-loader',
options: {
jsc: {
parser: {
syntax: 'typescript',
},
},
},
},
},
],
},
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html',
}),
],
devServer: {
static: './dist',
port: 3000,
hot: true, // HMR is non-negotiable in 2026
},
// Source maps are heavy, but necessary for debugging TS
devtool: 'inline-source-map',
};
Writing TypeScript That Doesn’t Fight the Bundler

// src/api/user.ts
interface UserData {
id: number;
name: string;
email: string;
}
export class UserAPI {
private baseUrl: string;
constructor(url: string) {
this.baseUrl = url;
}
// Async/Await works natively now, no polyfill hell needed
async fetchUser(id: number): Promise<UserData | null> {
try {
const response = await fetch(${this.baseUrl}/users/${id});
if (!response.ok) {
throw new Error(HTTP Error: ${response.status});
}
const data: UserData = await response.json();
return data;
} catch (error) {
console.error("Failed to fetch user:", error);
return null;
}
}
}
// src/index.ts
import { UserAPI } from './api/user';
const initApp = async () => {
const appDiv = document.getElementById('app');
if (!appDiv) {
// Webpack won't catch this at build time, but TS will warn if strictNullChecks is on
throw new Error("DOM element #app not found");
}
const api = new UserAPI('https://jsonplaceholder.typicode.com');
appDiv.innerHTML = '<p>Loading...</p>';
const user = await api.fetchUser(1);
if (user) {
// specific DOM manipulation
const userCard = document.createElement('div');
userCard.className = 'user-card';
userCard.innerHTML =
<h2>${user.name}</h2>
<p>${user.email}</p>
;
appDiv.innerHTML = ''; // Clear loading state
appDiv.appendChild(userCard);
} else {
appDiv.innerHTML = '<p class="error">Failed to load user.</p>';
}
};
// Execute
initApp();
The “Strict” Trap
If you see an error like ModuleResolutionKind mismatch, check your tsconfig.json. You want "moduleResolution": "bundler". Do not use "node" or "node16" if you are using Webpack 5+. It causes weird pathing issues with imports that don’t have extensions. The TypeScript Module Resolution documentation provides more details on this configuration.
Common questions
Why use Webpack instead of Vite for enterprise React apps in 2026?
For sprawling enterprise monoliths relying on custom module federation, obscure asset handling, or legacy browser support (like ancient Chromium forks still used by some banks), Webpack remains the only tool that doesn’t break. Standard React apps in 2026 probably don’t need Webpack, but Vite’s plugin ecosystem hits walls on complex setups. Webpack is verbose and painful to configure, but it works exactly as instructed rather than relying on zero-config magic.
How much faster is swc-loader compared to ts-loader in Webpack?
Swapping the default TypeScript compiler for SWC (Speedy Web Compiler) within the Webpack pipeline cut cold start time from 42 seconds down to about 8 seconds on an M3 MacBook running Node 24.1.0 — roughly a 4x speedup. It’s not instant like Vite, but fast enough to avoid losing your train of thought. Using ts-loader in 2026 is described as voluntarily wasting your life waiting for builds.
What moduleResolution setting should I use in tsconfig.json with Webpack 5?
Set “moduleResolution”: “bundler” in tsconfig.json when using Webpack 5+. Do not use “node” or “node16”, as those cause weird pathing issues with imports that don’t have extensions and can trigger a ModuleResolutionKind mismatch error. The TypeScript Module Resolution documentation provides more details. This is part of avoiding what the article calls the “strict trap” when pairing TypeScript with a modern Webpack setup.
Why do I need to include .js in Webpack resolve extensions for a TypeScript project?
In the Webpack config’s resolve.extensions array, you must include both ‘.ts’ and ‘.js’ — the article flags this as crucial because imports will break otherwise. The working config also defines entry as ‘./src/index.ts’, outputs bundle.[contenthash].js to dist with clean:true, uses HtmlWebpackPlugin, and enables hot module replacement on port 3000 with inline-source-map devtool for debugging TypeScript.
