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: falseinpackage.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:
| Concern | What it means | How it is measured |
|---|---|---|
| Bundle size | How much code (bytes) is shipped to the client / included in the output | bundlephobia, webpack-bundle-analyzer, du -sh dist/ |
| Startup cost | How 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 viacreateRequireat 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:
| Component | Technique | When it's instantiated / loaded |
|---|---|---|
zod peer dependency | createRequire at runtime | First call to .getSchema('zod') |
@faker-js/faker peer dependency | createRequire at runtime | First call to .mock() |
QMockGenerator instance | Lazy static getter | First call to any .mock() |
IntegrityService instance (+ 14 transformer constructors) | Lazy static getter | First 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.
// ✅ 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 dependency | Feature | When it's loaded |
|---|---|---|
zod | ZodSchemaGenerator / getSchema('zod') | First call to .getSchema('zod') |
@faker-js/faker | QMockGenerator random data | First 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
// ✅ 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 path | What it exports | Typical use case |
|---|---|---|
quickmodel | QModel, @Quick, @QType, @QRule, validators, decorators | Main application models |
quickmodel/mock | QMockGenerator, QMockBuilder | Test files, fixture factories |
quickmodel/schema | All 7 schema generators (Zod lazy) | Schema-export utilities |
quickmodel/schema/zod | ZodSchemaGenerator only | Targeted Zod integration |
quickmodel/advanced | Serializer, deserializer, all generators, registry, transformers | Library authors, internal tooling |
quickmodel/utils | QType, QModelError, QMockBuilder, QLogger | Utility-only consumers |
quickmodel/forms | Form helpers and rule checkers | Form handling |
quickmodel/validators | IsEmail, Min, Max, etc. | Standalone validator imports |
quickmodel/transformers | All transformers index | Transformer discovery |
quickmodel/transformers/[name] | Individual transformer | Single-transformer consumers |
quickmodel/matchers | Matchers | Matcher-only consumers |
quickmodel/types | Type barrel | Type-only imports |
quickmodel/compat/ts5/forms | TC39 decorator form helpers | TypeScript 5+ projects |
quickmodel/core | Core internal index | Advanced 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:
// 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 };// 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:
// 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):
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 kBOptional features loaded only when triggered:
+ zod (if getSchema('zod') is called): ~13 kB
+ @faker-js/faker (if .mock() is called): ~500+ kBFaker 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:
// ❌ 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');