Initial commit
This commit is contained in:
20
.claude-plugin/plugin.json
Normal file
20
.claude-plugin/plugin.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"name": "angular-performance",
|
||||||
|
"description": "Performance optimization with OnPush, bundle analysis, and change detection strategies",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"author": {
|
||||||
|
"name": "Ihsan - Full-Stack Developer & AI Strategist",
|
||||||
|
"url": "https://github.com/EhssanAtassi"
|
||||||
|
},
|
||||||
|
"skills": [
|
||||||
|
"./skills/change-detection/SKILL.md",
|
||||||
|
"./skills/bundle-optimization/SKILL.md"
|
||||||
|
],
|
||||||
|
"agents": [
|
||||||
|
"./agents/angular-performance-optimizer.md"
|
||||||
|
],
|
||||||
|
"commands": [
|
||||||
|
"./commands/optimize-component.md",
|
||||||
|
"./commands/analyze-bundle.md"
|
||||||
|
]
|
||||||
|
}
|
||||||
3
README.md
Normal file
3
README.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# angular-performance
|
||||||
|
|
||||||
|
Performance optimization with OnPush, bundle analysis, and change detection strategies
|
||||||
113
agents/angular-performance-optimizer.md
Normal file
113
agents/angular-performance-optimizer.md
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
# Angular Performance Optimizer
|
||||||
|
|
||||||
|
Expert in Angular performance optimization, bundle size reduction, and runtime efficiency.
|
||||||
|
|
||||||
|
## Expertise
|
||||||
|
|
||||||
|
- **Change Detection**: OnPush strategy, Signals, Zone.js optimization
|
||||||
|
- **Bundle Optimization**: Code splitting, tree shaking, lazy loading
|
||||||
|
- **Runtime Performance**: TrackBy, pure pipes, memo patterns
|
||||||
|
- **Memory Management**: Subscription cleanup, memory leak prevention
|
||||||
|
- **Build Optimization**: AOT, production configs, prerendering
|
||||||
|
- **Network Performance**: HTTP caching, compression, CDN strategies
|
||||||
|
- **Rendering Performance**: Virtual scrolling, image lazy loading
|
||||||
|
|
||||||
|
## Core Responsibilities
|
||||||
|
|
||||||
|
1. **Analyze performance bottlenecks** in Angular applications
|
||||||
|
2. **Optimize change detection** with OnPush and Signals
|
||||||
|
3. **Reduce bundle sizes** through code splitting and tree shaking
|
||||||
|
4. **Improve runtime performance** with trackBy, memoization, virtual scrolling
|
||||||
|
5. **Eliminate memory leaks** from subscriptions and DOM references
|
||||||
|
6. **Configure production builds** for maximum efficiency
|
||||||
|
7. **Implement caching strategies** for HTTP and data
|
||||||
|
|
||||||
|
## Available Commands
|
||||||
|
|
||||||
|
- `/angular-performance:optimize-component` - Optimize component performance
|
||||||
|
- `/angular-performance:analyze-bundle` - Analyze and reduce bundle size
|
||||||
|
|
||||||
|
## Available Skills
|
||||||
|
|
||||||
|
- `change-detection` - Deep dive into change detection optimization
|
||||||
|
- `bundle-optimization` - Bundle size reduction strategies
|
||||||
|
|
||||||
|
## Optimization Philosophy
|
||||||
|
|
||||||
|
- **Measure first** - Use Chrome DevTools and Lighthouse
|
||||||
|
- **Progressive enhancement** - Don't over-optimize prematurely
|
||||||
|
- **User-centric metrics** - Focus on LCP, FID, CLS
|
||||||
|
- **Balance trade-offs** - Performance vs maintainability
|
||||||
|
|
||||||
|
## Code Quality Standards
|
||||||
|
|
||||||
|
1. **OnPush by default** for all presentational components
|
||||||
|
2. **TrackBy functions** for all *ngFor loops
|
||||||
|
3. **Unsubscribe properly** using takeUntilDestroyed()
|
||||||
|
4. **Lazy load routes** for all feature modules
|
||||||
|
5. **Pure pipes** for data transformations
|
||||||
|
6. **Virtual scrolling** for large lists
|
||||||
|
7. **Image optimization** with lazy loading and WebP
|
||||||
|
|
||||||
|
## Communication Style
|
||||||
|
|
||||||
|
- Provide **concrete metrics** and benchmarks
|
||||||
|
- Show **before/after** performance comparisons
|
||||||
|
- Include **Chrome DevTools** profiling guidance
|
||||||
|
- Reference **Web Vitals** when relevant
|
||||||
|
- Give **actionable recommendations** prioritized by impact
|
||||||
|
|
||||||
|
## Example Interaction
|
||||||
|
|
||||||
|
**User**: "My user list is slow with 10,000 items"
|
||||||
|
|
||||||
|
**You**:
|
||||||
|
```typescript
|
||||||
|
// Before: Default change detection, no trackBy
|
||||||
|
@Component({
|
||||||
|
selector: 'app-user-list',
|
||||||
|
template: `
|
||||||
|
<div *ngFor="let user of users">
|
||||||
|
{{ user.name }}
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
})
|
||||||
|
export class UserListComponent {
|
||||||
|
@Input() users: User[] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// After: OnPush + trackBy + virtual scrolling
|
||||||
|
@Component({
|
||||||
|
selector: 'app-user-list',
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
template: `
|
||||||
|
<cdk-virtual-scroll-viewport itemSize="50" class="h-screen">
|
||||||
|
<div *cdkVirtualFor="let user of users; trackBy: trackById">
|
||||||
|
{{ user.name }}
|
||||||
|
</div>
|
||||||
|
</cdk-virtual-scroll-viewport>
|
||||||
|
`,
|
||||||
|
imports: [CdkVirtualScrollViewport, CdkVirtualForOf]
|
||||||
|
})
|
||||||
|
export class UserListComponent {
|
||||||
|
@Input() users: User[] = [];
|
||||||
|
|
||||||
|
trackById(index: number, user: User): string {
|
||||||
|
return user.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Performance Impact:**
|
||||||
|
- 🚀 **Initial render**: 850ms → 45ms (94% faster)
|
||||||
|
- 🚀 **Scroll performance**: 60 FPS maintained
|
||||||
|
- 🚀 **Memory usage**: 120MB → 25MB (79% reduction)
|
||||||
|
|
||||||
|
**Why it works:**
|
||||||
|
1. OnPush reduces change detection cycles
|
||||||
|
2. trackBy prevents unnecessary DOM updates
|
||||||
|
3. Virtual scrolling renders only visible items
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Measure, optimize, validate - repeat! 📊*
|
||||||
359
commands/analyze-bundle.md
Normal file
359
commands/analyze-bundle.md
Normal file
@@ -0,0 +1,359 @@
|
|||||||
|
# Analyze Bundle Command
|
||||||
|
|
||||||
|
Analyze, visualize, and optimize Angular bundle sizes.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
/angular-performance:analyze-bundle
|
||||||
|
```
|
||||||
|
|
||||||
|
## What It Does
|
||||||
|
|
||||||
|
1. Generate bundle statistics
|
||||||
|
2. Visualize bundle composition
|
||||||
|
3. Identify optimization opportunities
|
||||||
|
4. Provide actionable recommendations
|
||||||
|
|
||||||
|
## Step 1: Generate Bundle Stats
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build with stats
|
||||||
|
ng build --configuration production --stats-json
|
||||||
|
|
||||||
|
# Output: dist/<project>/stats.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 2: Analyze with Tools
|
||||||
|
|
||||||
|
### Webpack Bundle Analyzer
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install --save-dev webpack-bundle-analyzer
|
||||||
|
|
||||||
|
# Analyze
|
||||||
|
npx webpack-bundle-analyzer dist/<project>/stats.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### Source Map Explorer
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install --save-dev source-map-explorer
|
||||||
|
|
||||||
|
# Build with source maps
|
||||||
|
ng build --configuration production --source-map
|
||||||
|
|
||||||
|
# Analyze
|
||||||
|
npx source-map-explorer dist/**/*.js
|
||||||
|
```
|
||||||
|
|
||||||
|
## Optimization Strategies
|
||||||
|
|
||||||
|
### 1. Lazy Loading
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// angular.json - before
|
||||||
|
const routes: Routes = [
|
||||||
|
{ path: 'dashboard', component: DashboardComponent },
|
||||||
|
{ path: 'users', component: UsersComponent },
|
||||||
|
{ path: 'reports', component: ReportsComponent }
|
||||||
|
];
|
||||||
|
|
||||||
|
// After - lazy load feature modules
|
||||||
|
const routes: Routes = [
|
||||||
|
{
|
||||||
|
path: 'dashboard',
|
||||||
|
loadComponent: () => import('./dashboard/dashboard.component')
|
||||||
|
.then(m => m.DashboardComponent)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'users',
|
||||||
|
loadChildren: () => import('./users/users.routes')
|
||||||
|
.then(m => m.USERS_ROUTES)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'reports',
|
||||||
|
loadChildren: () => import('./reports/reports.routes')
|
||||||
|
.then(m => m.REPORTS_ROUTES)
|
||||||
|
}
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
**Impact**: Reduces initial bundle by 40-60%
|
||||||
|
|
||||||
|
### 2. Tree Shaking
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Before: Imports entire library
|
||||||
|
import * as _ from 'lodash';
|
||||||
|
|
||||||
|
// After: Import only what you need
|
||||||
|
import { debounce, throttle } from 'lodash-es';
|
||||||
|
|
||||||
|
// Or use native alternatives
|
||||||
|
const debounce = (fn, delay) => {
|
||||||
|
let timeoutId;
|
||||||
|
return (...args) => {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
timeoutId = setTimeout(() => fn(...args), delay);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Remove Unused Dependencies
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Find unused dependencies
|
||||||
|
npx depcheck
|
||||||
|
|
||||||
|
# Analyze package size before adding
|
||||||
|
npx bundlephobia <package-name>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Angular Material - Import Selectively
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Before: Import entire Material
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { MatCardModule } from '@angular/material/card';
|
||||||
|
import { MatTableModule } from '@angular/material/table';
|
||||||
|
// ... 20+ imports
|
||||||
|
|
||||||
|
// After: Only import what you use per component
|
||||||
|
@Component({
|
||||||
|
standalone: true,
|
||||||
|
imports: [MatButtonModule], // Only button for this component
|
||||||
|
template: '<button mat-raised-button>Click</button>'
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Replace Heavy Libraries
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Before: Moment.js (67KB gzipped)
|
||||||
|
import * as moment from 'moment';
|
||||||
|
const date = moment().format('YYYY-MM-DD');
|
||||||
|
|
||||||
|
// After: date-fns (6KB gzipped)
|
||||||
|
import { format } from 'date-fns';
|
||||||
|
const date = format(new Date(), 'yyyy-MM-dd');
|
||||||
|
|
||||||
|
// Or native Intl API (0KB - built-in)
|
||||||
|
const date = new Intl.DateTimeFormat('en-US').format(new Date());
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Code Splitting with Dynamic Imports
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Before: Import at top level
|
||||||
|
import { ChartComponent } from './chart/chart.component';
|
||||||
|
|
||||||
|
export class DashboardComponent {
|
||||||
|
showChart = false;
|
||||||
|
chart = ChartComponent; // Always in bundle
|
||||||
|
}
|
||||||
|
|
||||||
|
// After: Dynamic import
|
||||||
|
export class DashboardComponent {
|
||||||
|
showChart = false;
|
||||||
|
chartComponent: any;
|
||||||
|
|
||||||
|
async loadChart() {
|
||||||
|
const { ChartComponent } = await import('./chart/chart.component');
|
||||||
|
this.chartComponent = ChartComponent;
|
||||||
|
this.showChart = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. Optimize Images
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install image optimization tools
|
||||||
|
npm install --save-dev imagemin imagemin-webp imagemin-pngquant
|
||||||
|
|
||||||
|
# Use WebP format with fallbacks
|
||||||
|
<picture>
|
||||||
|
<source srcset="image.webp" type="image/webp">
|
||||||
|
<img src="image.jpg" alt="Description">
|
||||||
|
</picture>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8. Enable Build Optimizations
|
||||||
|
|
||||||
|
```json
|
||||||
|
// angular.json
|
||||||
|
{
|
||||||
|
"configurations": {
|
||||||
|
"production": {
|
||||||
|
"optimization": {
|
||||||
|
"scripts": true,
|
||||||
|
"styles": {
|
||||||
|
"minify": true,
|
||||||
|
"inlineCritical": true
|
||||||
|
},
|
||||||
|
"fonts": true
|
||||||
|
},
|
||||||
|
"outputHashing": "all",
|
||||||
|
"sourceMap": false,
|
||||||
|
"namedChunks": false,
|
||||||
|
"aot": true,
|
||||||
|
"extractLicenses": true,
|
||||||
|
"buildOptimizer": true,
|
||||||
|
"budgets": [
|
||||||
|
{
|
||||||
|
"type": "initial",
|
||||||
|
"maximumWarning": "500kb",
|
||||||
|
"maximumError": "1mb"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "anyComponentStyle",
|
||||||
|
"maximumWarning": "6kb",
|
||||||
|
"maximumError": "10kb"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 9. Differential Loading
|
||||||
|
|
||||||
|
Angular automatically generates ES5 and ES2015+ bundles:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- Modern browsers get smaller ES2015+ bundle -->
|
||||||
|
<script src="main-es2015.js" type="module"></script>
|
||||||
|
|
||||||
|
<!-- Legacy browsers get ES5 bundle -->
|
||||||
|
<script src="main-es5.js" nomodule></script>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 10. Service Worker & Caching
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Add service worker
|
||||||
|
ng add @angular/pwa
|
||||||
|
|
||||||
|
# Configures ngsw-config.json for caching
|
||||||
|
```
|
||||||
|
|
||||||
|
## Bundle Budget Enforcement
|
||||||
|
|
||||||
|
```json
|
||||||
|
// angular.json - Set budgets
|
||||||
|
"budgets": [
|
||||||
|
{
|
||||||
|
"type": "initial",
|
||||||
|
"maximumWarning": "500kb",
|
||||||
|
"maximumError": "1mb"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "anyComponentStyle",
|
||||||
|
"maximumWarning": "6kb",
|
||||||
|
"maximumError": "10kb"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Analysis Report Example
|
||||||
|
|
||||||
|
```
|
||||||
|
📦 Bundle Analysis Report
|
||||||
|
|
||||||
|
📊 Current Bundle Size:
|
||||||
|
┌─────────────────┬──────────┬──────────┐
|
||||||
|
│ Chunk │ Size │ Gzipped │
|
||||||
|
├─────────────────┼──────────┼──────────┤
|
||||||
|
│ main.js │ 1.2 MB │ 380 KB │
|
||||||
|
│ polyfills.js │ 145 KB │ 45 KB │
|
||||||
|
│ runtime.js │ 12 KB │ 4 KB │
|
||||||
|
│ styles.css │ 85 KB │ 15 KB │
|
||||||
|
├─────────────────┼──────────┼──────────┤
|
||||||
|
│ Total │ 1.44 MB │ 444 KB │
|
||||||
|
└─────────────────┴──────────┴──────────┘
|
||||||
|
|
||||||
|
⚠️ Issues Found:
|
||||||
|
|
||||||
|
1. 🔴 Lodash (287 KB) - Replace with lodash-es or native
|
||||||
|
2. 🟡 Moment.js (67 KB) - Replace with date-fns (6 KB)
|
||||||
|
3. 🟡 Feature modules not lazy loaded (420 KB in main)
|
||||||
|
4. 🟡 Angular Material fully imported (180 KB unused)
|
||||||
|
|
||||||
|
✅ Recommended Actions:
|
||||||
|
|
||||||
|
1. Lazy load feature modules → Save ~400 KB
|
||||||
|
- Dashboard, Users, Reports modules
|
||||||
|
|
||||||
|
2. Replace heavy libraries:
|
||||||
|
- lodash → lodash-es → Save 250 KB
|
||||||
|
- moment.js → date-fns → Save 60 KB
|
||||||
|
|
||||||
|
3. Tree-shake Material imports → Save 150 KB
|
||||||
|
- Import only used components
|
||||||
|
|
||||||
|
4. Enable build optimizations → Save 100 KB
|
||||||
|
- Already in angular.json
|
||||||
|
|
||||||
|
💡 Potential Savings: ~860 KB (59% reduction)
|
||||||
|
|
||||||
|
📈 After Optimization:
|
||||||
|
┌─────────────────┬──────────┬──────────┐
|
||||||
|
│ main.js │ 580 KB │ 180 KB │
|
||||||
|
│ dashboard.js │ 120 KB │ 35 KB │ (lazy)
|
||||||
|
│ users.js │ 95 KB │ 28 KB │ (lazy)
|
||||||
|
│ reports.js │ 205 KB │ 62 KB │ (lazy)
|
||||||
|
├─────────────────┼──────────┼──────────┤
|
||||||
|
│ Total Initial │ 580 KB │ 180 KB │ ⬇️ 60%
|
||||||
|
│ Total All │ 1.0 MB │ 305 KB │ ⬇️ 31%
|
||||||
|
└─────────────────┴──────────┴──────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Continuous Monitoring
|
||||||
|
|
||||||
|
### GitHub Action for Bundle Size
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: Bundle Size Check
|
||||||
|
|
||||||
|
on: [pull_request]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check-size:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: actions/setup-node@v3
|
||||||
|
- run: npm ci
|
||||||
|
- run: npm run build -- --stats-json
|
||||||
|
|
||||||
|
- name: Analyze bundle
|
||||||
|
uses: github/webpack-bundle-analyzer@v1
|
||||||
|
with:
|
||||||
|
bundle-stats: 'dist/stats.json'
|
||||||
|
|
||||||
|
- name: Check size limits
|
||||||
|
run: |
|
||||||
|
SIZE=$(stat -f%z "dist/main.*.js")
|
||||||
|
if [ $SIZE -gt 500000 ]; then
|
||||||
|
echo "Bundle too large: $SIZE bytes"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quick Wins Checklist
|
||||||
|
|
||||||
|
- [ ] Enable production mode optimizations
|
||||||
|
- [ ] Lazy load all feature modules
|
||||||
|
- [ ] Replace moment.js with date-fns
|
||||||
|
- [ ] Use lodash-es instead of lodash
|
||||||
|
- [ ] Remove unused dependencies
|
||||||
|
- [ ] Optimize images (WebP, lazy loading)
|
||||||
|
- [ ] Import Material components selectively
|
||||||
|
- [ ] Enable differential loading
|
||||||
|
- [ ] Set up bundle budgets
|
||||||
|
- [ ] Add service worker for caching
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Smaller bundles = Faster apps! 📦⚡*
|
||||||
366
commands/optimize-component.md
Normal file
366
commands/optimize-component.md
Normal file
@@ -0,0 +1,366 @@
|
|||||||
|
# Optimize Component Command
|
||||||
|
|
||||||
|
Analyze and optimize Angular component performance using OnPush, trackBy, memoization, and other strategies.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
/angular-performance:optimize-component <ComponentName>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Natural Language
|
||||||
|
|
||||||
|
- "Optimize UserListComponent performance"
|
||||||
|
- "Make my dashboard component faster"
|
||||||
|
- "Reduce change detection in ProductCardComponent"
|
||||||
|
|
||||||
|
## Optimization Strategies
|
||||||
|
|
||||||
|
### 1. OnPush Change Detection
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Before
|
||||||
|
@Component({
|
||||||
|
selector: 'app-product-list',
|
||||||
|
templateUrl: './product-list.component.html'
|
||||||
|
})
|
||||||
|
export class ProductListComponent {
|
||||||
|
@Input() products: Product[] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// After
|
||||||
|
@Component({
|
||||||
|
selector: 'app-product-list',
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
templateUrl: './product-list.component.html'
|
||||||
|
})
|
||||||
|
export class ProductListComponent {
|
||||||
|
@Input() products: Product[] = [];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Impact**: Reduces change detection checks by 80-90%
|
||||||
|
|
||||||
|
### 2. TrackBy Functions
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Before
|
||||||
|
<div *ngFor="let product of products">
|
||||||
|
{{ product.name }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
// After
|
||||||
|
@for (product of products; track product.id) {
|
||||||
|
<div>{{ product.name }}</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
// Or with trackBy function
|
||||||
|
trackById(index: number, item: Product): string {
|
||||||
|
return item.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
<div *ngFor="let product of products; trackBy: trackById">
|
||||||
|
{{ product.name }}
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Impact**: Prevents unnecessary DOM recreations, 60% faster re-renders
|
||||||
|
|
||||||
|
### 3. Memoization with Signals
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Before: Computed on every change detection
|
||||||
|
get filteredProducts(): Product[] {
|
||||||
|
return this.products().filter(p => p.price > 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
// After: Memoized with computed signal
|
||||||
|
filteredProducts = computed(() =>
|
||||||
|
this.products().filter(p => p.price > 100)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Impact**: Computation runs only when dependencies change
|
||||||
|
|
||||||
|
### 4. Pure Pipes
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Before: Impure pipe (runs on every CD)
|
||||||
|
@Pipe({ name: 'filter' })
|
||||||
|
export class FilterPipe {
|
||||||
|
transform(items: any[], searchTerm: string): any[] {
|
||||||
|
return items.filter(item =>
|
||||||
|
item.name.includes(searchTerm)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// After: Pure pipe with immutable data
|
||||||
|
@Pipe({ name: 'filter', pure: true })
|
||||||
|
export class FilterPipe {
|
||||||
|
transform(items: any[], searchTerm: string): any[] {
|
||||||
|
return items.filter(item =>
|
||||||
|
item.name.includes(searchTerm)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Component: Use immutable operations
|
||||||
|
addProduct(product: Product) {
|
||||||
|
this.products = [...this.products, product]; // ✅ New reference
|
||||||
|
// NOT: this.products.push(product); // ❌ Mutates array
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Virtual Scrolling
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Before: Renders all items
|
||||||
|
<div *ngFor="let user of users">
|
||||||
|
<app-user-card [user]="user" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
// After: Renders only visible items
|
||||||
|
<cdk-virtual-scroll-viewport itemSize="80" class="h-screen">
|
||||||
|
<app-user-card
|
||||||
|
*cdkVirtualFor="let user of users; trackBy: trackById"
|
||||||
|
[user]="user"
|
||||||
|
/>
|
||||||
|
</cdk-virtual-scroll-viewport>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Impact**: 95% faster rendering for large lists (>1000 items)
|
||||||
|
|
||||||
|
### 6. Lazy Loading Images
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Before
|
||||||
|
<img [src]="product.imageUrl" [alt]="product.name">
|
||||||
|
|
||||||
|
// After
|
||||||
|
<img
|
||||||
|
[src]="product.imageUrl"
|
||||||
|
[alt]="product.name"
|
||||||
|
loading="lazy"
|
||||||
|
[width]="300"
|
||||||
|
[height]="200"
|
||||||
|
>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. Subscription Management
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Before: Manual unsubscribe
|
||||||
|
export class UserComponent implements OnInit, OnDestroy {
|
||||||
|
private subscription = new Subscription();
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.subscription.add(
|
||||||
|
this.userService.getUser().subscribe(...)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
this.subscription.unsubscribe();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// After: takeUntilDestroyed()
|
||||||
|
export class UserComponent implements OnInit {
|
||||||
|
private destroyRef = inject(DestroyRef);
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.userService.getUser()
|
||||||
|
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||||
|
.subscribe(...);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Or use async pipe (best)
|
||||||
|
user$ = this.userService.getUser();
|
||||||
|
// Template: {{ user$ | async }}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8. Detach Change Detection
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// For components that rarely update
|
||||||
|
export class StaticContentComponent implements OnInit {
|
||||||
|
constructor(private cdr: ChangeDetectorRef) {}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.cdr.detach(); // Stop automatic change detection
|
||||||
|
}
|
||||||
|
|
||||||
|
updateContent() {
|
||||||
|
// Manually trigger when needed
|
||||||
|
this.cdr.detectChanges();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Optimization Checklist
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Component optimization checklist
|
||||||
|
@Component({
|
||||||
|
selector: 'app-optimized',
|
||||||
|
templateUrl: './optimized.component.html',
|
||||||
|
styleUrls: ['./optimized.component.scss'],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush, // ✅ 1. OnPush
|
||||||
|
standalone: true,
|
||||||
|
imports: [CommonModule, CdkVirtualScrollViewport]
|
||||||
|
})
|
||||||
|
export class OptimizedComponent {
|
||||||
|
// ✅ 2. Use signals for reactive state
|
||||||
|
private userService = inject(UserService);
|
||||||
|
users = signal<User[]>([]);
|
||||||
|
|
||||||
|
// ✅ 3. Computed for derived state
|
||||||
|
activeUsers = computed(() =>
|
||||||
|
this.users().filter(u => u.active)
|
||||||
|
);
|
||||||
|
|
||||||
|
// ✅ 4. TrackBy function
|
||||||
|
trackById(index: number, user: User): string {
|
||||||
|
return user.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
// ✅ 5. takeUntilDestroyed for subscriptions
|
||||||
|
this.userService.getUsers()
|
||||||
|
.pipe(takeUntilDestroyed())
|
||||||
|
.subscribe(users => this.users.set(users));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- Template optimizations -->
|
||||||
|
<cdk-virtual-scroll-viewport itemSize="60" class="h-screen">
|
||||||
|
<!-- ✅ 6. Virtual scrolling -->
|
||||||
|
<div *cdkVirtualFor="let user of users(); trackBy: trackById">
|
||||||
|
<!-- ✅ 7. Lazy loading images -->
|
||||||
|
<img
|
||||||
|
[src]="user.avatar"
|
||||||
|
loading="lazy"
|
||||||
|
[width]="50"
|
||||||
|
[height]="50"
|
||||||
|
>
|
||||||
|
|
||||||
|
<!-- ✅ 8. Async pipe for observables -->
|
||||||
|
<span>{{ user.name }}</span>
|
||||||
|
</div>
|
||||||
|
</cdk-virtual-scroll-viewport>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Performance Metrics
|
||||||
|
|
||||||
|
Measure with Chrome DevTools:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Add performance marks
|
||||||
|
performance.mark('component-init-start');
|
||||||
|
// ... component logic
|
||||||
|
performance.mark('component-init-end');
|
||||||
|
performance.measure('Component Init', 'component-init-start', 'component-init-end');
|
||||||
|
```
|
||||||
|
|
||||||
|
## Output Example
|
||||||
|
|
||||||
|
```
|
||||||
|
🎯 Component Optimization Report: UserListComponent
|
||||||
|
|
||||||
|
📊 Before:
|
||||||
|
- Change Detection: Default (runs on every event)
|
||||||
|
- List Rendering: 2,500 items fully rendered
|
||||||
|
- Memory Usage: 145 MB
|
||||||
|
- Initial Render: 850ms
|
||||||
|
- FPS during scroll: 25-30
|
||||||
|
|
||||||
|
✅ After Optimizations:
|
||||||
|
1. ✓ OnPush change detection
|
||||||
|
2. ✓ trackBy function for list
|
||||||
|
3. ✓ Virtual scrolling (renders ~20 items)
|
||||||
|
4. ✓ Image lazy loading
|
||||||
|
5. ✓ Subscription cleanup with takeUntilDestroyed
|
||||||
|
|
||||||
|
📈 Results:
|
||||||
|
- Change Detection: 92% reduction in checks
|
||||||
|
- List Rendering: 2,500 → 20 items (99% less DOM)
|
||||||
|
- Memory Usage: 145 MB → 28 MB (81% reduction)
|
||||||
|
- Initial Render: 850ms → 42ms (95% faster)
|
||||||
|
- FPS during scroll: 58-60 (smooth)
|
||||||
|
|
||||||
|
🎁 Bonus:
|
||||||
|
- Lighthouse Performance Score: 67 → 98
|
||||||
|
- First Contentful Paint: 2.1s → 0.8s
|
||||||
|
- Time to Interactive: 3.5s → 1.2s
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Patterns
|
||||||
|
|
||||||
|
### Pattern 1: List with Filters
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
@Component({
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
template: `
|
||||||
|
<input [(ngModel)]="searchTerm" placeholder="Search...">
|
||||||
|
|
||||||
|
<cdk-virtual-scroll-viewport itemSize="60">
|
||||||
|
<div *cdkVirtualFor="let item of filteredItems(); trackBy: trackById">
|
||||||
|
{{ item.name }}
|
||||||
|
</div>
|
||||||
|
</cdk-virtual-scroll-viewport>
|
||||||
|
`
|
||||||
|
})
|
||||||
|
export class FilterableListComponent {
|
||||||
|
items = signal<Item[]>([]);
|
||||||
|
searchTerm = signal('');
|
||||||
|
|
||||||
|
filteredItems = computed(() => {
|
||||||
|
const term = this.searchTerm().toLowerCase();
|
||||||
|
return this.items().filter(item =>
|
||||||
|
item.name.toLowerCase().includes(term)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
trackById = (index: number, item: Item) => item.id;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pattern 2: Nested Components
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Parent: Smart component with OnPush
|
||||||
|
@Component({
|
||||||
|
selector: 'app-user-dashboard',
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
template: `
|
||||||
|
<app-user-profile [user]="user()" />
|
||||||
|
<app-user-stats [stats]="stats()" />
|
||||||
|
`
|
||||||
|
})
|
||||||
|
export class UserDashboardComponent {
|
||||||
|
user = signal<User | null>(null);
|
||||||
|
stats = signal<Stats | null>(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Child: Dumb component with OnPush
|
||||||
|
@Component({
|
||||||
|
selector: 'app-user-profile',
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
template: `
|
||||||
|
<h2>{{ user.name }}</h2>
|
||||||
|
<img [src]="user.avatar" loading="lazy">
|
||||||
|
`
|
||||||
|
})
|
||||||
|
export class UserProfileComponent {
|
||||||
|
@Input({ required: true }) user!: User;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Optimize smart, measure always! 🚀*
|
||||||
61
plugin.lock.json
Normal file
61
plugin.lock.json
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
{
|
||||||
|
"$schema": "internal://schemas/plugin.lock.v1.json",
|
||||||
|
"pluginId": "gh:EhssanAtassi/angular-marketplace-developer:plugins/angular-performance",
|
||||||
|
"normalized": {
|
||||||
|
"repo": null,
|
||||||
|
"ref": "refs/tags/v20251128.0",
|
||||||
|
"commit": "80c7e4c3d1c6775c7308e13aeb375ee1d0ebd55b",
|
||||||
|
"treeHash": "cd816ab8c39027f0c39b501b788e30d10833fab6978113ffaa9df639def40645",
|
||||||
|
"generatedAt": "2025-11-28T10:10:28.483002Z",
|
||||||
|
"toolVersion": "publish_plugins.py@0.2.0"
|
||||||
|
},
|
||||||
|
"origin": {
|
||||||
|
"remote": "git@github.com:zhongweili/42plugin-data.git",
|
||||||
|
"branch": "master",
|
||||||
|
"commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390",
|
||||||
|
"repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data"
|
||||||
|
},
|
||||||
|
"manifest": {
|
||||||
|
"name": "angular-performance",
|
||||||
|
"description": "Performance optimization with OnPush, bundle analysis, and change detection strategies",
|
||||||
|
"version": "1.0.0"
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"files": [
|
||||||
|
{
|
||||||
|
"path": "README.md",
|
||||||
|
"sha256": "07ffb40f3698502afd9ed435ac898b390f6a0e9d8e01869117e22d6f2dd3f795"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "agents/angular-performance-optimizer.md",
|
||||||
|
"sha256": "c7eecdd27fcb342db3f0b5f3c5d0c01353fb2909522c7445511b742226384406"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": ".claude-plugin/plugin.json",
|
||||||
|
"sha256": "58a8fbef06bc54e05cffd8503b1651f8c161942880f379ec5b1583d14812dd2a"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "commands/analyze-bundle.md",
|
||||||
|
"sha256": "3ad9cf4b0f7f4f413b27b3e979da87156fea17a9b4e7f319132df8aa430ec2cd"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "commands/optimize-component.md",
|
||||||
|
"sha256": "be29cc07a597067dcd98f8ca4e0cc6002cd5745117c94635d2d57b93f4b9a814"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/change-detection/SKILL.md",
|
||||||
|
"sha256": "2ef4cec19aaf1af04693c91cb67b8cfe6f100ee8920e16aa27f6b6f075d43dbc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/bundle-optimization/SKILL.md",
|
||||||
|
"sha256": "6ad0c52b27ed332f46d60aa4fa947fdc18386079cbe67b9606c1318d001c475d"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"dirSha256": "cd816ab8c39027f0c39b501b788e30d10833fab6978113ffaa9df639def40645"
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"scannedAt": null,
|
||||||
|
"scannerVersion": null,
|
||||||
|
"flags": []
|
||||||
|
}
|
||||||
|
}
|
||||||
755
skills/bundle-optimization/SKILL.md
Normal file
755
skills/bundle-optimization/SKILL.md
Normal file
@@ -0,0 +1,755 @@
|
|||||||
|
# Angular Bundle Optimization
|
||||||
|
|
||||||
|
Complete guide to reducing Angular bundle sizes through code splitting, tree shaking, and lazy loading.
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
1. [Bundle Analysis](#bundle-analysis)
|
||||||
|
2. [Lazy Loading Strategies](#lazy-loading-strategies)
|
||||||
|
3. [Tree Shaking](#tree-shaking)
|
||||||
|
4. [Code Splitting](#code-splitting)
|
||||||
|
5. [Library Optimization](#library-optimization)
|
||||||
|
6. [Build Configuration](#build-configuration)
|
||||||
|
7. [Image Optimization](#image-optimization)
|
||||||
|
8. [Caching Strategies](#caching-strategies)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Bundle Analysis
|
||||||
|
|
||||||
|
### Generate Bundle Stats
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build with statistics
|
||||||
|
ng build --configuration production --stats-json
|
||||||
|
|
||||||
|
# Output: dist/<project>/stats.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### Analyze with Tools
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Webpack Bundle Analyzer
|
||||||
|
npm install --save-dev webpack-bundle-analyzer
|
||||||
|
npx webpack-bundle-analyzer dist/<project>/stats.json
|
||||||
|
|
||||||
|
# Source Map Explorer
|
||||||
|
npm install --save-dev source-map-explorer
|
||||||
|
ng build --configuration production --source-map
|
||||||
|
npx source-map-explorer dist/**/*.js
|
||||||
|
|
||||||
|
# Bundle Buddy
|
||||||
|
npx bundle-buddy dist/<project>/stats.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### Reading the Analysis
|
||||||
|
|
||||||
|
```
|
||||||
|
main.js (1.2 MB)
|
||||||
|
├── @angular/core (280 KB)
|
||||||
|
├── @angular/common (150 KB)
|
||||||
|
├── lodash (287 KB) ⚠️ Can optimize
|
||||||
|
├── moment (67 KB) ⚠️ Can replace
|
||||||
|
├── rxjs (98 KB)
|
||||||
|
└── application code (318 KB)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Lazy Loading Strategies
|
||||||
|
|
||||||
|
### Route-Based Lazy Loading
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// app.routes.ts
|
||||||
|
export const routes: Routes = [
|
||||||
|
{
|
||||||
|
path: 'dashboard',
|
||||||
|
loadComponent: () => import('./dashboard/dashboard.component')
|
||||||
|
.then(m => m.DashboardComponent)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'users',
|
||||||
|
loadChildren: () => import('./users/users.routes')
|
||||||
|
.then(m => m.USERS_ROUTES)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'admin',
|
||||||
|
loadChildren: () => import('./admin/admin.routes')
|
||||||
|
.then(m => m.ADMIN_ROUTES),
|
||||||
|
canActivate: [AuthGuard] // Only loads if authorized
|
||||||
|
}
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
**Impact**: Main bundle reduced by 40-60%
|
||||||
|
|
||||||
|
### Component Lazy Loading
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Before: Imported at module level
|
||||||
|
import { HeavyChartComponent } from './chart/heavy-chart.component';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
template: `
|
||||||
|
<app-heavy-chart *ngIf="showChart" [data]="chartData" />
|
||||||
|
`
|
||||||
|
})
|
||||||
|
export class DashboardComponent {
|
||||||
|
showChart = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// After: Dynamic import
|
||||||
|
@Component({
|
||||||
|
template: `
|
||||||
|
<ng-container *ngIf="chartComponent">
|
||||||
|
<ng-container *ngComponentOutlet="chartComponent; inputs: chartInputs" />
|
||||||
|
</ng-container>
|
||||||
|
`
|
||||||
|
})
|
||||||
|
export class DashboardComponent {
|
||||||
|
chartComponent: any;
|
||||||
|
chartInputs = { data: [] };
|
||||||
|
|
||||||
|
async loadChart() {
|
||||||
|
const { HeavyChartComponent } = await import('./chart/heavy-chart.component');
|
||||||
|
this.chartComponent = HeavyChartComponent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Preloading Strategies
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// app.config.ts
|
||||||
|
import { PreloadAllModules, NoPreloading } from '@angular/router';
|
||||||
|
|
||||||
|
export const appConfig: ApplicationConfig = {
|
||||||
|
providers: [
|
||||||
|
provideRouter(
|
||||||
|
routes,
|
||||||
|
// Option 1: No preloading (default)
|
||||||
|
withPreloading(NoPreloading),
|
||||||
|
|
||||||
|
// Option 2: Preload all
|
||||||
|
withPreloading(PreloadAllModules),
|
||||||
|
|
||||||
|
// Option 3: Custom preloading
|
||||||
|
withPreloading(CustomPreloadingStrategy)
|
||||||
|
)
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
// Custom preloading strategy
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class CustomPreloadingStrategy implements PreloadingStrategy {
|
||||||
|
preload(route: Route, load: () => Observable<any>): Observable<any> {
|
||||||
|
// Preload only routes with data.preload = true
|
||||||
|
return route.data?.['preload'] ? load() : of(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Route configuration
|
||||||
|
{
|
||||||
|
path: 'important',
|
||||||
|
loadChildren: () => import('./important/routes'),
|
||||||
|
data: { preload: true } // Will preload
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Lazy Load on Interaction
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
@Component({
|
||||||
|
template: `
|
||||||
|
<button (click)="openDialog()">Open Settings</button>
|
||||||
|
`
|
||||||
|
})
|
||||||
|
export class AppComponent {
|
||||||
|
async openDialog() {
|
||||||
|
// Load dialog only when button clicked
|
||||||
|
const { SettingsDialogComponent } = await import(
|
||||||
|
'./settings/settings-dialog.component'
|
||||||
|
);
|
||||||
|
|
||||||
|
const dialogRef = this.dialog.open(SettingsDialogComponent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tree Shaking
|
||||||
|
|
||||||
|
### How Tree Shaking Works
|
||||||
|
|
||||||
|
Tree shaking removes unused code during the build process.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// library.ts
|
||||||
|
export function usedFunction() { /* ... */ }
|
||||||
|
export function unusedFunction() { /* ... */ }
|
||||||
|
|
||||||
|
// app.ts
|
||||||
|
import { usedFunction } from './library';
|
||||||
|
|
||||||
|
usedFunction();
|
||||||
|
// unusedFunction is removed from bundle ✂️
|
||||||
|
```
|
||||||
|
|
||||||
|
### Optimize Imports
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ❌ BAD: Imports entire library
|
||||||
|
import * as _ from 'lodash';
|
||||||
|
import * as moment from 'moment';
|
||||||
|
|
||||||
|
_.debounce(fn, 300);
|
||||||
|
moment().format('YYYY-MM-DD');
|
||||||
|
|
||||||
|
// ✅ GOOD: Import only what you need
|
||||||
|
import { debounce } from 'lodash-es';
|
||||||
|
import { format } from 'date-fns';
|
||||||
|
|
||||||
|
debounce(fn, 300);
|
||||||
|
format(new Date(), 'yyyy-MM-dd');
|
||||||
|
```
|
||||||
|
|
||||||
|
### Material Components
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ❌ BAD: Import entire Material module
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { MatCardModule } from '@angular/material/card';
|
||||||
|
import { MatTableModule } from '@angular/material/table';
|
||||||
|
import { MatPaginatorModule } from '@angular/material/paginator';
|
||||||
|
// ... 20+ more imports in a shared module
|
||||||
|
|
||||||
|
// ✅ GOOD: Import per component/feature
|
||||||
|
@Component({
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
MatButtonModule, // Only button needed here
|
||||||
|
CommonModule
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class SimpleComponent { }
|
||||||
|
```
|
||||||
|
|
||||||
|
### providedIn: 'root' for Services
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ❌ BAD: Service in module providers
|
||||||
|
@NgModule({
|
||||||
|
providers: [DataService] // Always in bundle
|
||||||
|
})
|
||||||
|
|
||||||
|
// ✅ GOOD: Tree-shakeable service
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root' // Only in bundle if used
|
||||||
|
})
|
||||||
|
export class DataService { }
|
||||||
|
```
|
||||||
|
|
||||||
|
### Side-Effect-Free Code
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ❌ BAD: Side effects prevent tree shaking
|
||||||
|
export class Logger {
|
||||||
|
constructor() {
|
||||||
|
console.log('Logger initialized'); // Side effect!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Even if unused, stays in bundle
|
||||||
|
|
||||||
|
// ✅ GOOD: No side effects
|
||||||
|
export class Logger {
|
||||||
|
log(message: string) {
|
||||||
|
console.log(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Code Splitting
|
||||||
|
|
||||||
|
### Manual Chunks
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// angular.json
|
||||||
|
{
|
||||||
|
"projects": {
|
||||||
|
"app": {
|
||||||
|
"architect": {
|
||||||
|
"build": {
|
||||||
|
"configurations": {
|
||||||
|
"production": {
|
||||||
|
"optimization": {
|
||||||
|
"scripts": true,
|
||||||
|
"styles": {
|
||||||
|
"minify": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"budgets": [
|
||||||
|
{
|
||||||
|
"type": "initial",
|
||||||
|
"maximumWarning": "500kb",
|
||||||
|
"maximumError": "1mb"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"namedChunks": false,
|
||||||
|
"outputHashing": "all"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Vendor Chunking
|
||||||
|
|
||||||
|
Angular automatically creates vendor chunks:
|
||||||
|
|
||||||
|
```
|
||||||
|
dist/
|
||||||
|
├── main.js (Your code)
|
||||||
|
├── vendor.js (node_modules)
|
||||||
|
├── polyfills.js (Browser polyfills)
|
||||||
|
└── runtime.js (Webpack runtime)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom Webpack Config
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// custom-webpack.config.ts
|
||||||
|
module.exports = {
|
||||||
|
optimization: {
|
||||||
|
splitChunks: {
|
||||||
|
chunks: 'all',
|
||||||
|
cacheGroups: {
|
||||||
|
vendor: {
|
||||||
|
test: /[\\/]node_modules[\\/]/,
|
||||||
|
name: 'vendor',
|
||||||
|
priority: 10
|
||||||
|
},
|
||||||
|
common: {
|
||||||
|
minChunks: 2,
|
||||||
|
priority: 5,
|
||||||
|
reuseExistingChunk: true
|
||||||
|
},
|
||||||
|
// Separate heavy libraries
|
||||||
|
charts: {
|
||||||
|
test: /[\\/]node_modules[\\/](chart\.js|d3)[\\/]/,
|
||||||
|
name: 'charts',
|
||||||
|
priority: 15
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// angular.json
|
||||||
|
{
|
||||||
|
"architect": {
|
||||||
|
"build": {
|
||||||
|
"builder": "@angular-builders/custom-webpack:browser",
|
||||||
|
"options": {
|
||||||
|
"customWebpackConfig": {
|
||||||
|
"path": "./custom-webpack.config.ts"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Library Optimization
|
||||||
|
|
||||||
|
### Replace Heavy Libraries
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ❌ Moment.js (67 KB gzipped)
|
||||||
|
import * as moment from 'moment';
|
||||||
|
const date = moment().format('YYYY-MM-DD');
|
||||||
|
|
||||||
|
// ✅ date-fns (6 KB gzipped)
|
||||||
|
import { format } from 'date-fns';
|
||||||
|
const date = format(new Date(), 'yyyy-MM-dd');
|
||||||
|
|
||||||
|
// ✅ Native Intl API (0 KB - built-in)
|
||||||
|
const date = new Intl.DateTimeFormat('en-US', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit'
|
||||||
|
}).format(new Date());
|
||||||
|
|
||||||
|
// ❌ Lodash (24 KB gzipped)
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
// ✅ Lodash-es (tree-shakeable)
|
||||||
|
import { debounce, throttle } from 'lodash-es';
|
||||||
|
|
||||||
|
// ✅ Native alternatives
|
||||||
|
const unique = [...new Set(array)];
|
||||||
|
const grouped = Object.groupBy(array, item => item.category);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Polyfill Only What's Needed
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// polyfills.ts
|
||||||
|
// ❌ Import everything
|
||||||
|
import 'core-js';
|
||||||
|
|
||||||
|
// ✅ Import selectively
|
||||||
|
import 'core-js/es/array/flat';
|
||||||
|
import 'core-js/es/array/flat-map';
|
||||||
|
import 'core-js/es/string/replace-all';
|
||||||
|
|
||||||
|
// Or use browserslist to auto-determine
|
||||||
|
```
|
||||||
|
|
||||||
|
### Check Before Adding
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check package size before installing
|
||||||
|
npx bundlephobia <package-name>
|
||||||
|
|
||||||
|
# Example output:
|
||||||
|
# lodash: 72.4 KB (24.2 KB gzipped)
|
||||||
|
# lodash-es: 72.4 KB (tree-shakeable!)
|
||||||
|
# date-fns: 78 KB (6 KB gzipped per function)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Build Configuration
|
||||||
|
|
||||||
|
### Production Optimizations
|
||||||
|
|
||||||
|
```json
|
||||||
|
// angular.json
|
||||||
|
{
|
||||||
|
"configurations": {
|
||||||
|
"production": {
|
||||||
|
"optimization": {
|
||||||
|
"scripts": true,
|
||||||
|
"styles": {
|
||||||
|
"minify": true,
|
||||||
|
"inlineCritical": true
|
||||||
|
},
|
||||||
|
"fonts": true
|
||||||
|
},
|
||||||
|
"outputHashing": "all",
|
||||||
|
"sourceMap": false,
|
||||||
|
"namedChunks": false,
|
||||||
|
"aot": true,
|
||||||
|
"extractLicenses": true,
|
||||||
|
"buildOptimizer": true,
|
||||||
|
"fileReplacements": [
|
||||||
|
{
|
||||||
|
"replace": "src/environments/environment.ts",
|
||||||
|
"with": "src/environments/environment.prod.ts"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Bundle Budgets
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"budgets": [
|
||||||
|
{
|
||||||
|
"type": "initial",
|
||||||
|
"maximumWarning": "500kb",
|
||||||
|
"maximumError": "1mb"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "anyComponentStyle",
|
||||||
|
"maximumWarning": "6kb",
|
||||||
|
"maximumError": "10kb"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "bundle",
|
||||||
|
"name": "vendor",
|
||||||
|
"maximumWarning": "2mb",
|
||||||
|
"maximumError": "3mb"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Differential Loading
|
||||||
|
|
||||||
|
Angular automatically generates ES5 and ES2015+ bundles:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- Modern browsers (95% of users) -->
|
||||||
|
<script src="main-es2015.js" type="module"></script>
|
||||||
|
|
||||||
|
<!-- Legacy browsers (5% of users) -->
|
||||||
|
<script src="main-es5.js" nomodule></script>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Impact**: Modern browsers download 30-40% less code
|
||||||
|
|
||||||
|
### AOT Compilation
|
||||||
|
|
||||||
|
```json
|
||||||
|
// Always use AOT in production
|
||||||
|
{
|
||||||
|
"aot": true, // Ahead-of-Time compilation
|
||||||
|
"buildOptimizer": true // Additional optimizations
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Benefits**:
|
||||||
|
- Faster rendering (templates pre-compiled)
|
||||||
|
- Smaller bundles (compiler not included)
|
||||||
|
- Early error detection
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Image Optimization
|
||||||
|
|
||||||
|
### Modern Formats
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- Use WebP with fallback -->
|
||||||
|
<picture>
|
||||||
|
<source srcset="image.webp" type="image/webp">
|
||||||
|
<source srcset="image.jpg" type="image/jpeg">
|
||||||
|
<img src="image.jpg" alt="Description">
|
||||||
|
</picture>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Responsive Images
|
||||||
|
|
||||||
|
```html
|
||||||
|
<img
|
||||||
|
srcset="
|
||||||
|
image-320w.jpg 320w,
|
||||||
|
image-640w.jpg 640w,
|
||||||
|
image-1280w.jpg 1280w
|
||||||
|
"
|
||||||
|
sizes="(max-width: 320px) 280px,
|
||||||
|
(max-width: 640px) 600px,
|
||||||
|
1200px"
|
||||||
|
src="image-640w.jpg"
|
||||||
|
alt="Description"
|
||||||
|
>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Lazy Loading
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- Native lazy loading -->
|
||||||
|
<img src="image.jpg" loading="lazy" alt="Description">
|
||||||
|
|
||||||
|
<!-- With Angular -->
|
||||||
|
<img
|
||||||
|
[src]="imageUrl"
|
||||||
|
loading="lazy"
|
||||||
|
[width]="300"
|
||||||
|
[height]="200"
|
||||||
|
alt="Description"
|
||||||
|
>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Image CDN
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Use ImageKit, Cloudinary, or similar
|
||||||
|
const optimizedUrl = `https://ik.imagekit.io/demo/tr:w-400,h-300,f-webp/${imagePath}`;
|
||||||
|
|
||||||
|
// Transformations:
|
||||||
|
// tr:w-400,h-300 = resize to 400x300
|
||||||
|
// f-webp = convert to WebP
|
||||||
|
// q-80 = quality 80%
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Caching Strategies
|
||||||
|
|
||||||
|
### Service Worker
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Add Angular PWA
|
||||||
|
ng add @angular/pwa
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
// ngsw-config.json
|
||||||
|
{
|
||||||
|
"index": "/index.html",
|
||||||
|
"assetGroups": [
|
||||||
|
{
|
||||||
|
"name": "app",
|
||||||
|
"installMode": "prefetch",
|
||||||
|
"resources": {
|
||||||
|
"files": [
|
||||||
|
"/favicon.ico",
|
||||||
|
"/index.html",
|
||||||
|
"/*.css",
|
||||||
|
"/*.js"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "assets",
|
||||||
|
"installMode": "lazy",
|
||||||
|
"updateMode": "prefetch",
|
||||||
|
"resources": {
|
||||||
|
"files": [
|
||||||
|
"/assets/**",
|
||||||
|
"/*.(eot|svg|cur|jpg|png|webp|gif|otf|ttf|woff|woff2)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"dataGroups": [
|
||||||
|
{
|
||||||
|
"name": "api",
|
||||||
|
"urls": ["/api/**"],
|
||||||
|
"cacheConfig": {
|
||||||
|
"maxSize": 100,
|
||||||
|
"maxAge": "1h",
|
||||||
|
"timeout": "10s",
|
||||||
|
"strategy": "freshness"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### HTTP Caching Headers
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// HTTP interceptor
|
||||||
|
export const cacheInterceptor: HttpInterceptorFn = (req, next) => {
|
||||||
|
// Add cache headers for static assets
|
||||||
|
if (req.url.includes('/api/static/')) {
|
||||||
|
req = req.clone({
|
||||||
|
setHeaders: {
|
||||||
|
'Cache-Control': 'public, max-age=31536000, immutable'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return next(req);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Browser Caching
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Output hashing (angular.json)
|
||||||
|
{
|
||||||
|
"outputHashing": "all" // Generates unique filenames
|
||||||
|
}
|
||||||
|
|
||||||
|
// Result:
|
||||||
|
// main.abc123.js
|
||||||
|
// vendor.def456.js
|
||||||
|
// Enables long-term caching!
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Optimization Checklist
|
||||||
|
|
||||||
|
### Quick Wins
|
||||||
|
- [ ] Enable production mode
|
||||||
|
- [ ] Lazy load all feature routes
|
||||||
|
- [ ] Use lodash-es instead of lodash
|
||||||
|
- [ ] Replace moment.js with date-fns
|
||||||
|
- [ ] Enable OnPush change detection
|
||||||
|
- [ ] Add service worker for caching
|
||||||
|
- [ ] Optimize images (WebP, lazy loading)
|
||||||
|
|
||||||
|
### Medium Impact
|
||||||
|
- [ ] Analyze bundle with webpack-bundle-analyzer
|
||||||
|
- [ ] Remove unused dependencies
|
||||||
|
- [ ] Import Material components selectively
|
||||||
|
- [ ] Set up bundle budgets
|
||||||
|
- [ ] Implement preloading strategy
|
||||||
|
- [ ] Use trackBy for lists
|
||||||
|
- [ ] Add virtual scrolling for large lists
|
||||||
|
|
||||||
|
### Advanced
|
||||||
|
- [ ] Custom Webpack configuration
|
||||||
|
- [ ] Image CDN integration
|
||||||
|
- [ ] HTTP/2 push
|
||||||
|
- [ ] Critical CSS inlining
|
||||||
|
- [ ] SSR/Prerendering
|
||||||
|
- [ ] Compression (Brotli/Gzip)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Measuring Success
|
||||||
|
|
||||||
|
### Before Optimization
|
||||||
|
```
|
||||||
|
Bundle Sizes:
|
||||||
|
- main.js: 1.2 MB (380 KB gzipped)
|
||||||
|
- vendor.js: 850 KB (280 KB gzipped)
|
||||||
|
- Total: 2.05 MB (660 KB gzipped)
|
||||||
|
|
||||||
|
Lighthouse Score: 67
|
||||||
|
- FCP: 2.1s
|
||||||
|
- LCP: 3.5s
|
||||||
|
- TTI: 4.2s
|
||||||
|
```
|
||||||
|
|
||||||
|
### After Optimization
|
||||||
|
```
|
||||||
|
Bundle Sizes:
|
||||||
|
- main.js: 420 KB (135 KB gzipped)
|
||||||
|
- vendor.js: 580 KB (180 KB gzipped)
|
||||||
|
- dashboard.js: 120 KB (lazy)
|
||||||
|
- users.js: 95 KB (lazy)
|
||||||
|
- Total Initial: 1 MB (315 KB gzipped)
|
||||||
|
|
||||||
|
Lighthouse Score: 98
|
||||||
|
- FCP: 0.8s
|
||||||
|
- LCP: 1.2s
|
||||||
|
- TTI: 1.5s
|
||||||
|
|
||||||
|
Improvement:
|
||||||
|
- Bundle size: ⬇️ 51% reduction
|
||||||
|
- Load time: ⬇️ 64% faster
|
||||||
|
- Performance score: ⬆️ 46% better
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tools Reference
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Analysis
|
||||||
|
npx webpack-bundle-analyzer dist/stats.json
|
||||||
|
npx source-map-explorer dist/**/*.js
|
||||||
|
npx bundlephobia <package>
|
||||||
|
|
||||||
|
# Building
|
||||||
|
ng build --configuration production --stats-json
|
||||||
|
ng build --source-map
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
npm run lighthouse
|
||||||
|
npx @unlighthouse/cli --site http://localhost:4200
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Optimize bundles for lightning-fast loads! ⚡📦*
|
||||||
744
skills/change-detection/SKILL.md
Normal file
744
skills/change-detection/SKILL.md
Normal file
@@ -0,0 +1,744 @@
|
|||||||
|
# Angular Change Detection Optimization
|
||||||
|
|
||||||
|
Comprehensive guide to Angular change detection, OnPush strategy, Signals, and Zone.js optimization.
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
1. [How Change Detection Works](#how-change-detection-works)
|
||||||
|
2. [Default vs OnPush](#default-vs-onpush)
|
||||||
|
3. [OnPush Strategy Patterns](#onpush-strategy-patterns)
|
||||||
|
4. [Angular Signals](#angular-signals)
|
||||||
|
5. [Zone.js Optimization](#zonejs-optimization)
|
||||||
|
6. [Manual Change Detection](#manual-change-detection)
|
||||||
|
7. [Common Pitfalls](#common-pitfalls)
|
||||||
|
8. [Performance Monitoring](#performance-monitoring)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## How Change Detection Works
|
||||||
|
|
||||||
|
### The Basics
|
||||||
|
|
||||||
|
Angular uses **Zone.js** to detect async operations and trigger change detection:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Any of these trigger change detection:
|
||||||
|
setTimeout(() => {}); // ✓ Triggers CD
|
||||||
|
setInterval(() => {}, 1000); // ✓ Triggers CD
|
||||||
|
fetch('/api/data'); // ✓ Triggers CD
|
||||||
|
button.addEventListener('click'); // ✓ Triggers CD
|
||||||
|
Promise.resolve(); // ✓ Triggers CD
|
||||||
|
```
|
||||||
|
|
||||||
|
### The Component Tree
|
||||||
|
|
||||||
|
```
|
||||||
|
App Component
|
||||||
|
/ \
|
||||||
|
/ \
|
||||||
|
Header Main
|
||||||
|
/ \
|
||||||
|
/ \
|
||||||
|
Sidebar Content
|
||||||
|
/ \
|
||||||
|
/ \
|
||||||
|
List Detail
|
||||||
|
```
|
||||||
|
|
||||||
|
**Default Strategy**: When CD runs, Angular checks **every component** in the tree.
|
||||||
|
|
||||||
|
### Change Detection Cycle
|
||||||
|
|
||||||
|
1. **Event occurs** (click, HTTP response, timer)
|
||||||
|
2. **Zone.js intercepts** the async operation
|
||||||
|
3. **Angular runs change detection** from root
|
||||||
|
4. **Each component** checks if bindings changed
|
||||||
|
5. **DOM updates** if changes detected
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Default vs OnPush
|
||||||
|
|
||||||
|
### Default Strategy
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
@Component({
|
||||||
|
selector: 'app-user-list',
|
||||||
|
template: `
|
||||||
|
<div>{{ currentTime }}</div>
|
||||||
|
<button (click)="refresh()">Refresh</button>
|
||||||
|
`
|
||||||
|
})
|
||||||
|
export class UserListComponent {
|
||||||
|
currentTime = new Date().toLocaleTimeString();
|
||||||
|
|
||||||
|
refresh() {
|
||||||
|
this.currentTime = new Date().toLocaleTimeString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Behavior**:
|
||||||
|
- Checks on **every** CD cycle
|
||||||
|
- Even if data didn't change
|
||||||
|
- Performance cost: **High** (scales with components)
|
||||||
|
|
||||||
|
### OnPush Strategy
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
@Component({
|
||||||
|
selector: 'app-user-list',
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
template: `
|
||||||
|
<div>{{ currentTime }}</div>
|
||||||
|
<button (click)="refresh()">Refresh</button>
|
||||||
|
`
|
||||||
|
})
|
||||||
|
export class UserListComponent {
|
||||||
|
currentTime = new Date().toLocaleTimeString();
|
||||||
|
|
||||||
|
refresh() {
|
||||||
|
this.currentTime = new Date().toLocaleTimeString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Behavior**:
|
||||||
|
- Only checks when:
|
||||||
|
1. **@Input() reference changes**
|
||||||
|
2. **Event handler** in component template
|
||||||
|
3. **Observable emits** with async pipe
|
||||||
|
4. **Manual detection** triggered
|
||||||
|
- Performance cost: **Low** (90% reduction)
|
||||||
|
|
||||||
|
### Performance Comparison
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Scenario: 100 components, 10 CD cycles/second
|
||||||
|
|
||||||
|
// Default Strategy:
|
||||||
|
// 100 components × 10 cycles × 60 seconds = 60,000 checks/minute
|
||||||
|
|
||||||
|
// OnPush Strategy:
|
||||||
|
// ~5 components × 10 cycles × 60 seconds = 3,000 checks/minute
|
||||||
|
// ⚡ 95% reduction!
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## OnPush Strategy Patterns
|
||||||
|
|
||||||
|
### Pattern 1: Immutable Data
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
@Component({
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
|
})
|
||||||
|
export class ProductListComponent {
|
||||||
|
@Input() products: Product[] = [];
|
||||||
|
|
||||||
|
// ❌ BAD: Mutates array (OnPush won't detect)
|
||||||
|
addProductBad(product: Product) {
|
||||||
|
this.products.push(product);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ GOOD: New array reference
|
||||||
|
addProductGood(product: Product) {
|
||||||
|
this.products = [...this.products, product];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ GOOD: Immutable update
|
||||||
|
updateProduct(id: string, updates: Partial<Product>) {
|
||||||
|
this.products = this.products.map(p =>
|
||||||
|
p.id === id ? { ...p, ...updates } : p
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pattern 2: Async Pipe
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
@Component({
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
template: `
|
||||||
|
<!-- ✅ Async pipe triggers OnPush automatically -->
|
||||||
|
<div *ngFor="let user of users$ | async">
|
||||||
|
{{ user.name }}
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
})
|
||||||
|
export class UserListComponent {
|
||||||
|
users$ = this.userService.getUsers();
|
||||||
|
|
||||||
|
constructor(private userService: UserService) {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pattern 3: Event Handlers
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
@Component({
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
template: `
|
||||||
|
<button (click)="increment()">{{ count }}</button>
|
||||||
|
`
|
||||||
|
})
|
||||||
|
export class CounterComponent {
|
||||||
|
count = 0;
|
||||||
|
|
||||||
|
// ✅ Event handlers in template trigger OnPush
|
||||||
|
increment() {
|
||||||
|
this.count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pattern 4: Signals (Angular 16+)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
@Component({
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
template: `
|
||||||
|
<div>{{ count() }}</div>
|
||||||
|
<button (click)="increment()">+</button>
|
||||||
|
`
|
||||||
|
})
|
||||||
|
export class CounterComponent {
|
||||||
|
count = signal(0);
|
||||||
|
|
||||||
|
increment() {
|
||||||
|
this.count.update(c => c + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Angular Signals
|
||||||
|
|
||||||
|
### Signal Basics
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Create signal
|
||||||
|
const count = signal(0);
|
||||||
|
|
||||||
|
// Read signal
|
||||||
|
console.log(count()); // 0
|
||||||
|
|
||||||
|
// Update signal
|
||||||
|
count.set(5);
|
||||||
|
count.update(c => c + 1);
|
||||||
|
|
||||||
|
// Computed signal (derived state)
|
||||||
|
const doubled = computed(() => count() * 2);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Signals in Components
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
@Component({
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
template: `
|
||||||
|
<h2>Cart ({{ itemCount() }} items)</h2>
|
||||||
|
<div>Total: {{ total() | currency }}</div>
|
||||||
|
|
||||||
|
<div *ngFor="let item of items()">
|
||||||
|
{{ item.name }} - {{ item.price | currency }}
|
||||||
|
<button (click)="removeItem(item.id)">Remove</button>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
})
|
||||||
|
export class CartComponent {
|
||||||
|
items = signal<CartItem[]>([]);
|
||||||
|
|
||||||
|
itemCount = computed(() => this.items().length);
|
||||||
|
|
||||||
|
total = computed(() =>
|
||||||
|
this.items().reduce((sum, item) => sum + item.price, 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
addItem(item: CartItem) {
|
||||||
|
this.items.update(items => [...items, item]);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeItem(id: string) {
|
||||||
|
this.items.update(items => items.filter(i => i.id !== id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Signal Effects
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export class UserComponent {
|
||||||
|
userId = signal<string | null>(null);
|
||||||
|
user = signal<User | null>(null);
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
// Effect runs when dependencies change
|
||||||
|
effect(() => {
|
||||||
|
const id = this.userId();
|
||||||
|
if (id) {
|
||||||
|
this.userService.getUser(id).subscribe(
|
||||||
|
user => this.user.set(user)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Signals vs Observables
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Observable approach
|
||||||
|
export class ProductsComponent {
|
||||||
|
private searchTerm$ = new BehaviorSubject('');
|
||||||
|
|
||||||
|
products$ = this.searchTerm$.pipe(
|
||||||
|
debounceTime(300),
|
||||||
|
switchMap(term => this.productService.search(term))
|
||||||
|
);
|
||||||
|
|
||||||
|
search(term: string) {
|
||||||
|
this.searchTerm$.next(term);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Signal approach
|
||||||
|
export class ProductsComponent {
|
||||||
|
searchTerm = signal('');
|
||||||
|
|
||||||
|
products = computed(() => {
|
||||||
|
// Note: computed is synchronous
|
||||||
|
// For async, combine with effect
|
||||||
|
return this.productService.search(this.searchTerm());
|
||||||
|
});
|
||||||
|
|
||||||
|
search(term: string) {
|
||||||
|
this.searchTerm.set(term);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hybrid approach (best for now)
|
||||||
|
export class ProductsComponent {
|
||||||
|
searchTerm = signal('');
|
||||||
|
|
||||||
|
products$ = toObservable(this.searchTerm).pipe(
|
||||||
|
debounceTime(300),
|
||||||
|
switchMap(term => this.productService.search(term))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Zone.js Optimization
|
||||||
|
|
||||||
|
### Run Outside Zone
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export class ChartComponent {
|
||||||
|
constructor(private ngZone: NgZone) {}
|
||||||
|
|
||||||
|
startAnimation() {
|
||||||
|
// Run outside Angular's zone (no CD triggered)
|
||||||
|
this.ngZone.runOutsideAngular(() => {
|
||||||
|
const animate = () => {
|
||||||
|
// Heavy animation logic
|
||||||
|
this.updateChart();
|
||||||
|
requestAnimationFrame(animate);
|
||||||
|
};
|
||||||
|
requestAnimationFrame(animate);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manually trigger CD when needed
|
||||||
|
updateData(data: any) {
|
||||||
|
this.ngZone.run(() => {
|
||||||
|
this.chartData = data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Polling Outside Zone
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export class LiveDataComponent implements OnInit, OnDestroy {
|
||||||
|
data: any;
|
||||||
|
private interval: any;
|
||||||
|
|
||||||
|
constructor(private ngZone: NgZone) {}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
// Poll every second without triggering CD
|
||||||
|
this.ngZone.runOutsideAngular(() => {
|
||||||
|
this.interval = setInterval(() => {
|
||||||
|
this.fetchData().then(data => {
|
||||||
|
// Only trigger CD when data arrives
|
||||||
|
this.ngZone.run(() => {
|
||||||
|
this.data = data;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}, 1000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
clearInterval(this.interval);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Zone-less Angular (Experimental)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// main.ts
|
||||||
|
bootstrapApplication(AppComponent, {
|
||||||
|
providers: [
|
||||||
|
provideExperimentalZonelessChangeDetection()
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
// Component must use OnPush + Signals
|
||||||
|
@Component({
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
|
})
|
||||||
|
export class MyComponent {
|
||||||
|
count = signal(0); // Signals work without Zone.js
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Manual Change Detection
|
||||||
|
|
||||||
|
### ChangeDetectorRef
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
@Component({
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
|
})
|
||||||
|
export class ManualComponent {
|
||||||
|
data: any;
|
||||||
|
|
||||||
|
constructor(private cdr: ChangeDetectorRef) {}
|
||||||
|
|
||||||
|
// Method 1: detectChanges() - Check this component
|
||||||
|
updateData(data: any) {
|
||||||
|
this.data = data;
|
||||||
|
this.cdr.detectChanges();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method 2: markForCheck() - Mark for next CD cycle
|
||||||
|
scheduleUpdate(data: any) {
|
||||||
|
this.data = data;
|
||||||
|
this.cdr.markForCheck();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method 3: detach() - Stop automatic CD
|
||||||
|
ngOnInit() {
|
||||||
|
this.cdr.detach();
|
||||||
|
|
||||||
|
// Manual control
|
||||||
|
setInterval(() => {
|
||||||
|
this.data = new Date();
|
||||||
|
this.cdr.detectChanges();
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method 4: reattach() - Resume automatic CD
|
||||||
|
enableAutoDetection() {
|
||||||
|
this.cdr.reattach();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### When to Use Manual Detection
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ Use Case 1: Third-party lib updates
|
||||||
|
export class MapComponent {
|
||||||
|
constructor(private cdr: ChangeDetectorRef) {}
|
||||||
|
|
||||||
|
initMap() {
|
||||||
|
this.mapLibrary.on('update', (data) => {
|
||||||
|
this.mapData = data;
|
||||||
|
this.cdr.markForCheck(); // Tell Angular to check
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Use Case 2: WebSocket updates
|
||||||
|
export class LiveFeedComponent {
|
||||||
|
constructor(
|
||||||
|
private cdr: ChangeDetectorRef,
|
||||||
|
private ws: WebSocketService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.ws.messages$.subscribe(msg => {
|
||||||
|
this.messages.push(msg);
|
||||||
|
this.cdr.markForCheck();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Use Case 3: Performance-critical updates
|
||||||
|
export class GameComponent {
|
||||||
|
constructor(private cdr: ChangeDetectorRef) {}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.cdr.detach(); // Stop auto CD
|
||||||
|
|
||||||
|
// Game loop
|
||||||
|
const loop = () => {
|
||||||
|
this.updateGame();
|
||||||
|
|
||||||
|
// Only trigger CD when rendering
|
||||||
|
if (this.shouldRender) {
|
||||||
|
this.cdr.detectChanges();
|
||||||
|
}
|
||||||
|
|
||||||
|
requestAnimationFrame(loop);
|
||||||
|
};
|
||||||
|
requestAnimationFrame(loop);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Common Pitfalls
|
||||||
|
|
||||||
|
### Pitfall 1: Mutating @Input
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ❌ BAD: Parent change won't trigger OnPush
|
||||||
|
@Component({
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
|
})
|
||||||
|
export class ChildComponent {
|
||||||
|
@Input() user: User;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parent mutates object
|
||||||
|
this.user.name = 'Updated'; // OnPush child won't detect
|
||||||
|
|
||||||
|
// ✅ GOOD: Create new reference
|
||||||
|
this.user = { ...this.user, name: 'Updated' };
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pitfall 2: Nested Objects
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ❌ BAD: Nested mutation
|
||||||
|
@Input() config = { theme: { color: 'blue' } };
|
||||||
|
|
||||||
|
this.config.theme.color = 'red'; // Won't trigger OnPush
|
||||||
|
|
||||||
|
// ✅ GOOD: Deep immutable update
|
||||||
|
this.config = {
|
||||||
|
...this.config,
|
||||||
|
theme: { ...this.config.theme, color: 'red' }
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pitfall 3: Array Modifications
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ❌ BAD: Mutating array
|
||||||
|
@Input() items: string[] = [];
|
||||||
|
|
||||||
|
this.items.push('new'); // Won't trigger
|
||||||
|
this.items.splice(0, 1); // Won't trigger
|
||||||
|
this.items[0] = 'updated'; // Won't trigger
|
||||||
|
|
||||||
|
// ✅ GOOD: New array
|
||||||
|
this.items = [...this.items, 'new'];
|
||||||
|
this.items = this.items.filter((_, i) => i !== 0);
|
||||||
|
this.items = this.items.map((item, i) => i === 0 ? 'updated' : item);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pitfall 4: Async without async pipe
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ❌ BAD: Manual subscription in OnPush
|
||||||
|
@Component({
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
|
})
|
||||||
|
export class BadComponent {
|
||||||
|
users: User[] = [];
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.userService.getUsers().subscribe(users => {
|
||||||
|
this.users = users; // Won't trigger OnPush!
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ GOOD: Use async pipe
|
||||||
|
@Component({
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
template: `<div *ngFor="let user of users$ | async">...</div>`
|
||||||
|
})
|
||||||
|
export class GoodComponent {
|
||||||
|
users$ = this.userService.getUsers();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ GOOD: Or use markForCheck
|
||||||
|
@Component({
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
|
})
|
||||||
|
export class AlsoGoodComponent {
|
||||||
|
users: User[] = [];
|
||||||
|
|
||||||
|
constructor(private cdr: ChangeDetectorRef) {}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.userService.getUsers().subscribe(users => {
|
||||||
|
this.users = users;
|
||||||
|
this.cdr.markForCheck();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Performance Monitoring
|
||||||
|
|
||||||
|
### Chrome DevTools Profiler
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 1. Open Chrome DevTools
|
||||||
|
// 2. Performance tab
|
||||||
|
// 3. Click Record
|
||||||
|
// 4. Interact with app
|
||||||
|
// 5. Stop recording
|
||||||
|
|
||||||
|
// Look for:
|
||||||
|
// - Long tasks (>50ms)
|
||||||
|
// - Excessive change detection
|
||||||
|
// - Layout thrashing
|
||||||
|
```
|
||||||
|
|
||||||
|
### Angular DevTools
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 1. Install Angular DevTools extension
|
||||||
|
// 2. Open DevTools > Angular tab
|
||||||
|
// 3. Profiler section
|
||||||
|
// 4. Record profile
|
||||||
|
// 5. Analyze change detection cycles
|
||||||
|
|
||||||
|
// Shows:
|
||||||
|
// - Change detection timing
|
||||||
|
// - Component tree
|
||||||
|
// - Change detection strategy per component
|
||||||
|
```
|
||||||
|
|
||||||
|
### Performance Marks
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export class ProfiledComponent {
|
||||||
|
loadData() {
|
||||||
|
performance.mark('data-load-start');
|
||||||
|
|
||||||
|
this.service.getData().subscribe(data => {
|
||||||
|
this.data = data;
|
||||||
|
|
||||||
|
performance.mark('data-load-end');
|
||||||
|
performance.measure(
|
||||||
|
'Data Load',
|
||||||
|
'data-load-start',
|
||||||
|
'data-load-end'
|
||||||
|
);
|
||||||
|
|
||||||
|
// View in Performance tab
|
||||||
|
const measure = performance.getEntriesByName('Data Load')[0];
|
||||||
|
console.log(`Data load took ${measure.duration}ms`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Change Detection Counter
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Directive to count CD cycles
|
||||||
|
@Directive({
|
||||||
|
selector: '[cdCounter]',
|
||||||
|
standalone: true
|
||||||
|
})
|
||||||
|
export class CdCounterDirective implements DoCheck {
|
||||||
|
private count = 0;
|
||||||
|
|
||||||
|
ngDoCheck() {
|
||||||
|
this.count++;
|
||||||
|
console.log(`CD cycle #${this.count}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage
|
||||||
|
<app-my-component cdCounter></app-my-component>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Best Practices Summary
|
||||||
|
|
||||||
|
✅ **DO**:
|
||||||
|
- Use `ChangeDetectionStrategy.OnPush` by default
|
||||||
|
- Use `async` pipe for observables
|
||||||
|
- Use Angular Signals for reactive state
|
||||||
|
- Create new references for objects/arrays
|
||||||
|
- Use `trackBy` for lists
|
||||||
|
- Run heavy operations outside Zone
|
||||||
|
- Use `markForCheck()` for third-party integrations
|
||||||
|
|
||||||
|
❌ **DON'T**:
|
||||||
|
- Mutate `@Input()` properties
|
||||||
|
- Manually subscribe in OnPush without `markForCheck()`
|
||||||
|
- Use Default strategy unless necessary
|
||||||
|
- Perform heavy computations in getters
|
||||||
|
- Forget to unsubscribe from observables
|
||||||
|
- Mix imperative and reactive patterns
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Reference
|
||||||
|
|
||||||
|
### Change Detection Triggers (OnPush)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
✓ @Input() reference changes
|
||||||
|
✓ Event from template
|
||||||
|
✓ async pipe emission
|
||||||
|
✓ cdr.detectChanges()
|
||||||
|
✓ cdr.markForCheck()
|
||||||
|
✓ Signal updates (in templates)
|
||||||
|
|
||||||
|
✗ Object mutation
|
||||||
|
✗ Array.push/splice/pop
|
||||||
|
✗ Nested property changes
|
||||||
|
✗ Manual subscription
|
||||||
|
✗ setTimeout/setInterval (without events)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Strategy Comparison
|
||||||
|
|
||||||
|
| Feature | Default | OnPush |
|
||||||
|
|---------|---------|--------|
|
||||||
|
| Checks on every CD | ✓ | ✗ |
|
||||||
|
| Checks on @Input change | ✓ | ✓ (reference) |
|
||||||
|
| Checks on events | ✓ | ✓ |
|
||||||
|
| Checks on async pipe | ✓ | ✓ |
|
||||||
|
| Performance | Low | High |
|
||||||
|
| Complexity | Low | Medium |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Master change detection for blazing-fast Angular apps! 🚀*
|
||||||
Reference in New Issue
Block a user