DEV Community

Cover image for How I Made TypeScript Object Mapping 32x Faster (And Why Your NestJS APIs Are Slow)
Mohit Sarsahay
Mohit Sarsahay

Posted on

How I Made TypeScript Object Mapping 32x Faster (And Why Your NestJS APIs Are Slow)

If you are running high-throughput TypeScript backend services, you have likely run into a silent CPU killer: object serialization and validation.

Whether you are building with NestJS, Express, Fastify, or even mapping API payloads on a frontend React/Angular application, transforming raw JSON data into typed class instances is a massive event-loop blocker.

In the NestJS ecosystem, this bottleneck is magnified because the framework's standard validation pipeline forces your payload through two separate reflection passes: first class-transformer to instantiate the DTO, then class-validator to inspect it.

To solve this bottleneck universally, I built fast-class-transformer — a zero-dependency, JIT-compiled alternative that delivers up to 32x+ faster execution speeds by compiling mapping metadata into static, monomorphic JavaScript functions optimized for the V8 engine.

Here is a deep-dive autopsy into why standard reflection-based mapping is slow, and how runtime compilation bypasses it completely.

The Root Problem: How V8 Layouts Get Deoptimized

To understand why traditional mapping libraries (like the original class-transformer) are slow, we have to look at how the V8 Engine (the JavaScript engine running Node.js and Bun) compiles code at runtime.

1. Hidden Classes (Shapes) and Dictionary Mode

JavaScript is dynamically typed, but V8 needs static-like structures to run property lookups at machine-code speeds. It does this by creating internal schemas called Hidden Classes (or Shapes).

When you instantiate a class:

class User {
  id: number;
  name: string;
}
Enter fullscreen mode Exit fullscreen mode

V8 expects properties to be assigned in a strict, predictable order (id first, then name).

However, reflection mappers assign fields dynamically inside generic loops:

// Inside standard class-transformer loop:
for (const key in plainObject) {
  instance[key] = plainObject[key]; // Dynamic key assignment
}
Enter fullscreen mode Exit fullscreen mode

Because V8 cannot predict the keys or assignment order inside a dynamic loop, it deoptimizes the object. V8 strips its Hidden Class, dropping the object layout into Dictionary Mode (slow hash table lookup). Every property access on that object from that point on is significantly slower.

2. Megamorphic Inline Caches (ICs)

V8 uses Inline Caches to store the memory offsets of object properties. If a function always receives objects of the exact same shape (monomorphic), V8 caches the property offsets.
Because traditional mappers use a single generic function to map every single DTO in your application, V8's Inline Caches become megamorphic. V8 gives up on caching, forcing Node/Bun to execute slow dynamic lookups on every single request.

The JIT Solution: Compiling Monomorphic Code at Runtime

Instead of resolving metadata and traversing loops on every request, fast-class-transformer uses a Runtime Just-in-Time (JIT) Compiler.

The first time you map a class:

  1. Our compiler parses the decorators (@Expose, @Exclude, @Type, @Transform) once.
  2. It generates a custom JavaScript code string containing static property assignments specifically for that DTO.
  3. It compiles the string into an executable function using new Function().
  4. It caches the compiled mapper.

Here is what the JIT compiler generates under the hood for your DTO shape:

// Compiled JIT Mapper (V8 Optimized)
function mapUser(plain) {
  const inst = new User();

  // Static property writes preserve V8 Hidden Classes!
  inst.id = plain.id;
  inst.firstName = plain.first_name; // Rename mappings resolved at compile-time
  inst.createdAt = new Date(plain.createdAt); 

  return inst;
}
Enter fullscreen mode Exit fullscreen mode

Because this compiled function is dedicated to one shape, V8 treats it as monomorphic. The engine optimizes it instantly, running property writes at near-native C++ speeds.

Single-Pass Validation (Optional Integration)

For backend applications (especially NestJS), we took JIT compilation a step further by integrating with class-validator.

Instead of running mapping and validation in two separate passes, the JIT compiler parses your validation decorators and inlines the checks directly into the compiled mapping function:

// Compiled JIT Mapper with Inlined Validation checks
function mapAndValidateUser(plain) {
  const inst = new User();

  // Single-pass validation checks (Zero runtime reflection)
  if (typeof plain.first_name !== 'string') {
    throw new ValidationError('firstName must be a string');
  }

  inst.id = plain.id;
  inst.firstName = plain.first_name;
  return inst;
}
Enter fullscreen mode Exit fullscreen mode

Validation and instantiation are combined into a single pass, completely bypassing class-validator's reflection loop during execution.

The Benchmarks: 4-Dimensional Metrics

Here are the benchmark results run over 100,000 iterations using mitata on an Intel i5-12500H (Bun 1.3.0 runtime):

Mitata benchmark terminal output showing fast-class-transformer speedups

Benchmark comparison table showing 134x flat, 60x nested, 186x array, and 64x single-pass validation speedups

Benchmark Methodology: Benchmarks were run using mitata on Bun 1.3.0 after JIT warmup. Mapping functions were pre-compiled to measure hot-path execution throughput rather than startup compilation latency. Outputs were passed to do_not_optimize() to mitigate V8 dead-code elimination, and inputs were rotated across 1,024 payload instances to prevent constant propagation.

For simple flat mappings, V8 compiles property writes into static monomorphic assignments, executing in 17 nanoseconds (a 134x speedup). Deep arrays are mapped 186x faster, and inline validations run 64x faster than the traditional validation pipeline.

Universal Integration (How to Use)

fast-class-transformer is a drop-in replacement. You can install it on any Node/Bun backend or frontend project:

npm install fast-class-transformer
Enter fullscreen mode Exit fullscreen mode

1. General Node.js / Express / Fastify / Frontend Use

Replace your imports and map payloads instantly. The API is fully compatible with standard decorator schemas:

import { Expose, Type, plainToInstance } from 'fast-class-transformer';

class Profile {
  @Expose() bio!: string;
}

class User {
  @Expose() id!: number;
  @Expose() @Type(() => Profile) profile!: Profile;
}

const user = plainToInstance(User, rawPayload);
Enter fullscreen mode Exit fullscreen mode

2. NestJS Specific Optimization

To optimize your NestJS API controller endpoints, use our @FastMap() decorator to run JIT-compiled single-pass mapping and validation:

import { Controller, Post } from '@nestjs/common';
import { FastMap } from 'fast-class-transformer';
import { CreateUserDto } from './create-user.dto';

@Controller('users')
export class UsersController {
  @Post()
  async create(@FastMap() createUserDto: CreateUserDto) {
    // Fully instantiated, validated, and optimized
    return this.usersService.create(createUserDto);
  }
}
Enter fullscreen mode Exit fullscreen mode

Cooperating with the Runtime

Serialization is too often treated as a trivial backend overhead, but at scale, it acts as a primary driver of event loop lag. By shifting from runtime reflection to JIT-compiled monomorphic pathways, we can design services that actively cooperate with the V8 compiler rather than fighting it.

fast-class-transformer is fully open-source. If you are running high-volume services under Node.js or Bun, I invite you to drop it into a staging environment, profile your p99 latencies, and share your metrics.

Contributions, production profiling logs, and pull requests are welcome.

Top comments (0)