@defer: Keď Angular konečne pochopil lazy loading - Problém a riešenie (Časť 1)
@defer: Keď Angular konečne pochopil lazy loading
"A vo verzii 17. Angular povedal: Nech je @defer..." ✨
Roky. Roky sme trpeli. Router-based loading? Funguje, ale musíš všetko cpať do routes. Dynamic imports? Fungujú, ale 50 riadkov boilerplate. NgComponentOutlet? Funguje, ale memory leaky striehnu za každým rohom.
A potom prišiel Angular 17 s @defer blocks. A zrazu to celé dávalo zmysel.
Prológ: Príbehy hrôzy z lazy loadingu
Každý Angular developer má svoju traumatickú skúsenosť. Tu sú moje:
// 2016: "Len lazy loadni tú route"
Developer: "Chcem lazy loadnúť tento chart komponent"
Angular: "Daj ho do samostatnej route!"
Developer: "Ale je na tej istej stránke..."
Angular: "NOVÁ ROUTE!"
Developer: "..."
// 2019: "Použi dynamic import"
Developer: "Ok, použijem dynamic import..."
*píše 50 riadkov boilerplate*
Developer: "Toto je únavné..."
// 2022: "Skús NgComponentOutlet"
Developer: "Skúsim NgComponentOutlet"
*manažuje ViewContainerRef, ComponentFactory, manuálny cleanup*
Developer: "Ja som len chcel lazy loadnúť chart..."
// 2023: Angular 17 prichádza
Developer: "@defer blocks?!"
*píše 3 riadky*
Developer: "ČO JE TOTO ZA MÁGIU?!"Kapitola 1: Staré spôsoby (aka Bolesť)
Než sa dostaneme k riešeniu, pozrime sa na to, čím sme si museli prechádzať. Lebo len tak oceníte, aký je @defer dar z nebies.
Spôsob #1: Router-based lazy loading
Jediný "oficiálny" spôsob pred Angular 17. A bol to... zážitok.
// app.routes.ts
const routes: Routes = [
{
path: 'dashboard',
loadComponent: () =>
import('./dashboard/dashboard.component')
.then(m => m.DashboardComponent)
},
{
path: 'heavy-chart', // Počkať... samostatná route?
loadComponent: () =>
import('./charts/heavy-chart.component')
.then(m => m.HeavyChartComponent)
}
];
// dashboard.component.ts
@Component({
template: `
<h1>Dashboard</h1>
<!-- Chceš zobraziť chart? Naviguj na neho! -->
<a routerLink="/heavy-chart">Zobraziť Chart</a>
<!-- Alebo... hacknúť to s router-outlet? -->
<router-outlet name="chart"></router-outlet>
`
})Čo všetko bolo zle:
❌ Chart potrebuje vlastnú route (prečo?!)
❌ Nejde kondicionálne zobraziť na tej istej stránke
❌ URL sa mení pri načítaní komponentu
❌ Manažovanie viacerých outletov je nočná mora
❌ Over-engineering pre jednoduchý lazy loadA realita bola ešte horšia:
// Čo sme chceli:
"Zobraziť chart keď užívateľ klikne na tlačidlo"
// Čo sme dostali:
routes: [
{ path: 'dashboard', component: Dashboard },
{ path: 'dashboard/chart', component: Chart }, // Samostatná route!
{ path: 'dashboard/stats', component: Stats }, // Ďalšia route!
{ path: 'dashboard/table', component: Table }, // Ešte ďalšia!
]
// URL hell:
/dashboard
/dashboard/chart
/dashboard/stats
/dashboard/table
// Developer (so slzami v očiach): "Ja som len chcel kondicionálne renderovanie..."Spôsob #2: Dynamic imports + ViewContainerRef
Keď router nestačil, siahli sme po tomto. A ľutovali sme.
@Component({
template: `
<button (click)="loadChart()">Načítať Chart</button>
<ng-container #chartContainer></ng-container>
`
})
export class DashboardComponent implements OnDestroy {
@ViewChild('chartContainer', { read: ViewContainerRef })
container!: ViewContainerRef;
private componentRef?: ComponentRef<any>;
async loadChart() {
// 1. Dynamic import
const { HeavyChartComponent } = await import(
'./charts/heavy-chart.component'
);
// 2. Alebo použiť novší prístup
this.componentRef = this.container.createComponent(
HeavyChartComponent
);
// 3. Predať inputy manuálne
this.componentRef.instance.data = this.chartData;
// 4. Subscribnúť sa na outputy manuálne
this.componentRef.instance.chartClick.subscribe(
data => this.handleChartClick(data)
);
// 5. Spustiť change detection
this.componentRef.changeDetectorRef.detectChanges();
}
ngOnDestroy() {
// 6. Manuálny cleanup (zabudneš = memory leak!)
this.componentRef?.destroy();
}
}
// Celkovo: 40+ riadkov pre jednoduchý lazy load
// Úroveň bolesti: VYSOKÁ 😤Problémy:
❌ Príliš veľa boilerplate
❌ Manuálne lifecycle management
❌ Ľahko vytvoriť memory leaky
❌ Predávanie inputov/outputov je utrpenie
❌ Problémy s change detection
❌ Testovanie je nočná moraSpôsob #3: "Postavím si vlastný framework"
Toto bol ten moment, keď developer stratil nádej a rozhodol sa vyriešiť problém sám.
// lazy-load.service.ts (100+ riadkov)
@Injectable({ providedIn: 'root' })
export class LazyLoadService {
private cache = new Map<string, Type<any>>();
async loadComponent(path: string): Promise<Type<any>> {
if (this.cache.has(path)) {
return this.cache.get(path)!;
}
const module = await import(path);
const component = Object.values(module)[0] as Type<any>;
this.cache.set(path, component);
return component;
}
// ... 80 ďalších riadkov utility metód
}
// Každý developer znovu vynachádza koleso
// Každého implementácia mierne odlišná
// Údržba je utrpenieTypický priebeh:
Týždeň 1: "Postavím si vlastný lazy load service! Bude to elegantné!"
Týždeň 2: "Hmm, toto sa komplikuje..."
Týždeň 3: "Edge cases. Edge cases všade..."
Týždeň 4: "Možno by to Angular mal podporovať natívne?"
Týždeň 5: *Angular 17 vychádza*
Týždeň 6: "...no do riti." 🤦Kapitola 2: Zjavenie
A potom to prišlo. Angular 17. @defer blocks. A svet už nikdy nebol rovnaký.
Základná syntax
Toto je to, čo sme vždy chceli:
@Component({
template: `
<h1>Dashboard</h1>
<!-- Starý spôsob: 40+ riadkov kódu -->
<!-- Nový spôsob: 3 riadky -->
@defer {
<app-heavy-chart [data]="chartData" />
}
`
})ÁNO. TO JE VŠETKO. 🎉
Žiadny ViewContainerRef. Žiadne dynamic importy. Žiadny manuálny cleanup. Proste... @defer.
S placeholderom
@Component({
template: `
@defer {
<app-heavy-chart [data]="chartData" />
} @placeholder {
<div class="skeleton">Načítavam chart...</div>
}
`
})
// Čo sa deje automaticky:
// ✅ Lazy loadne komponent
// ✅ Zobrazí placeholder
// ✅ Vymení keď je načítaný
// ✅ Žiadny manuálny cleanup
// ✅ Žiadne memory leakyS loading stavom a error handlingom
@Component({
template: `
@defer {
<app-heavy-chart [data]="chartData" />
} @loading (minimum 500ms; after 100ms) {
<app-spinner />
} @error {
<div class="error">
Nepodarilo sa načítať chart
<button (click)="retry()">Skúsiť znova</button>
</div>
} @placeholder (minimum 500ms) {
<div class="skeleton"></div>
}
`
})
// Profesionálny loading experience v 15 riadkoch!
// Žiadne manuálne state management!
// Žiadne RxJS subscriptions!Keď som to prvýkrát videl, myslel som si že to musí byť nejaký trik. Že to nemôže byť také jednoduché. Ale bolo.
Kapitola 3: Sedem magických triggerov
Tu @defer naozaj žiari. Máte 7 rôznych triggerov a môžete ich kombinovať. Je to ako mať superschopnosti.
Trigger 1: on idle (default)
// Načíta keď je prehliadač nečinný
@defer {
<app-analytics-widget />
}
// Používa requestIdleCallback pod kapotou
// Perfektné pre nekritický obsahTrigger 2: on viewport
Toto je absolútny game changer pre infinite scroll.
@Component({
template: `
<div class="posts">
@for (post of visiblePosts; track post.id) {
<app-post-card [post]="post" />
}
</div>
@defer (on viewport) {
<app-more-posts />
} @placeholder {
<div class="load-more">Scrolluj pre viac...</div>
}
`
})
// Používa IntersectionObserver API automaticky
// Žiadny manuálny observer setup!Pamätáte sa na tie 50+ riadkové IntersectionObserver implementácie? Teraz sú to 3 riadky.
Trigger 3: on interaction
@defer (on interaction) {
<app-comments [postId]="postId" />
} @placeholder {
<button>Načítať komentáre ({{ commentCount }})</button>
}
// Klikne na tlačidlo → načíta komponent
// Perfektné pre "zobraziť viac" vzory!Trigger 4: on hover
@defer (on hover) {
<app-user-card [userId]="userId" />
} @placeholder {
<div class="avatar">
<img [src]="avatarUrl" />
</div>
}
// Hover nad avatar → načíta plnú user card
// Perfektné pre tooltipy!Trigger 5: on timer
@defer (on timer(5s)) {
<app-newsletter-popup />
}
// Načíta po 5 sekundách
// Žiadny setTimeout boilerplate!Trigger 6: when condition
@defer (when isUserPremium()) {
<app-premium-features />
} @placeholder {
<div>Premium funkcie (upgradnite pre prístup)</div>
}
// Reaktívny lazy loading!
// Funguje so signals!Trigger 7: Kombinuj ich všetky!
@defer (
on interaction; // ALEBO
on viewport; // ALEBO
on timer(10s); // ALEBO
when userScrolled() // ALEBO
) {
<app-heavy-chart />
}
// Načíta keď AKÁKOĽVEK podmienka je trueBonus: Prefetching
A teraz tá najlepšia časť. Prefetch.
@defer (
on interaction; // Renderuj pri kliknutí
prefetch on hover // Ale stiahni pri hover!
) {
<app-expensive-component />
} @placeholder {
<button>Zobraziť detaily</button>
}
// Čo sa deje:
// 1. Užívateľ hoveruje tlačidlo → sťahuje chunk na pozadí
// 2. Užívateľ klikne → renderuje OKAMŽITE (už stiahnuté!)
// 3. Užívateľ si myslí že ste génius ⚡V ďalšej časti sa pozrieme na reálne porovnania, pokročilé vzory, úskalia a migračnú stratégiu.