Prerequisites
- Basic understanding of JavaScript 📝
- TypeScript installation ⚡
- VS Code or preferred IDE 💻
What you'll learn
- Understand incremental compilation fundamentals 🎯
- Apply incremental builds in real projects 🏗️
- Debug common build issues 🐛
- Write efficient build configurations ✨
🎯 Introduction
Welcome to the world of blazing-fast TypeScript builds! 🚀 If you’ve ever waited ages for your TypeScript project to compile, this tutorial is your ticket to the fast lane!
Incremental compilation is like having a brilliant assistant who remembers what they’ve already done 🧠. Instead of rebuilding everything from scratch every time, TypeScript keeps track of what’s changed and only rebuilds what’s necessary. It’s the difference between cleaning your entire house daily vs. just tidying up the rooms you’ve used! 🏠✨
By the end of this tutorial, you’ll be building TypeScript projects faster than ever before! Let’s supercharge your development workflow! ⚡
📚 Understanding Incremental Compilation
🤔 What is Incremental Compilation?
Incremental compilation is like having a smart photo album 📸. Instead of reprinting all your photos every time you add a new one, you only print the new photos and keep the old ones as they are!
In TypeScript terms, incremental compilation:
- ✨ Tracks which files have changed since the last build
- 🚀 Only recompiles modified files and their dependencies
- 🛡️ Stores build information to make future builds faster
- 📊 Dramatically reduces build times in large projects
💡 Why Use Incremental Compilation?
Here’s why developers love incremental builds:
- Speed Boost ⚡: Builds that took minutes now take seconds
- Better Development Experience 💻: Faster feedback loops mean more productivity
- Efficient CI/CD 🔄: Continuous integration becomes lightning-fast
- Resource Savings 💰: Less CPU and memory usage
- Improved Workflow 🎯: More time coding, less time waiting
Real-world example: Imagine working on a massive e-commerce platform 🛒. With incremental compilation, changing a single product component doesn’t require rebuilding the entire checkout system!
🔧 Basic Syntax and Usage
📝 Simple Setup
Let’s start with the fundamental configuration:
// 📁 tsconfig.json
{
"compilerOptions": {
"incremental": true, // 🚀 Enable incremental compilation
"tsBuildInfoFile": ".tsbuildinfo", // 📊 Where to store build info
"outDir": "./dist", // 📦 Output directory
"rootDir": "./src", // 🌱 Source directory
"strict": true // 🛡️ Type safety first!
},
"include": [
"src/**/*" // 🎯 Include all source files
]
}
💡 Explanation: The incremental
flag tells TypeScript to track changes, while tsBuildInfoFile
specifies where to store the magic build information!
🎯 Basic Commands
Here are the commands you’ll use daily:
# 🚀 First build - creates the build info file
tsc
# ⚡ Subsequent builds - lightning fast!
tsc
# 🧹 Clean build - start fresh
tsc --build --clean
# 👀 Watch mode with incremental builds
tsc --watch
# 🔍 Force rebuild everything
tsc --build --force
💡 Practical Examples
🛒 Example 1: E-commerce Project Structure
Let’s set up a real e-commerce project:
// 📁 Project structure
// src/
// ├── components/
// │ ├── ProductCard.ts
// │ └── ShoppingCart.ts
// ├── services/
// │ ├── PaymentService.ts
// │ └── InventoryService.ts
// └── utils/
// └── formatters.ts
// 🛍️ src/components/ProductCard.ts
export interface Product {
id: string;
name: string;
price: number;
emoji: string; // Every product needs personality!
}
export class ProductCard {
constructor(private product: Product) {}
// 🎨 Render product with style
render(): string {
return `
<div class="product-card">
<span class="emoji">${this.product.emoji}</span>
<h3>${this.product.name}</h3>
<p class="price">$${this.product.price}</p>
</div>
`;
}
// 💰 Calculate discounted price
getDiscountedPrice(discount: number): number {
return this.product.price * (1 - discount);
}
}
// 🛒 src/components/ShoppingCart.ts
import { Product } from './ProductCard';
export class ShoppingCart {
private items: Map<string, { product: Product; quantity: number }> = new Map();
// ➕ Add item to cart
addItem(product: Product, quantity: number = 1): void {
const existing = this.items.get(product.id);
if (existing) {
existing.quantity += quantity;
} else {
this.items.set(product.id, { product, quantity });
}
console.log(`🎉 Added ${quantity}x ${product.emoji} ${product.name} to cart!`);
}
// 💵 Calculate total
getTotal(): number {
let total = 0;
this.items.forEach(({ product, quantity }) => {
total += product.price * quantity;
});
return total;
}
}
🏗️ Example 2: Multi-Project Build Configuration
For larger applications with multiple packages:
// 📁 tsconfig.json (root)
{
"compilerOptions": {
"incremental": true,
"composite": true, // 🧩 Enable project references
"declaration": true, // 📝 Generate .d.ts files
"declarationMap": true, // 🗺️ Source maps for declarations
"tsBuildInfoFile": "./build/.tsbuildinfo"
},
"references": [ // 🔗 Link to sub-projects
{ "path": "./packages/core" },
{ "path": "./packages/ui" },
{ "path": "./packages/api" }
]
}
// 📁 packages/core/tsconfig.json
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./dist",
"tsBuildInfoFile": "./dist/.tsbuildinfo"
},
"include": ["src/**/*"]
}
// 📁 packages/ui/tsconfig.json
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./dist",
"tsBuildInfoFile": "./dist/.tsbuildinfo"
},
"references": [
{ "path": "../core" } // 🎯 Depends on core package
],
"include": ["src/**/*"]
}
🎮 Build Commands for Multi-Project:
# 🚀 Build all projects incrementally
tsc --build
# ⚡ Build specific project
tsc --build packages/ui
# 🔄 Watch all projects
tsc --build --watch
# 🧹 Clean everything
tsc --build --clean
🚀 Advanced Concepts
🧙♂️ Advanced Build Info Management
When you’re ready to level up your build game:
// 📁 Advanced tsconfig.json
{
"compilerOptions": {
"incremental": true,
"tsBuildInfoFile": "./build-cache/.tsbuildinfo",
// 🎯 Performance optimizations
"skipLibCheck": true, // ⚡ Skip type checking of .d.ts files
"assumeChangesOnlyAffectDirectDependencies": true, // 🚀 Faster rebuilds
// 📊 Debugging build issues
"listFiles": false, // 🔍 Show which files are included
"traceResolution": false, // 🕵️ Debug module resolution
"extendedDiagnostics": false // 📈 Show build performance info
}
}
🏗️ Custom Build Scripts
For complex projects, create smart build scripts:
// 📁 scripts/build.ts
import * as fs from 'fs';
import * as path from 'path';
import { exec } from 'child_process';
interface BuildConfig {
projects: string[];
parallel: boolean;
watch: boolean;
}
class IncrementalBuilder {
constructor(private config: BuildConfig) {}
// 🚀 Smart build that checks for changes
async build(): Promise<void> {
console.log('🏗️ Starting incremental build...');
const buildInfoExists = fs.existsSync('.tsbuildinfo');
if (!buildInfoExists) {
console.log('📊 No build info found, performing full build...');
}
const startTime = Date.now();
if (this.config.parallel) {
await this.buildParallel();
} else {
await this.buildSequential();
}
const duration = Date.now() - startTime;
console.log(`✅ Build completed in ${duration}ms! 🎉`);
}
// 🔄 Parallel builds for independent projects
private async buildParallel(): Promise<void> {
const promises = this.config.projects.map(project =>
this.buildProject(project)
);
await Promise.all(promises);
}
// 📝 Build single project
private buildProject(project: string): Promise<void> {
return new Promise((resolve, reject) => {
const command = `tsc --build ${project}`;
exec(command, (error, stdout, stderr) => {
if (error) {
console.error(`❌ Build failed for ${project}:`, stderr);
reject(error);
} else {
console.log(`✅ Built ${project} successfully!`);
resolve();
}
});
});
}
}
⚠️ Common Pitfalls and Solutions
😱 Pitfall 1: Stale Build Info
// ❌ Problem - build info gets corrupted
// Your builds start failing mysteriously 🤯
// ✅ Solution - Clean and rebuild
// In your package.json:
{
"scripts": {
"build": "tsc",
"build:clean": "tsc --build --clean && tsc", // 🧹 Clean slate
"build:force": "tsc --build --force" // 💪 Force rebuild
}
}
🤯 Pitfall 2: Missing Dependencies in Project References
// ❌ Wrong way - forgetting dependencies
{
"references": [
{ "path": "./packages/ui" }
// 💥 Missing core dependency!
]
}
// ✅ Correct way - include all dependencies
{
"references": [
{ "path": "./packages/core" }, // 🎯 Core comes first
{ "path": "./packages/ui" } // 🎨 UI depends on core
]
}
🐛 Pitfall 3: Build Info Location Conflicts
// ❌ Dangerous - same build info file for different configs
// tsconfig.json
{ "compilerOptions": { "tsBuildInfoFile": ".tsbuildinfo" } }
// tsconfig.prod.json
{ "compilerOptions": { "tsBuildInfoFile": ".tsbuildinfo" } } // 💥 Conflict!
// ✅ Safe - unique build info files
// tsconfig.json
{ "compilerOptions": { "tsBuildInfoFile": "./cache/dev.tsbuildinfo" } }
// tsconfig.prod.json
{ "compilerOptions": { "tsBuildInfoFile": "./cache/prod.tsbuildinfo" } }
🛠️ Best Practices
- 🎯 Use Project References: Break large codebases into smaller, manageable projects
- 📁 Organize Build Info: Keep build info files in a dedicated cache directory
- 🧹 Regular Cleanup: Add clean scripts to your build process
- ⚡ Enable Skip Checks: Use
skipLibCheck
for faster builds in large projects - 📊 Monitor Performance: Use
extendedDiagnostics
to identify slow parts - 🔄 CI/CD Optimization: Cache build info files in your continuous integration
- 🎨 Separate Configs: Use different tsconfig files for development and production
🧪 Hands-On Exercise
🎯 Challenge: Build Performance Optimization
Create an optimized build system for a multi-package monorepo:
📋 Requirements:
- ✅ Set up incremental compilation for 3 packages:
core
,ui
,api
- 🏷️ Configure proper project references between packages
- 👤 Add build scripts for development and production
- 📅 Implement build caching and cleanup
- 🎨 Create a performance monitoring script
- 🚀 Add watch mode for development
🚀 Bonus Points:
- Implement parallel builds for independent packages
- Add build time comparison before/after incremental setup
- Create automated build info cleanup based on age
💡 Solution
🔍 Click to see solution
// 📁 Root tsconfig.json
{
"compilerOptions": {
"incremental": true,
"composite": true,
"declaration": true,
"declarationMap": true,
"skipLibCheck": true,
"tsBuildInfoFile": "./build-cache/root.tsbuildinfo"
},
"references": [
{ "path": "./packages/core" },
{ "path": "./packages/ui" },
{ "path": "./packages/api" }
]
}
// 📁 packages/core/tsconfig.json
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src",
"tsBuildInfoFile": "./dist/.tsbuildinfo"
},
"include": ["src/**/*"]
}
// 📁 packages/ui/tsconfig.json
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src",
"tsBuildInfoFile": "./dist/.tsbuildinfo"
},
"references": [
{ "path": "../core" }
],
"include": ["src/**/*"]
}
// 📁 packages/api/tsconfig.json
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src",
"tsBuildInfoFile": "./dist/.tsbuildinfo"
},
"references": [
{ "path": "../core" }
],
"include": ["src/**/*"]
}
// 📁 scripts/build-monitor.ts
import * as fs from 'fs';
interface BuildMetrics {
duration: number;
timestamp: number;
incremental: boolean;
}
class BuildMonitor {
private metricsFile = './build-metrics.json';
// 📊 Record build performance
recordBuild(duration: number, incremental: boolean): void {
const metrics: BuildMetrics = {
duration,
timestamp: Date.now(),
incremental
};
const history = this.loadHistory();
history.push(metrics);
// 📈 Keep only last 100 builds
if (history.length > 100) {
history.splice(0, history.length - 100);
}
fs.writeFileSync(this.metricsFile, JSON.stringify(history, null, 2));
this.showStats(metrics, history);
}
// 📊 Show build statistics
private showStats(current: BuildMetrics, history: BuildMetrics[]): void {
const incrementalBuilds = history.filter(b => b.incremental);
const fullBuilds = history.filter(b => !b.incremental);
console.log('📊 Build Performance:');
console.log(` ⚡ Current build: ${current.duration}ms (${current.incremental ? 'incremental' : 'full'})`);
if (incrementalBuilds.length > 0) {
const avgIncremental = incrementalBuilds.reduce((sum, b) => sum + b.duration, 0) / incrementalBuilds.length;
console.log(` 🚀 Avg incremental: ${Math.round(avgIncremental)}ms`);
}
if (fullBuilds.length > 0 && incrementalBuilds.length > 0) {
const avgFull = fullBuilds.reduce((sum, b) => sum + b.duration, 0) / fullBuilds.length;
const avgIncremental = incrementalBuilds.reduce((sum, b) => sum + b.duration, 0) / incrementalBuilds.length;
const speedup = Math.round((avgFull / avgIncremental) * 10) / 10;
console.log(` 🎯 Speed improvement: ${speedup}x faster! 💪`);
}
}
private loadHistory(): BuildMetrics[] {
try {
return JSON.parse(fs.readFileSync(this.metricsFile, 'utf8'));
} catch {
return [];
}
}
}
// 📁 package.json scripts
{
"scripts": {
"dev": "tsc --build --watch",
"build": "node scripts/monitored-build.js",
"build:clean": "tsc --build --clean && npm run build",
"build:force": "tsc --build --force",
"build:stats": "tsc --build --extendedDiagnostics",
"cache:clean": "node scripts/clean-cache.js"
}
}
🎓 Key Takeaways
You’ve mastered the art of incremental compilation! Here’s what you can now do:
- ✅ Configure incremental builds that save massive amounts of time 💪
- ✅ Set up project references for complex monorepos 🛡️
- ✅ Optimize build performance like a pro 🎯
- ✅ Debug build issues with confidence 🐛
- ✅ Monitor and improve your build pipeline! 🚀
Remember: A fast build is a happy build! Your development experience will never be the same. 🤝
🤝 Next Steps
Congratulations! 🎉 You’ve unlocked the power of lightning-fast TypeScript builds!
Here’s what to do next:
- 💻 Implement incremental compilation in your current project
- 🏗️ Set up project references if you have a monorepo
- 📚 Learn about “TypeScript Watch Mode Advanced Features” in our next tutorial
- 🌟 Share your build time improvements with your team!
Remember: Every second saved in builds is a second more for creating amazing things. Keep building, keep optimizing, and most importantly, enjoy the speed! 🚀
Happy building! 🎉🚀✨