Skip to content

Bundle Size & Tree-shaking ​

QuickModel is built from the ground up to minimize the weight it adds to your application bundle. This page explains how the library is structured internally, what is loaded eagerly vs lazily, and how to use granular subpath imports to load only what you actually use.

How the build works ​

QuickModel ships a dual ESM + CJS bundle generated by tsup:

  • Code splitting enabled — shared internals are factored into chunk files automatically.
  • Tree-shaking (preset: 'smallest') — unused exports are removed when your bundler supports ESM.
  • sideEffects: false in package.json — signals bundlers that every file is safe to tree-shake.
  • Minified output — all production files are minified.

ESM vs CJS

ESM consumers get the best tree-shaking experience because bundlers can statically analyse import statements. CJS consumers (require()) should prefer granular subpath imports (listed below) to avoid pulling in the entire library.

Startup cost vs. bundle size ​

These are two different concerns that are often confused:

ConcernWhat it meansHow it is measured
Bundle sizeHow much code (bytes) is shipped to the client / included in the outputbundlephobia, webpack-bundle-analyzer, du -sh dist/
Startup costHow much CPU work happens when the module is first evaluated (constructors, registrations…)Profiler, console.time, cold-start benchmarks

QuickModel addresses both, but with different techniques depending on the type of dependency:

  • Peer dependencies (zod, @faker-js/faker) — loaded via createRequire at runtime; they never appear in the static import graph → eliminates both size and startup cost.
  • Internal code (IntegrityService, form-data/stream helpers) — statically imported, so they are always in the bundle, but construction is deferred → eliminates startup cost without a breaking API change.

What is loaded lazily ​

The following components are deferred until first use:

ComponentTechniqueWhen it's instantiated / loaded
zod peer dependencycreateRequire at runtimeFirst call to .getSchema('zod')
@faker-js/faker peer dependencycreateRequire at runtimeFirst call to .mock()
QMockGenerator instanceLazy static getterFirst call to any .mock()
IntegrityService instance (+ 14 transformer constructors)Lazy static getterFirst call to .$qCheckIntegrity() or .$qIsValid()

This means importing quickmodel and declaring model classes does not run a single transformer constructor — all 14+ transformer instances inside IntegrityService are only created when validation is first requested.

ts
// ✅ Module load: zero transformer constructors run
import { QModel, Quick } from 'quickmodel';

interface IOrder {
	id: number;
	status: string;
}

@Quick({ id: Number, status: String })
class Order extends QModel<IOrder> {
	declare id: number;
	declare status: string;
}

// ✅ Still no transformer constructors — just deserialization + serialization
const order = new Order({ id: 1, status: 'pending' });
const plain = order.$qSerialize();

// ⚡ HERE: 14+ transformer constructors run (once, cached after this)
const ok = order.$qIsValid();

Optional peer dependencies: zod and faker ​

Peer dependencies are loaded on demand using Node's createRequire, so they never appear in your static import graph if you don't call the relevant feature:

Optional dependencyFeatureWhen it's loaded
zodZodSchemaGenerator / getSchema('zod')First call to .getSchema('zod')
@faker-js/fakerQMockGenerator random dataFirst call to .mock()

This means bundlers will not include zod or @faker-js/faker in your bundle unless your code actually reaches those code paths.

How lazy loading helps ​

ts
// ✅ This import does NOT pull zod into your bundle
import { QModel, Quick } from 'quickmodel';

interface IUser {
	name: string;
	createdAt: string;
}

@Quick({ name: String, createdAt: Date })
class User extends QModel<IUser> {
	declare name: string;
	declare createdAt: Date;
}

// zod is only required the moment this line executes at runtime
const zodSchema = User.getSchema('zod');

Granular subpath imports ​

For maximum control over what ends up in your bundle, use the dedicated entry points instead of the main quickmodel barrel:

Import pathWhat it exportsTypical use case
quickmodelQModel, @Quick, @QType, @QRule, validators, decoratorsMain application models
quickmodel/mockQMockGenerator, QMockBuilderTest files, fixture factories
quickmodel/schemaAll 7 schema generators (Zod lazy)Schema-export utilities
quickmodel/schema/zodZodSchemaGenerator onlyTargeted Zod integration
quickmodel/advancedSerializer, deserializer, all generators, registry, transformersLibrary authors, internal tooling
quickmodel/utilsQType, QModelError, QMockBuilder, QLoggerUtility-only consumers
quickmodel/formsForm helpers and rule checkersForm handling
quickmodel/validatorsIsEmail, Min, Max, etc.Standalone validator imports
quickmodel/transformersAll transformers indexTransformer discovery
quickmodel/transformers/[name]Individual transformerSingle-transformer consumers
quickmodel/matchersMatchersMatcher-only consumers
quickmodel/typesType barrelType-only imports
quickmodel/compat/ts5/formsTC39 decorator form helpersTypeScript 5+ projects
quickmodel/coreCore internal indexAdvanced extension

Practical example: mocks only in tests ​

The mock system (faker) is never instantiated in production code — only on the first .mock() call. You can safely call .mock() from tests without it affecting production bundles:

ts
// src/user.model.ts — production code
import { QModel, Quick } from 'quickmodel';

interface IUser {
	name: string;
	age: number;
	role: string;
}

@Quick({ name: String, age: Number, role: String })
class User extends QModel<IUser> {
	declare name: string;
	declare age: number;
	declare role: string;
}

export { User };
ts
// tests/user.test.ts — test code (faker loads here, not in production)
import { User } from '../src/user.model';

// Generate a random instance
const user = User.mock().random();

// Generate with fixed overrides
const admin = User.mock().random({ role: 'admin' });

// Generate an array of 5 instances
const users = User.mock().array(5);

Practical example: schema export only ​

When using quickmodel/schema/zod directly, zod is never loaded at application startup:

ts
// scripts/export-schema.ts
import { ZodSchemaGenerator } from 'quickmodel/schema/zod';

// zod loads here — not when the application module is first imported
const schema = ZodSchemaGenerator.generate({
	className: 'User',
	decoratorConfig: { name: String, age: Number, createdAt: Date },
	properties: ['name', 'age', 'createdAt'],
});

Alternatively, use the static method on the model class (zod still loads lazily):

ts
import { QModel, Quick } from 'quickmodel';


## Feature & Coverage Comparison

<BenchmarkChart
  :only-tabs="['features', 'coverage', 'performance']"
  default-tab="features"
/>
interface IUser { name: string; age: number; createdAt: string; }

@Quick({ name: String, age: Number, createdAt: Date })
class User extends QModel<IUser> {
	declare name: string;
	declare age: number;
	declare createdAt: Date;
}

// zod loads here, on first call
const zodSchema = User.getSchema('zod');

Measuring your actual footprint ​

Use bundlephobia or your bundler's bundle analyser to verify impact after integration. A minimal import of quickmodel (models + decorators only, no mock or schema features triggered) adds approximately:

quickmodel (ESM, minified+gzip): ~8 kB
reflect-metadata (peer):          ~3 kB

Optional features loaded only when triggered:

+ zod (if getSchema('zod') is called): ~13 kB
+ @faker-js/faker (if .mock() is called): ~500+ kB

Faker in production bundles

@faker-js/faker is a development/test peer dependency. Make sure it is only imported in test code or behind a lazy path never reached in production builds.

CJS consumers: avoid the main barrel ​

When consuming QuickModel via require() (CJS), bundlers cannot tree-shake namespace re-exports (export * as Advanced, export * as Utils, etc.). To avoid pulling in components you don't need, import from the granular subpaths:

js
// ❌ May pull in extra code in CJS
const { QModel } = require('quickmodel');

// ✅ Preferred for CJS: use the specific subpath
const { QMockGenerator } = require('quickmodel/mock');
const { ZodSchemaGenerator } = require('quickmodel/schema/zod');