Obsah článku / Contents
@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:

typescript
// 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.

typescript
// 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 load

A realita bola ešte horšia:

typescript
// Č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.

typescript
@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á mora

Spô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.

typescript
// 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 utrpenie

Typický 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:

typescript
@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

typescript
@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 leaky

S loading stavom a error handlingom

typescript
@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)

typescript
// Načíta keď je prehliadač nečinný
@defer {
  <app-analytics-widget />
}

// Používa requestIdleCallback pod kapotou
// Perfektné pre nekritický obsah

Trigger 2: on viewport

Toto je absolútny game changer pre infinite scroll.

typescript
@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

typescript
@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

typescript
@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

typescript
@defer (on timer(5s)) {
  <app-newsletter-popup />
}

// Načíta po 5 sekundách
// Žiadny setTimeout boilerplate!

Trigger 6: when condition

typescript
@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!

typescript
@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 true

Bonus: Prefetching

A teraz tá najlepšia časť. Prefetch.

typescript
@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.

Všetky články