Skip to content

TypeScript Performance Optimization — Code Speed and Bundle Size

DodaTech Updated 2026-06-28 8 min read

In this tutorial, you will learn about TypeScript Performance Optimization. We cover key concepts, practical examples, and best practices to help you master this topic.

TypeScript performance optimization spans compile-time (build speed, type-checking cost) and runtime (bundle size, code efficiency) — understanding both dimensions ensures your TypeScript applications are fast to build and fast to execute.

What You'll Learn

  • Compiler performance flags
  • Project references for incremental builds
  • Type inference cost management
  • Tree shaking and dead code elimination
  • Lazy loading patterns
  • Bundle size optimization

Why It Matters

TypeScript's compiler can become a bottleneck in large projects — 100,000+ file codebases can take minutes to type-check. At runtime, TypeScript's type erasure means you only pay for the JavaScript you emit, but certain patterns create unnecessary overhead.

Real-World Use

The Doda Browser codebase had a 12-minute TypeScript build. By applying project references, incremental builds, and optimizing type-heavy modules, they reduced check times to under 2 minutes while maintaining full type safety.

Learning Path

flowchart LR
  A[Pattern Matching] --> B[Performance]
  B --> C[Project: REST API]
  B --> D[You Are Here]
  C --> E[Project: Dashboard]
  D --> F[Migration from JS]

Compiler Performance Flags

Several tsconfig.json options directly impact build speed:

{
  "compilerOptions": {
    "incremental": true,
    "tsBuildInfoFile": ".tsbuildinfo",
    "skipLibCheck": true,
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "exactOptionalPropertyTypes": false,
    "isolatedModules": true,
    "verbatimModuleSyntax": true
  }
}
Flag Impact
incremental Rebuild only changed files (saves 60-80% on subsequent builds)
skipLibCheck Skip type-checking .d.ts files (huge savings with many dependencies)
isolatedModules Faster non-comprehensive check (compatible with esbuild)
verbatimModuleSyntax Avoids unnecessary module transformations

Measuring Build Time

# Time a full build
time npx tsc --noEmit

# With incremental
time npx tsc --noEmit --incremental

# Generate performance trace
npx tsc --generateTrace trace
npm install -g @typescript/analyze-trace
npx analyze-trace trace

The performance trace shows which files and types take the longest to check.

Project References for Large Codebases

Split your codebase into smaller projects that TypeScript checks independently:

// tsconfig.json (root)
{
  "references": [
    { "path": "./packages/core" },
    { "path": "./packages/api" },
    { "path": "./packages/web" }
  ],
  "files": []
}

// packages/core/tsconfig.json
{
  "compilerOptions": {
    "composite": true,
    "outDir": "./dist",
    "rootDir": "./src"
  },
  "include": ["src"]
}

// packages/api/tsconfig.json
{
  "compilerOptions": {
    "composite": true,
    "outDir": "./dist",
    "rootDir": "./src"
  },
  "references": [{ "path": "../core" }],
  "include": ["src"]
}

How it works: TypeScript builds core first, then api uses the pre-built .d.ts files from core instead of re-checking core's source. This prevents cascading re-checks.

Type Inference Cost Management

Complex type inference slows both the compiler and your editor:

// ❌ Expensive — deep nested inference
const result = await someFunction()
  .then(data => process(data))
  .then(transformed => transform(transformed))
  .then(final => finalize(final));

// ✅ Fast — explicit typing breaks the chain
interface FinalResult {
  id: string;
  status: 'done' | 'pending';
}

const result: FinalResult = await someFunction()
  .then(data => process(data))
  .then(transformed => transform(transformed))
  .then(final => finalize(final));

Expensive Type Patterns

// ❌ Recursive types that aren't memoized
type DeepReadonly<T> = {
  readonly [P in keyof T]: T[P] extends object
    ? DeepReadonly<T[P]>
    : T[P];
};

// ✅ Limit recursion depth for common cases
type ShallowReadonly<T> = {
  readonly [P in keyof T]: T[P];
};

// ❌ Large template literal types
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
type ApiRoutes<T extends string> = `/api/${T}/${HttpMethod}`;
// TypeScript expands this combinatorially

// ✅ Narrow the literal types
type ApiRoute = '/api/users/GET' | '/api/posts/GET' | '/api/posts/POST';

Tree Shaking and Dead Code

TypeScript emits all code by default. Use ES modules for tree shaking:

// utils.ts
export function usedFunction() {
  return 'This is included';
}

export function unusedFunction() {
  return 'This is removed by tree shaking';
}

// main.ts
import { usedFunction } from './utils';
console.log(usedFunction());

When bundled with esbuild or webpack, unusedFunction is eliminated from the final bundle.

Isolated Modules for Better Tree Shaking

{
  "compilerOptions": {
    "isolatedModules": true,
    "verbatimModuleSyntax": true
  }
}

verbatimModuleSyntax preserves import type statements, making it clear to bundlers which imports can be removed:

// This import is fully removed at runtime
import type { User } from './types';

// This import is kept
import { getUser } from './api';

Lazy Loading Patterns

Split large modules at natural boundaries:

// ❌ Eager — all routes load upfront
import { HeavyDashboard } from './dashboard';
import { HeavyReports } from './reports';
import { HeavySettings } from './settings';

// ✅ Lazy — load on demand
async function loadComponent(route: string) {
  switch (route) {
    case 'dashboard':
      const { HeavyDashboard } = await import('./dashboard');
      return HeavyDashboard;
    case 'reports':
      const { HeavyReports } = await import('./reports');
      return HeavyReports;
    case 'settings':
      const { HeavySettings } = await import('./settings');
      return HeavySettings;
  }
}

Route-Based Code Splitting with React

import { lazy, Suspense } from 'react';

const Dashboard = lazy(() => import('./pages/Dashboard'));
const Reports = lazy(() => import('./pages/Reports'));

function App() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <Routes>
        <Route path="/dashboard" element={<Dashboard />} />
        <Route path="/reports" element={<Reports />} />
      </Routes>
    </Suspense>
  );
}

Each lazy-loaded page is a separate chunk loaded only when the user navigates to it.

Bundle Size Optimization

Track bundle size with tsc --showConfig and analyze with tools:

# Check what TypeScript outputs
npx tsc --showConfig

# Analyze bundle with source-map-explorer
npm install --save-dev source-map-explorer
npx source-map-explorer dist/bundle.js

# Or use the built-in analyze mode in esbuild
npx esbuild src/main.ts --bundle --analyze

Dead Type Elimination

Types are erased at compile time — but type imports can inflate declaration files:

// ❌ Generates larger .d.ts
export function processItem(item: {
  id: string;
  name: string;
  metadata: Record<string, unknown>;
}): void;

// ✅ Uses an interface — same size, but reusable
export interface ProcessItemInput {
  id: string;
  name: string;
  metadata: Record<string, unknown>;
}

export function processItem(item: ProcessItemInput): void;

Runtime Performance Patterns

TypeScript's type erasure means emitted JavaScript is what runs. But certain TypeScript patterns create slow JavaScript:

// ❌ Expensive — spread creates shallow copies
function updateUser(user: User, changes: Partial<User>): User {
  return { ...user, ...changes };
}

// ✅ Fast — mutation is faster when safe
function updateUser(user: User, changes: Partial<User>): User {
  user.name = changes.name ?? user.name;
  user.email = changes.email ?? user.email;
  return user;
}

// ❌ Expensive — conditional spread in hot loops
items.forEach(item => {
  config.push({
    id: item.id,
    ...(item.active && { activeAt: Date.now() }),
  });
});

// ✅ Fast — avoid conditional spread in loops
items.forEach(item => {
  const entry: ConfigEntry = { id: item.id };
  if (item.active) {
    entry.activeAt = Date.now();
  }
  config.push(entry);
});

Common Mistakes

1. Enabling all strict checks without incremental builds

Strict mode + no incremental = slow builds. Always pair strict checks with incremental: true.

2. Deeply nested mapped types in hot modules

Complex mapped types (like DeepPartial<T>) are checked every time the module is compiled. Extract them to a shared module.

3. Not using skipLibCheck in large projects

Type checking node_modules type definitions for thousands of packages is slow. Libraries are tested by their authors — skip their checks.

4. Single tsconfig for monolithic codebase

A single tsconfig means every file is re-checked on every build. Use project references to split.

5. Excessive conditional types in public API surfaces

Conditional types like Extract<T, U> are checked at every call site. If the type can be computed once, pre-compute it.

6. Not analyzing the performance trace

TypeScript's --generateTrace reveals the specific files and types causing slow checks. Use it before optimizing.

7. Optimizing prematurely

Profile first, then optimize. Many perceived TypeScript performance issues are negligible in practice — measure before fixing.

Practice Questions

  1. What does incremental: true do? Caches previous compilation results and only re-checks changed files and their dependencies, reducing subsequent build times by 60-80%.

  2. How do project references improve build time? They split the codebase into independent projects. Changes to one project only trigger re-checks in dependent projects, not the entire codebase.

  3. What is skipLibCheck and when should you use it? It skips type-checking .d.ts declaration files. Use it in production builds — library type definitions are presumed correct.

  4. How does isolatedModules affect compilation? It enables per-file transpilation (used by esbuild, ts-jest) without cross-file type inference. Faster but less comprehensive type checking.

  5. What's the most impactful single optimization for build speed? Enabling incremental: true — it's a single flag that dramatically improves developer iteration time.

Challenge

Profile a TypeScript project with --generateTrace, analyze the trace with analyze-trace, identify the three slowest files or type operations, and optimize them with explicit typing, project references, or type extraction.

FAQ

Does TypeScript affect runtime performance?

Only through the JavaScript it emits. TypeScript types are erased at compile time — they don't exist at runtime. Poor TypeScript patterns (excessive spreads, complex conditionals) create slower JavaScript.

How much faster is esbuild than tsc for building?

esbuild is 10-100x faster for transpilation because it skips type checking entirely. Use tsc for type checking in development, esbuild for production builds.

Should I use Babel or esbuild with TypeScript?

esbuild is faster and supports most TypeScript features. Babel has better plugin ecosystem but slower compilation. For new projects, use esbuild.

Does `const` vs `let` affect compiled output?

No — both emit var or let depending on target. The TypeScript compiler ignores const for performance optimization.

How do I profile TypeScript build time?

Use tsc --generateTrace trace and analyze with @typescript/analyze-trace. This shows file-level and type-level check times.

What's the fastest tsconfig for development?

Use skipLibCheck: true, isolatedModules: true, incremental: true with strict mode disabled. Enable strict checks in CI only.

Mini Project

Optimize a TypeScript Monorepo build pipeline:

  • Project references: Split into core, api, web, and shared packages
  • Incremental builds: Enable across all sub-projects
  • Build trace: Generate and analyze a performance trace
  • CI optimization: Different tsconfig for CI (full check) vs development (fast check)
  • Bundle analysis: Use source-map-explorer to identify large modules and lazy-load them

What's Next

You've optimized TypeScript performance. Now apply everything you've learned by building a complete REST API with {{< ref "55-project-rest-api" >}}, a React dashboard with {{< ref "56-project-react-dashboard" >}}, or a CLI tool with {{< ref "57-project-cli-tool" >}}.

For Migration strategies, see {{< ref "58-migration-from-js" >}}.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro