Prerequisites
- Basic understanding of JavaScript ๐
- TypeScript installation โก
- VS Code or preferred IDE ๐ป
What you'll learn
- Understand the concept fundamentals ๐ฏ
- Apply the concept in real projects ๐๏ธ
- Debug common issues ๐
- Write type-safe code โจ
๐ฏ Introduction
Welcome to this exciting tutorial on lazy loading and code splitting! ๐ In this guide, weโll explore how to dramatically improve your applicationโs performance by loading code only when itโs needed.
Youโll discover how lazy loading can transform your TypeScript applications from slow-loading behemoths into lightning-fast experiences. Whether youโre building SPAs ๐, e-commerce sites ๐, or dashboards ๐, understanding code splitting is essential for delivering snappy user experiences.
By the end of this tutorial, youโll feel confident implementing lazy loading patterns in your own projects! Letโs dive in! ๐โโ๏ธ
๐ Understanding Lazy Loading & Code Splitting
๐ค What is Lazy Loading?
Lazy loading is like ordering food ร la carte instead of a buffet ๐ฝ๏ธ. Think of it as loading resources only when your users actually need them, rather than serving everything upfront.
In TypeScript terms, lazy loading means splitting your application into smaller chunks and loading them on-demand. This means you can:
- โจ Reduce initial bundle size
- ๐ Improve first page load speed
- ๐ก๏ธ Load features only when accessed
- ๐ก Save bandwidth for your users
๐ก Why Use Code Splitting?
Hereโs why developers love code splitting:
- Faster Initial Load ๐โโ๏ธ: Users see content quicker
- Better Performance ๐ป: Less JavaScript to parse upfront
- Smarter Resource Usage ๐: Load only whatโs needed
- Improved User Experience ๐ง: Smooth, responsive interfaces
Real-world example: Imagine building an admin dashboard ๐. With code splitting, users accessing only the analytics page wonโt download the entire user management module code!
๐ง Basic Syntax and Usage
๐ Simple Dynamic Import
Letโs start with a friendly example:
// ๐ Traditional import (loads immediately)
import { HeavyComponent } from './HeavyComponent';
// ๐จ Dynamic import (loads on demand)
const loadHeavyComponent = async () => {
const module = await import('./HeavyComponent');
return module.HeavyComponent;
};
// ๐ Using the lazy-loaded component
async function renderWhenNeeded() {
console.log('Loading component... โณ');
const Component = await loadHeavyComponent();
console.log('Component loaded! โจ');
}
๐ก Explanation: Notice how we use import()
as a function! This tells bundlers to create a separate chunk for this module.
๐ฏ Common Patterns
Here are patterns youโll use daily:
// ๐๏ธ Pattern 1: React lazy loading
import React, { lazy, Suspense } from 'react';
// ๐จ Lazy load a component
const Dashboard = lazy(() => import('./Dashboard'));
// ๐ Pattern 2: Route-based splitting
const routes = {
home: () => import('./pages/Home'),
profile: () => import('./pages/Profile'),
settings: () => import('./pages/Settings')
};
// ๐ฆ Pattern 3: Feature-based splitting
interface Feature {
name: string;
loader: () => Promise<any>;
emoji: string;
}
const features: Feature[] = [
{ name: 'charts', loader: () => import('./features/Charts'), emoji: '๐' },
{ name: 'reports', loader: () => import('./features/Reports'), emoji: '๐' },
{ name: 'export', loader: () => import('./features/Export'), emoji: '๐พ' }
];
๐ก Practical Examples
๐ Example 1: E-commerce Product Gallery
Letโs build something real:
// ๐๏ธ Product gallery with lazy loading
interface Product {
id: string;
name: string;
price: number;
imageUrl: string;
hasVideo: boolean;
emoji: string;
}
// ๐ฅ Lazy load video player only when needed
class ProductGallery {
private videoPlayer: any = null;
// ๐ธ Display product images immediately
displayProduct(product: Product): void {
console.log(`${product.emoji} Showing ${product.name}`);
if (product.hasVideo) {
this.loadVideoPlayer();
}
}
// ๐ Lazy load the video player
private async loadVideoPlayer(): Promise<void> {
if (!this.videoPlayer) {
console.log('โณ Loading video player...');
const { VideoPlayer } = await import('./VideoPlayer');
this.videoPlayer = new VideoPlayer();
console.log('โ
Video player ready!');
}
}
}
// ๐ฎ Usage example
const gallery = new ProductGallery();
gallery.displayProduct({
id: '1',
name: 'Gaming Laptop',
price: 999,
imageUrl: 'laptop.jpg',
hasVideo: true,
emoji: '๐ป'
});
๐ฏ Try it yourself: Add lazy loading for product reviews and specifications!
๐ฎ Example 2: Game Level Loading
Letโs make it fun:
// ๐ Game with lazy-loaded levels
interface GameLevel {
id: number;
name: string;
difficulty: 'easy' | 'medium' | 'hard';
assets: string[];
}
class GameEngine {
private loadedLevels: Map<number, any> = new Map();
private currentLevel: any = null;
// ๐ฎ Load level on demand
async loadLevel(levelId: number): Promise<void> {
console.log(`๐ฏ Loading level ${levelId}...`);
// ๐ก Check if already loaded
if (this.loadedLevels.has(levelId)) {
this.currentLevel = this.loadedLevels.get(levelId);
console.log('โก Level loaded from cache!');
return;
}
// ๐ Dynamically import level module
try {
const levelModule = await import(`./levels/Level${levelId}`);
const level = new levelModule.default();
this.loadedLevels.set(levelId, level);
this.currentLevel = level;
console.log(`โจ Level ${levelId} ready to play!`);
} catch (error) {
console.error(`โ Failed to load level ${levelId}:`, error);
}
}
// ๐ช Preload next level in background
async preloadNextLevel(nextLevelId: number): Promise<void> {
console.log(`๐ฆ Preloading level ${nextLevelId} in background...`);
// ๐จ Use requestIdleCallback for non-blocking load
if ('requestIdleCallback' in window) {
requestIdleCallback(() => this.loadLevel(nextLevelId));
} else {
setTimeout(() => this.loadLevel(nextLevelId), 1000);
}
}
}
// ๐ฏ Let's play!
const game = new GameEngine();
await game.loadLevel(1);
game.preloadNextLevel(2); // ๐ Smart preloading!
๐ Advanced Concepts
๐งโโ๏ธ Advanced Topic 1: Type-Safe Dynamic Imports
When youโre ready to level up, try this advanced pattern:
// ๐ฏ Type-safe dynamic imports with generics
type ModuleLoader<T> = () => Promise<{ default: T }>;
interface LazyModule<T> {
loader: ModuleLoader<T>;
instance?: T;
loading?: Promise<T>;
sparkles: "โจ" | "๐" | "๐ซ";
}
// ๐ช Generic lazy loader with caching
class LazyLoader<T> {
private modules: Map<string, LazyModule<T>> = new Map();
register(name: string, loader: ModuleLoader<T>): void {
this.modules.set(name, {
loader,
sparkles: "โจ"
});
}
async load(name: string): Promise<T> {
const module = this.modules.get(name);
if (!module) throw new Error(`Module ${name} not found! ๐ฑ`);
// ๐ก Return cached instance
if (module.instance) return module.instance;
// ๐ Return ongoing load
if (module.loading) return module.loading;
// ๐ Start new load
module.loading = module.loader()
.then(m => {
module.instance = m.default;
module.sparkles = "๐";
return m.default;
});
return module.loading;
}
}
๐๏ธ Advanced Topic 2: Webpack Magic Comments
For the brave developers using webpack:
// ๐ Advanced webpack code splitting
interface ChunkConfig {
name: string;
priority: number;
prefetch?: boolean;
preload?: boolean;
}
// ๐จ Smart chunk loading with hints
async function loadFeature(feature: string): Promise<any> {
switch (feature) {
case 'analytics':
// ๐ High priority, preload
return import(
/* webpackChunkName: "analytics" */
/* webpackPreload: true */
'./features/Analytics'
);
case 'settings':
// โ๏ธ Low priority, prefetch
return import(
/* webpackChunkName: "settings" */
/* webpackPrefetch: true */
'./features/Settings'
);
case 'admin':
// ๐ On-demand only
return import(
/* webpackChunkName: "admin" */
/* webpackMode: "lazy" */
'./features/Admin'
);
default:
throw new Error(`Unknown feature: ${feature} ๐คทโโ๏ธ`);
}
}
โ ๏ธ Common Pitfalls and Solutions
๐ฑ Pitfall 1: Missing Type Definitions
// โ Wrong way - losing type safety!
const Component = lazy(() => import('./MyComponent'));
// No TypeScript types! ๐ฐ
// โ
Correct way - preserve types!
const Component = lazy<React.ComponentType<Props>>(
() => import('./MyComponent')
);
// Full type safety! ๐ก๏ธ
๐คฏ Pitfall 2: Loading State Nightmares
// โ Dangerous - no loading state!
async function loadData() {
const module = await import('./DataModule');
return module.fetchData(); // ๐ฅ User sees nothing while loading!
}
// โ
Safe - proper loading states!
function DataLoader() {
const [loading, setLoading] = useState(true);
const [data, setData] = useState(null);
useEffect(() => {
import('./DataModule').then(async (module) => {
const result = await module.fetchData();
setData(result);
setLoading(false);
});
}, []);
if (loading) return <div>Loading... โณ</div>;
return <div>Data loaded! โจ</div>;
}
๐ ๏ธ Best Practices
- ๐ฏ Split by Route: Each page = separate chunk
- ๐ Group Related Code: Keep dependencies together
- ๐ก๏ธ Handle Errors: Always catch import failures
- ๐จ Show Loading States: Never leave users hanging
- โจ Preload Critical Paths: Anticipate user navigation
๐งช Hands-On Exercise
๐ฏ Challenge: Build a Dashboard with Lazy Widgets
Create a customizable dashboard with lazy-loaded widgets:
๐ Requirements:
- โ Dashboard with multiple widget types (charts, tables, metrics)
- ๐ท๏ธ Each widget loads only when added to dashboard
- ๐ค Save userโs widget preferences
- ๐ Preload commonly used widgets
- ๐จ Each widget has its own loading state!
๐ Bonus Points:
- Add widget error boundaries
- Implement retry logic for failed loads
- Create a widget marketplace with search
๐ก Solution
๐ Click to see solution
// ๐ฏ Our lazy-loaded dashboard system!
interface Widget {
id: string;
type: 'chart' | 'table' | 'metric' | 'map';
title: string;
emoji: string;
position: { x: number; y: number };
}
interface WidgetModule {
default: React.ComponentType<any>;
}
class DashboardManager {
private widgets: Map<string, Widget> = new Map();
private loaders: Map<string, () => Promise<WidgetModule>> = new Map();
constructor() {
// ๐ฆ Register widget loaders
this.loaders.set('chart', () => import('./widgets/ChartWidget'));
this.loaders.set('table', () => import('./widgets/TableWidget'));
this.loaders.set('metric', () => import('./widgets/MetricWidget'));
this.loaders.set('map', () => import('./widgets/MapWidget'));
}
// โ Add widget to dashboard
async addWidget(widget: Widget): Promise<React.ComponentType> {
console.log(`โ Adding ${widget.emoji} ${widget.title}`);
const loader = this.loaders.get(widget.type);
if (!loader) throw new Error(`Unknown widget type: ${widget.type}`);
try {
const module = await loader();
this.widgets.set(widget.id, widget);
console.log(`โ
Widget ${widget.title} loaded!`);
return module.default;
} catch (error) {
console.error(`โ Failed to load widget:`, error);
throw error;
}
}
// ๐ Preload popular widgets
async preloadCommonWidgets(): Promise<void> {
const commonTypes = ['chart', 'metric'];
console.log('๐ฆ Preloading common widgets...');
await Promise.all(
commonTypes.map(type => {
const loader = this.loaders.get(type);
return loader?.();
})
);
console.log('โจ Common widgets preloaded!');
}
}
// ๐ฎ React component with error boundary
const LazyWidget: React.FC<{ widget: Widget }> = ({ widget }) => {
const [Component, setComponent] = useState<React.ComponentType | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
const dashboard = new DashboardManager();
dashboard.addWidget(widget)
.then(setComponent)
.catch(setError)
.finally(() => setLoading(false));
}, [widget]);
if (loading) return <div>Loading {widget.emoji}...</div>;
if (error) return <div>Failed to load widget ๐ข</div>;
if (!Component) return null;
return <Component {...widget} />;
};
// ๐จ Usage
const myDashboard = new DashboardManager();
myDashboard.preloadCommonWidgets(); // ๐ Smart preloading!
const widget: Widget = {
id: '1',
type: 'chart',
title: 'Sales Analytics',
emoji: '๐',
position: { x: 0, y: 0 }
};
๐ Key Takeaways
Youโve learned so much! Hereโs what you can now do:
- โ Implement lazy loading with confidence ๐ช
- โ Split code intelligently for better performance ๐ก๏ธ
- โ Handle loading states like a pro ๐ฏ
- โ Debug code splitting issues effectively ๐
- โ Build faster applications with TypeScript! ๐
Remember: Code splitting isnโt about making your code complex - itโs about making your apps faster and more user-friendly! ๐ค
๐ค Next Steps
Congratulations! ๐ Youโve mastered lazy loading and code splitting!
Hereโs what to do next:
- ๐ป Practice with the dashboard exercise above
- ๐๏ธ Analyze your current projects for splitting opportunities
- ๐ Move on to our next tutorial: Tree Shaking
- ๐ Share your performance wins with others!
Remember: Every millisecond saved in load time makes users happier. Keep optimizing, keep learning, and most importantly, have fun! ๐
Happy coding! ๐๐โจ