Steve Kinney

TypeScript Performance for Large Codebases

When your React codebase grows from thousands to millions of lines, TypeScript can go from your helpful companion to a resource-hungry monster that makes your IDE crawl and your builds take forever. But here’s the thing: TypeScript is designed to scale. You just need to know the right levers to pull. Let’s explore battle-tested strategies for keeping TypeScript fast, even when your codebase is massive.

Think of optimizing TypeScript like tuning a race car. Small adjustments compound into massive performance gains. The difference between a 30-second and 3-minute build might just be a few configuration changes away.

Understanding TypeScript’s Performance Characteristics

Before optimizing, let’s understand what makes TypeScript slow in large codebases.

The Performance Bottlenecks

// What makes TypeScript slow?

// 1. Deep type inference chains
type DeepInference<T> = T extends Array<infer U>
  ? U extends object
    ? DeepInference<U>
    : U
  : T extends object
  ? { [K in keyof T]: DeepInference<T[K]> }
  : T;

// 2. Large union types
type MassiveUnion = 'option1' | 'option2' | /* ... 1000 more options ... */;

// 3. Complex conditional types
type ComplexConditional<T> = T extends string
  ? T extends `${infer Start}${infer Rest}`
    ? ComplexConditional<Rest>
    : never
  : T;

// 4. Circular dependencies
// File A imports from File B
// File B imports from File C
// File C imports from File A

// 5. No type boundaries
// Everything imports everything else
// TypeScript has to check the entire codebase for every change

Project References: The Foundation of Scale

Project references are TypeScript’s secret weapon for large codebases. They create boundaries that allow incremental compilation.

Setting Up Project References

// tsconfig.json (root)
{
  "files": [],
  "references": [
    { "path": "./packages/core" },
    { "path": "./packages/ui" },
    { "path": "./packages/app" }
  ],
  "compilerOptions": {
    "composite": true,
    "declaration": true,
    "declarationMap": true,
    "incremental": true
  }
}
// packages/core/tsconfig.json
{
  "compilerOptions": {
    "composite": true,
    "declaration": true,
    "declarationMap": true,
    "outDir": "./dist",
    "rootDir": "./src",
    "incremental": true,
    "tsBuildInfoFile": "./dist/.tsbuildinfo"
  },
  "include": ["src/**/*"],
  "exclude": ["**/*.test.ts", "**/*.spec.ts"]
}
// packages/ui/tsconfig.json
{
  "compilerOptions": {
    "composite": true,
    "declaration": true,
    "outDir": "./dist",
    "rootDir": "./src"
  },
  "references": [{ "path": "../core" }],
  "include": ["src/**/*"]
}
// packages/app/tsconfig.json
{
  "compilerOptions": {
    "composite": true,
    "declaration": true,
    "outDir": "./dist",
    "rootDir": "./src"
  },
  "references": [{ "path": "../core" }, { "path": "../ui" }],
  "include": ["src/**/*"]
}

Building with Project References

# Build all projects in dependency order
tsc --build

# Build only changed projects
tsc --build --incremental

# Clean build artifacts
tsc --build --clean

# Watch mode with project references
tsc --build --watch

Creating Build Scripts

// package.json
{
  "scripts": {
    "build": "tsc --build",
    "build:clean": "tsc --build --clean",
    "build:watch": "tsc --build --watch",
    "build:force": "tsc --build --force",
    "typecheck": "tsc --build --noEmit"
  }
}

Incremental Compilation Strategies

Incremental compilation can reduce build times by 50-80%.

Configuring Incremental Builds

// tsconfig.json
{
  "compilerOptions": {
    "incremental": true,
    "tsBuildInfoFile": ".tsbuildinfo",

    // Emit settings for incremental builds
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,

    // Output caching
    "outDir": "./dist",
    "rootDir": "./src",

    // Performance options
    "skipLibCheck": true,
    "skipDefaultLibCheck": true
  }
}

Smart File Organization

// ❌ Bad: Everything in one file
// components/index.ts
export * from './Button';
export * from './Input';
export * from './Modal';
// ... 100 more exports

// ✅ Good: Separate entry points
// components/Button/index.ts
export { Button } from './Button';
export type { ButtonProps } from './types';

// components/Input/index.ts
export { Input } from './Input';
export type { InputProps } from './types';

// components/index.ts (only if needed)
// Use specific exports instead of barrel files
export { Button } from './Button';
export { Input } from './Input';

Managing .tsbuildinfo Files

// build-utils/clean-cache.ts
import { rm } from 'fs/promises';
import { glob } from 'glob';

async function cleanBuildCache() {
  const buildInfoFiles = await glob('**/.tsbuildinfo', {
    ignore: ['node_modules/**'],
  });

  await Promise.all(buildInfoFiles.map((file) => rm(file)));

  console.log(`Cleaned ${buildInfoFiles.length} build cache files`);
}

cleanBuildCache();

Type Performance Optimization

Optimize your types for faster compilation.

Avoiding Complex Type Computations

// ❌ Slow: Deep recursive types
type DeepReadonly<T> = {
  readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P];
};

// ✅ Fast: Use built-in utility types
type FastReadonly<T> = Readonly<T>;

// ❌ Slow: Complex conditional types in hot paths
type SlowComponent<T> =
  T extends Array<infer U>
    ? U extends object
      ? React.FC<{ items: Array<DeepPartial<U>> }>
      : React.FC<{ items: U[] }>
    : React.FC<{ item: T }>;

// ✅ Fast: Simple, explicit types
interface ListProps<T> {
  items: T[];
}

interface ItemProps<T> {
  item: T;
}

type FastComponent<T> = React.FC<ListProps<T> | ItemProps<T>>;

Optimizing Union Types

// ❌ Slow: Large union types
type EventName = 'click' | 'focus' | 'blur' | /* ... 100 more */;

// ✅ Fast: Use enums or const objects
const EventNames = {
  Click: 'click',
  Focus: 'focus',
  Blur: 'blur',
  // ... more events
} as const;

type EventName = typeof EventNames[keyof typeof EventNames];

// ❌ Slow: Distributed conditionals over large unions
type Handler<T> = T extends EventName ? () => void : never;

// ✅ Fast: Use mapped types
type HandlerMap = {
  [K in EventName]: () => void;
};

Type Import Optimization

// ❌ Slow: Regular imports force type checking
import { Component } from './Component';

// ✅ Fast: Type-only imports skip emission
import type { ComponentProps } from './Component';

// ✅ Even better: Automatic type-only imports
// tsconfig.json
{
  "compilerOptions": {
    "importsNotUsedAsValues": "remove",
    "preserveValueImports": false,
    "isolatedModules": true,
    "verbatimModuleSyntax": true
  }
}

// Then TypeScript automatically optimizes:
import { Component, type ComponentProps } from './Component';

Module Resolution Optimization

Speed up module resolution in large codebases.

Path Mapping Strategy

// tsconfig.json
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@core/*": ["packages/core/src/*"],
      "@ui/*": ["packages/ui/src/*"],
      "@utils/*": ["packages/shared/utils/*"],
      "@types/*": ["packages/shared/types/*"],
      "@hooks/*": ["packages/shared/hooks/*"]
    },
    // Speed up resolution
    "moduleResolution": "bundler",
    "resolveJsonModule": true,
    "allowJs": false // Don't check JS files
  }
}

Optimizing node_modules

// tsconfig.json
{
  "compilerOptions": {
    // Skip type checking of declaration files
    "skipLibCheck": true,

    // Assume node_modules won't change
    "assumeChangesOnlyAffectDirectDependencies": true
  },
  "exclude": [
    "node_modules",
    "**/node_modules",
    "dist",
    "build",
    "coverage",
    "**/*.test.ts",
    "**/*.spec.ts",
    "**/*.stories.tsx"
  ]
}

Monorepo Performance Patterns

Optimize TypeScript for monorepo architectures.

Workspace Configuration

// pnpm-workspace.yaml
packages:
  - 'packages/*'
  - 'apps/*'
  - 'tools/*'
// tools/typescript-workspace-builder.ts
import { execSync } from 'child_process';
import { readFileSync, writeFileSync } from 'fs';
import { glob } from 'glob';

interface WorkspaceConfig {
  packages: string[];
  tsConfigPaths: string[];
}

class WorkspaceBuilder {
  private config: WorkspaceConfig;

  constructor() {
    this.config = this.loadWorkspaceConfig();
  }

  async buildAll(options: { parallel?: boolean; watch?: boolean } = {}) {
    const { parallel = true, watch = false } = options;

    if (parallel) {
      await this.buildParallel();
    } else {
      await this.buildSequential();
    }

    if (watch) {
      this.watch();
    }
  }

  private async buildParallel() {
    const projects = this.config.tsConfigPaths;
    const buildCommands = projects.map((project) => `tsc --build ${project}`);

    await Promise.all(buildCommands.map((cmd) => this.execAsync(cmd)));
  }

  private async buildSequential() {
    // Build in dependency order
    const buildOrder = this.calculateBuildOrder();

    for (const project of buildOrder) {
      await this.execAsync(`tsc --build ${project}`);
    }
  }

  private calculateBuildOrder(): string[] {
    // Analyze project references to determine build order
    const graph = this.buildDependencyGraph();
    return this.topologicalSort(graph);
  }

  private execAsync(command: string): Promise<void> {
    return new Promise((resolve, reject) => {
      exec(command, (error, stdout, stderr) => {
        if (error) reject(error);
        else resolve();
      });
    });
  }
}

Shared Type Packages

// packages/shared-types/src/index.ts
// Centralize shared types to avoid duplication

export * from './api-types';
export * from './domain-types';
export * from './ui-types';

// packages/shared-types/src/api-types.ts
export interface ApiResponse<T> {
  data: T;
  error?: ApiError;
  metadata?: ResponseMetadata;
}

export interface ApiError {
  code: string;
  message: string;
  details?: Record<string, unknown>;
}

// packages/shared-types/src/domain-types.ts
export interface User {
  id: string;
  email: string;
  profile: UserProfile;
}

export interface UserProfile {
  name: string;
  avatar?: string;
  preferences: UserPreferences;
}

Parallel Type Checking

// tools/parallel-typecheck.ts
import { Worker } from 'worker_threads';
import { cpus } from 'os';

interface TypeCheckTask {
  project: string;
  tsconfig: string;
}

class ParallelTypeChecker {
  private workers: Worker[] = [];
  private taskQueue: TypeCheckTask[] = [];
  private maxWorkers = cpus().length;

  async checkAll(projects: string[]): Promise<void> {
    this.taskQueue = projects.map((project) => ({
      project,
      tsconfig: `${project}/tsconfig.json`,
    }));

    await this.runWorkers();
  }

  private async runWorkers(): Promise<void> {
    const workerPromises: Promise<void>[] = [];

    for (let i = 0; i < Math.min(this.maxWorkers, this.taskQueue.length); i++) {
      workerPromises.push(this.runWorker());
    }

    await Promise.all(workerPromises);
  }

  private async runWorker(): Promise<void> {
    const worker = new Worker('./typecheck-worker.js');
    this.workers.push(worker);

    return new Promise((resolve, reject) => {
      worker.on('message', (msg) => {
        if (msg.type === 'ready') {
          this.assignTask(worker);
        } else if (msg.type === 'complete') {
          console.log(`${msg.project}`);
          this.assignTask(worker);
        } else if (msg.type === 'error') {
          console.error(`${msg.project}: ${msg.error}`);
          this.assignTask(worker);
        } else if (msg.type === 'done') {
          worker.terminate();
          resolve();
        }
      });

      worker.on('error', reject);
    });
  }

  private assignTask(worker: Worker): void {
    const task = this.taskQueue.shift();

    if (task) {
      worker.postMessage({ type: 'check', ...task });
    } else {
      worker.postMessage({ type: 'shutdown' });
    }
  }
}

IDE Performance Optimization

Keep your IDE responsive even with large codebases.

VS Code Settings

// .vscode/settings.json
{
  // Limit TypeScript's scope
  "typescript.tsserver.maxTsServerMemory": 4096,
  "typescript.tsserver.experimental.enableProjectDiagnostics": false,

  // Exclude files from watching
  "files.watcherExclude": {
    "**/node_modules/**": true,
    "**/dist/**": true,
    "**/build/**": true,
    "**/.tsbuildinfo": true
  },

  // Search exclusions
  "search.exclude": {
    "**/node_modules": true,
    "**/dist": true,
    "**/build": true,
    "**/*.tsbuildinfo": true
  },

  // Disable expensive features
  "typescript.suggest.completeJSDocs": false,
  "typescript.surveys.enabled": false,

  // Use workspace TypeScript version
  "typescript.tsdk": "./node_modules/typescript/lib",
  "typescript.enablePromptUseWorkspaceTsdk": true
}

TypeScript Server Plugins

// tsconfig.json
{
  "compilerOptions": {
    "plugins": [
      {
        "name": "typescript-plugin-css-modules",
        "options": {
          "customMatcher": "\\.module\\.(css|scss|sass)$"
        }
      },
      {
        "name": "typescript-strict-plugin",
        "options": {
          "alwaysStrict": true
        }
      }
    ]
  }
}

Language Service Optimization

// tools/optimize-tsserver.ts
import { writeFileSync } from 'fs';

interface TSServerConfig {
  maxFileSize: number;
  maxNodeModuleJsDepth: number;
  includeCompletionsWithSnippetText: boolean;
  includeAutomaticOptionalChainCompletions: boolean;
}

const config: TSServerConfig = {
  maxFileSize: 2000000, // 2MB
  maxNodeModuleJsDepth: 2,
  includeCompletionsWithSnippetText: false,
  includeAutomaticOptionalChainCompletions: false,
};

// Write to workspace settings
writeFileSync(
  '.vscode/settings.json',
  JSON.stringify(
    {
      'typescript.tsserver.maxTsServerMemory': 4096,
      'typescript.tsserver.nodePath': './node_modules/typescript/lib',
      'typescript.preferences.includePackageJsonAutoImports': 'off',
      'typescript.preferences.includeCompletionsWithSnippetText': false,
    },
    null,
    2,
  ),
);

Build Tool Integration

Optimize TypeScript with various build tools.

Webpack Configuration

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

module.exports = {
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        loader: 'ts-loader',
        options: {
          // Disable type checking in ts-loader
          transpileOnly: true,
          experimentalWatchApi: true,
          happyPackMode: true,
        },
      },
    ],
  },
  plugins: [
    // Type check in separate process
    new ForkTsCheckerWebpackPlugin({
      typescript: {
        mode: 'write-references',
        build: true,
        configOverwrite: {
          compilerOptions: {
            skipLibCheck: true,
            sourceMap: false,
            inlineSourceMap: false,
            declarationMap: false,
          },
        },
      },
      // Async type checking
      async: true,
      // ESLint in same process
      eslint: {
        files: './src/**/*.{ts,tsx}',
      },
    }),
  ],
};

ESBuild Integration

// esbuild.config.ts
import { build } from 'esbuild';
import { nodeExternalsPlugin } from 'esbuild-node-externals';

async function buildWithTypeCheck() {
  // Fast transpilation with esbuild
  await build({
    entryPoints: ['./src/index.tsx'],
    bundle: true,
    minify: true,
    sourcemap: true,
    target: ['es2020'],
    outfile: 'dist/bundle.js',
    plugins: [nodeExternalsPlugin()],
    // Don't type check - handle separately
    loader: {
      '.ts': 'ts',
      '.tsx': 'tsx',
    },
  });

  // Type check in parallel
  const { exec } = require('child_process');
  exec('tsc --noEmit', (error, stdout, stderr) => {
    if (error) {
      console.error('Type check failed:', stderr);
      process.exit(1);
    }
    console.log('Type check passed');
  });
}

SWC Configuration

// .swcrc
{
  "jsc": {
    "parser": {
      "syntax": "typescript",
      "tsx": true,
      "decorators": true
    },
    "transform": {
      "react": {
        "runtime": "automatic"
      }
    },
    "target": "es2020",
    "loose": false,
    "minify": {
      "compress": true,
      "mangle": true
    }
  },
  "module": {
    "type": "es6"
  },
  "minify": true,
  "sourceMaps": true
}

Performance Monitoring

Track TypeScript performance metrics.

Build Time Analysis

// tools/build-performance.ts
import { performance } from 'perf_hooks';
import { execSync } from 'child_process';

interface BuildMetrics {
  totalTime: number;
  typeCheckTime: number;
  emitTime: number;
  moduleResolutionTime: number;
  memoryUsage: number;
}

class BuildPerformanceMonitor {
  private metrics: BuildMetrics = {
    totalTime: 0,
    typeCheckTime: 0,
    emitTime: 0,
    moduleResolutionTime: 0,
    memoryUsage: 0,
  };

  async measureBuild(): Promise<BuildMetrics> {
    const startTime = performance.now();
    const startMemory = process.memoryUsage().heapUsed;

    // Run TypeScript compiler with diagnostics
    const output = execSync('tsc --diagnostics', {
      encoding: 'utf-8',
    });

    const endTime = performance.now();
    const endMemory = process.memoryUsage().heapUsed;

    this.metrics.totalTime = endTime - startTime;
    this.metrics.memoryUsage = (endMemory - startMemory) / 1024 / 1024; // MB

    // Parse diagnostics output
    this.parseDignostics(output);

    return this.metrics;
  }

  private parseDignostics(output: string): void {
    const lines = output.split('\n');

    for (const line of lines) {
      if (line.includes('Check time')) {
        this.metrics.typeCheckTime = this.parseTime(line);
      } else if (line.includes('Emit time')) {
        this.metrics.emitTime = this.parseTime(line);
      } else if (line.includes('Module resolution time')) {
        this.metrics.moduleResolutionTime = this.parseTime(line);
      }
    }
  }

  private parseTime(line: string): number {
    const match = line.match(/(\d+\.?\d*)s/);
    return match ? parseFloat(match[1]) * 1000 : 0;
  }

  generateReport(): string {
    return `
Build Performance Report
========================
Total Time: ${this.metrics.totalTime.toFixed(2)}ms
Type Check: ${this.metrics.typeCheckTime.toFixed(2)}ms
Emit: ${this.metrics.emitTime.toFixed(2)}ms
Module Resolution: ${this.metrics.moduleResolutionTime.toFixed(2)}ms
Memory Usage: ${this.metrics.memoryUsage.toFixed(2)}MB
    `;
  }
}

Continuous Performance Tracking

// tools/track-performance.ts
interface PerformanceHistory {
  timestamp: number;
  metrics: BuildMetrics;
  commit: string;
}

class PerformanceTracker {
  private history: PerformanceHistory[] = [];

  async trackBuild(): Promise<void> {
    const monitor = new BuildPerformanceMonitor();
    const metrics = await monitor.measureBuild();

    const commit = execSync('git rev-parse HEAD', {
      encoding: 'utf-8'
    }).trim();

    this.history.push({
      timestamp: Date.now(),
      metrics,
      commit
    });

    this.saveHistory();
    this.checkThresholds(metrics);
  }

  private checkThresholds(metrics: BuildMetrics): void {
    const thresholds = {
      totalTime: 60000, // 1 minute
      memoryUsage: 1024 // 1GB
    };

    if (metrics.totalTime > thresholds.totalTime) {
      console.warn(`⚠️ Build time exceeded threshold: ${metrics.totalTime}ms > ${thresholds.totalTime}ms`);
    }

    if (metrics.memoryUsage > thresholds.memoryUsage) {
      console.warn(`⚠️ Memory usage exceeded threshold: ${metrics.memoryUsage}MB > ${thresholds.memoryUsage}MB`);
    }
  }

  private saveHistory(): void {
    writeFileSync(
      'build-performance.json',
      JSON.stringify(this.history, null, 2)
    );
  }

  analyzeT trends(): void {
    const recentBuilds = this.history.slice(-10);
    const avgTime = recentBuilds.reduce((sum, h) =>
      sum + h.metrics.totalTime, 0
    ) / recentBuilds.length;

    console.log(`Average build time (last 10): ${avgTime.toFixed(2)}ms`);

    // Check for performance regression
    const lastBuild = this.history[this.history.length - 1];
    const previousBuild = this.history[this.history.length - 2];

    if (lastBuild && previousBuild) {
      const timeDiff = lastBuild.metrics.totalTime - previousBuild.metrics.totalTime;
      if (timeDiff > 5000) { // 5 second regression
        console.warn(`⚠️ Performance regression detected: +${timeDiff.toFixed(2)}ms`);
      }
    }
  }
}

Best Practices Checklist

Configuration

  • Enable incremental compilation
  • Use project references for large codebases
  • Configure skipLibCheck
  • Set up proper exclude patterns
  • Use composite projects

Code Organization

  • Avoid barrel exports
  • Use type-only imports
  • Split large files
  • Create module boundaries
  • Minimize circular dependencies

Type Design

  • Avoid deep recursive types
  • Limit union type size
  • Use interfaces over type aliases where possible
  • Prefer explicit over inferred complex types
  • Cache expensive type computations

Build Process

  • Use parallel type checking
  • Implement build caching
  • Separate type checking from transpilation
  • Use faster transpilers (esbuild/swc)
  • Monitor build performance

Wrapping Up

Scaling TypeScript in large React codebases isn’t about accepting slow builds as inevitable—it’s about understanding the tools and patterns that keep TypeScript fast at any scale. From project references to parallel builds, every optimization technique we’ve covered can dramatically improve your development experience.

Remember: Performance optimization is an ongoing process. Start with the biggest wins (project references, incremental builds), measure the impact, and iterate. Your team will thank you when builds are fast, IDEs are responsive, and TypeScript continues to catch bugs without slowing anyone down.

Last modified on .