Initial commit
This commit is contained in:
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! 🚀*
|
||||
Reference in New Issue
Block a user