Surviving TypeScript and Webpack in 2026 (When Everyone Else Moved On)

Look, I know exactly what you’re thinking. Why am I writing about Webpack right now? Half the industry just finished migrating to Vite, and the other half is playing with Turbopack. I get it. If I were starting a greenfield project today, I wouldn’t touch Webpack with a ten-foot pole.

But here’s the reality check. I maintain a massive enterprise dashboard. It has over 400 custom Webpack plugins written by developers who left the company three years ago. “Just rewrite it in Vite” is a great punchline on social media, but it’s not a Jira ticket my product manager is going to approve anytime soon.

So, we optimize. We make it work.

The ts-loader Bottleneck

If you’re still using ts-loader in your Webpack config, I need you to stop. Seriously. It’s doing way too much work on the main thread.

Webpack logo - Webpack JS logo
Webpack logo – Webpack JS logo” Sticker for Sale by hipstuff | Redbubble

I spent three hours last Tuesday trying to figure out why our CI pipeline was choking. Turns out, running full TypeScript type-checking during the emit phase is a terrible idea for large codebases. On our staging cluster with 3 nodes (running Node.js 22.1.0), the memory spikes were consistently hitting the 4GB V8 limit. I kept getting the dreaded Error: ENOMEM - ran out of memory right before the build finished.

The fix? Ditch it entirely for swc-loader and move the type checking to a separate background process.

// webpack.config.js
const path = require('path');
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');

module.exports = {
  entry: './src/index.ts',
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        use: {
          loader: 'swc-loader',
          options: {
            jsc: {
              parser: {
                syntax: "typescript",
                tsx: true
              },
              target: "es2022"
            }
          }
        },
        exclude: /node_modules/,
      },
    ],
  },
  resolve: {
    extensions: ['.tsx', '.ts', '.js'],
  },
  plugins: [
    // This is what saves your memory limit
    new ForkTsCheckerWebpackPlugin()
  ]
};

Making this single swap cut our production build time from 8m 23s to 1m 14s. I actually got up to get coffee and came back to a finished build. Felt weird.

Writing Actual TypeScript (Without Framework Bloat)

Now that the build doesn’t make you want to cry, let’s talk about the actual code going through it. One thing I constantly see junior devs mess up is handling asynchronous API calls and DOM updates safely.

TypeScript is notoriously strict about DOM types. This is a good thing, but it leads to a lot of ugly as HTMLElement casting if you don’t know what you’re doing. Here’s a pattern I use almost daily for fetching data and rendering it directly to the DOM. No massive frameworks, just clean, typed JS.

Webpack logo - Webpack: The Ultimate Guide. Webpack is a powerful static module ...
Webpack logo – Webpack: The Ultimate Guide. Webpack is a powerful static module …
// src/api-widget.ts
interface UserData {
  id: number;
  name: string;
  email: string;
  isActive: boolean;
}

// Handling the async API fetch safely
async function fetchActiveUsers(endpoint: string): Promise<UserData[]> {
  try {
    const response = await fetch(endpoint, {
      headers: { 'Accept': 'application/json' }
    });

    if (!response.ok) {
      throw new Error(HTTP error! status: ${response.status});
    }

    const data: UserData[] = await response.json();
    return data.filter(user => user.isActive);
    
  } catch (error) {
    console.error("Failed to grab users:", error);
    return [];
  }
}

// Safe DOM manipulation
export async function initializeUserList(containerId: string): Promise<void> {
  // The strict null check here saves you from 'Cannot read property of undefined'
  const container = document.getElementById(containerId) as HTMLDivElement | null;

  if (!container) {
    console.warn(Container #${containerId} missing from the DOM.);
    return;
  }

  container.innerHTML = '<p class="loading-state">Fetching users...</p>';

  const users = await fetchActiveUsers('https://api.example.com/v2/users');

  // Clear the loading text
  container.innerHTML = '';

  if (users.length === 0) {
    container.textContent = 'No active users found right now.';
    return;
  }

  const list = document.createElement('ul');
  list.className = 'user-directory';

  users.forEach(user => {
    const listItem = document.createElement('li');
    
    // Template literals with safe TS property access
    listItem.textContent = ${user.name} (${user.email});
    listItem.dataset.userId = user.id.toString();

    // Event listeners need explicit typing for the event target
    listItem.addEventListener('click', (e) => {
      const target = e.target as HTMLLIElement;
      console.log(Clicked user ID: ${target.dataset.userId});
      
      // Visual feedback
      target.classList.toggle('selected-user');
    });

    list.appendChild(listItem);
  });

  container.appendChild(list);
}

The Polyfill Gotcha

Here’s a fun one that bit me hard last month. If you’re using swc-loader (like I just told you to), watch your async/await transformations.

I ran into a bizarre edge case where our final bundle size ballooned by almost 400KB overnight. I dug into the output and realized SWC was aggressively polyfilling generator functions for older browser targets that we didn’t even support anymore. It was turning every clean async/await block into an unreadable mess of state machines.

The docs barely mention this, but if your browserslist file is missing or misconfigured, the bundler assumes you want to support ancient browsers. I pinned ours strictly to last 2 Chrome versions and the bundle size dropped immediately. Always check what your loader is actually outputting.

software developer coding - Commonly Confused Titles in Software Development | by Todd Cullum ...
software developer coding – Commonly Confused Titles in Software Development | by Todd Cullum …

Looking Ahead

Am I going to use Webpack forever? God, no.

I expect the remaining enterprise Webpack holdouts to migrate to Rspack by Q1 2027. It’s basically a drop-in Rust replacement for Webpack, which means you don’t have to rewrite your custom plugins from scratch. It’s the only realistic escape hatch for massive legacy apps.

But until my team gets the green light for that migration, I’m stuck with this config. If you’re in the same boat, just switch to SWC, separate your

typescriptworld_com

Learn More →

Leave a Reply

Your email address will not be published. Required fields are marked *