Performance Architecture
This document describes typesugar's caching architecture, performance optimizations, and recommended configurations for development and CI.
Overview
typesugar operates through three main execution paths:
- Vite/Rollup/esbuild (unplugin) — Uses
TransformationPipelinewith per-file transforms - CLI (
typesugar build/check/watch) — Direct TypeScript compilation with macro expansion - ts-patch — Direct integration with
tscvia custom transformer
All paths share core infrastructure for caching and transformation.
Caching Architecture
Four-Layer Cache Hierarchy
┌─────────────────────────────────────────────────────────────────┐
│ L1: In-Memory Preprocessor Cache │
│ • Caches preprocessed code (HKT F<A> rewriting) │
│ • Key: file content hash │
│ • Invalidates on file content change │
└─────────────────────────────────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────────┐
│ L2: In-Memory Transform Cache │
│ • Caches full transform results with source maps │
│ • Key: file content hash + dependency hashes │
│ • Invalidates on file OR dependency change │
└─────────────────────────────────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────────┐
│ L3: Disk Transform Cache (.typesugar-cache/transforms/) │
│ • Content-addressable storage for transform results │
│ • Persists across process restarts │
│ • Key: SHA256(file + deps + version) │
│ • Manifest file for fast startup │
└─────────────────────────────────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────────┐
│ L4: Macro Expansion Cache (.typesugar-cache/expansions/) │
│ • Caches individual macro expansion results │
│ • Key: SHA256(macro name + source + args) │
│ • Reused across files and builds │
└─────────────────────────────────────────────────────────────────┘Cache Hit Flow
File Changed? ──No──> L1 Hit? ──Yes──> L2 Hit? ──Yes──> Return cached
│ │ │
│ │ No
│ │ ▼
│ │ Check L3 Disk Cache
│ │ │
│ No │
│ ▼ │
│ Preprocess ◄───────────┘
│ │
Yes ▼
└───────────> Transform
│
▼
Update L1, L2, L3, L4 CachesContent Hashing
We use xxhash64 (via xxhash-wasm) for fast, collision-resistant hashing:
import { initHasher, hashContent } from "@typesugar/transformer/cache";
// Initialize at startup (async, fallback available)
await initHasher();
// Fast 64-bit hash (16 hex chars)
const hash = hashContent(fileContent);The fallback DJB2 hash is automatically used if xxhash isn't initialized.
Disk Cache Configuration
Enabling Disk Cache
CLI:
# Enable with default path (.typesugar-cache/transforms/)
typesugar build --cache
# Enable with custom path
typesugar build --cache /tmp/typesugar-cache
# Disable explicitly
typesugar build --no-cache
# Also works for `run` - useful when iterating on a script
typesugar run examples/showcase.ts --cacheVite (unplugin):
// vite.config.ts
import typesugar from "unplugin-typesugar/vite";
export default defineConfig({
plugins: [
typesugar({
diskCache: true, // or custom path string
}),
],
});TransformationPipeline API:
import { TransformationPipeline } from "@typesugar/transformer";
const pipeline = new TransformationPipeline(compilerOptions, fileNames, {
diskCache: true, // or ".typesugar-cache/transforms"
});ts-patch (tsconfig.json):
{
"compilerOptions": {
"plugins": [
{
"transform": "@typesugar/transformer",
"cacheDir": ".typesugar-cache"
}
]
}
}ts-patch limitations: ts-patch supports
cacheDirfor macro expansion caching. However, full transform caching (diskCache) and strict mode require the CLI or unplugin.This is because
tsctype-checks the original source before transformation. To type-check expanded output, use:
typesugar build --strictinstead oftsc, ortypesugar check --strictas a validation step aftertsc
Cache Directory Structure
.typesugar-cache/
├── transforms/
│ ├── manifest.json # File → cache key mapping
│ └── <hash>.json # Transform result entries
└── expansions/
└── <hash>.json # Macro expansion resultsGitignore Configuration
Add to .gitignore:
# typesugar caches
.typesugar-cache/Incremental Compilation
ts.Program Reuse
The pipeline uses TypeScript's incremental compilation API:
// When program needs recreation after invalidation:
this.program = ts.createProgram(
fileNames,
compilerOptions,
host,
this.oldProgram // Reuses unchanged ASTs
);This significantly speeds up rebuilds by reusing AST structures for unchanged files.
Factory State Reuse
The TransformerState class preserves expensive setup work across builds:
import macroTransformerFactory, { TransformerState } from "@typesugar/transformer";
// Create once, reuse across rebuilds (watch mode)
const state = new TransformerState({ verbose: true });
// Each rebuild reuses cached state
const factory = macroTransformerFactory(program, { verbose }, state);Components preserved across rebuilds:
HygieneContext— identifier conflict trackingMacroExpansionCache— macro expansion resultsscannedFiles— files already scanned for registrationsloadedPrograms— programs with loaded macro packages
Strict Mode
Strict mode type-checks the expanded output (after macro transformation) to catch bugs in macro-generated code.
Why Strict Mode?
Normal tsc type-checks the original source code before macro expansion. This means:
| Check | Original Source | Expanded Output |
|---|---|---|
tsc (with ts-patch) | ✅ | ❌ |
typesugar build | ✅ | ❌ |
typesugar build --strict | ✅ | ✅ |
If a macro generates invalid TypeScript, you won't know until runtime—unless you use strict mode.
CLI Usage
# Typecheck expanded output
typesugar build --strict
# Combine with verbose for details
typesugar build --strict --verbose
# For ts-patch users: run as separate validation
tsc && typesugar check --strictVite Usage
// vite.config.ts
export default defineConfig({
plugins: [
typesugar({
strict: true,
}),
],
});API Usage
const pipeline = new TransformationPipeline(compilerOptions, fileNames, {
strict: true,
});
// Manually run strict typecheck
const diagnostics = pipeline.strictTypecheck();What Strict Mode Catches
- Invalid macro expansions that produce malformed TypeScript
- Type errors in generated code (e.g., derive macros)
- Missing imports in macro-generated code
- Incorrect type inference in expanded code
Performance Profiling
Enabling Profiling
# Enable detailed timing output
TYPESUGAR_PROFILE=1 typesugar buildProfile Output
=== typesugar Transform Profile ===
Per-operation timing (aggregated):
cli.build.total : 12,456.3ms (1 calls, avg 12,456.3ms)
cli.build.preprocess : 234.5ms (1 calls, avg 234.5ms)
cli.build.createProgram: 1,234.5ms (1 calls, avg 1,234.5ms)
cli.build.emit : 9,876.5ms (1 calls, avg 9,876.5ms)
Per-file timing (top 10 by total):
src/app.ts : 56.3ms (read: 0.1ms, hash: 0.0ms, transform: 45.2ms)
src/utils.ts : 34.2ms (read: 0.1ms, hash: 0.0ms, transform: 23.1ms)
...Key Metrics
readMs— File read timehashMs— Content hash computationcacheCheckMs— Cache lookup timepreprocessMs— Custom syntax preprocessingtransformMs— Macro transformationprintMs— AST printing (skipped if unchanged)
Recommended Configurations
Development (Fast Feedback)
// vite.config.ts
export default defineConfig({
plugins: [
typesugar({
diskCache: true, // Persist cache across restarts
verbose: false, // Quiet unless debugging
}),
],
});CI (Correctness First)
# .github/workflows/ci.yml
- name: Build with strict mode
run: typesugar build --strict --cache
- name: Run tests
run: pnpm test// vite.config.ts (CI override)
export default defineConfig({
plugins: [
typesugar({
strict: process.env.CI === "true",
diskCache: true, // CI caches can be restored
}),
],
});Production Build
# Full clean build with strict checking
rm -rf .typesugar-cache
typesugar build --strictWatch Mode (Maximum Speed)
# Watch mode automatically reuses state
typesugar watch --verboseThe watch mode:
- Reuses
TransformerStateacross rebuilds - Uses TypeScript's
BuilderProgramfor change detection - Only processes changed files
Performance Quick Wins
Already Implemented
- Identifier caching in safeRef — Avoids repeated
ts.factory.createIdentifier()allocations - Skip printFile for unchanged AST — Reference equality check skips unnecessary printing
- String-based fast-skip for registration scans — Checks
includes("instance(")before AST walk - xxhash64 content hashing — 64-bit collision-resistant hashing
- TransformerState reuse — Preserves expensive setup across rebuilds
- Disk transform cache — Persists transform results across process restarts
- Incremental program creation — Reuses unchanged ASTs
Profiling Tips
- Large projects: Enable disk cache to avoid cold start penalty
- Many small files: Factory reuse gives biggest wins
- Complex macros: Expansion cache amortizes macro cost
- CI pipelines: Cache
.typesugar-cache/between runs
Troubleshooting
Slow First Build
The first build is slower because:
- xxhash-wasm needs initialization
- Disk cache is empty
- No
oldProgramto reuse
Solutions:
- Pre-warm with
typesugar check - Restore CI cache from previous run
Cache Invalidation Issues
If the cache seems stale:
# Clear all caches
rm -rf .typesugar-cache
# Rebuild
typesugar build --cacheMemory Usage
For very large projects, consider:
- Reducing
maxCacheSizeoption (default: 1000) - Using
--no-cachefor one-off builds
Debugging Cache Behavior
# Verbose logging shows cache hits/misses
typesugar build --cache --verbose
# Output includes:
# [typesugar] Disk cache hit for src/app.ts
# [typesugar] Transform cache miss for src/changed.ts