@defer

@defer holds back part of a template until a trigger fires, so the first screen renders sooner. On Lynx it delays creating views, not downloading code: the deferred component's code is already in your bundle.

Defer until a condition is true

src/app/feed.ts
import { Component, signal } from '@angular/core';
import { LYNX_ELEMENTS } from '@blotch/angular-lynx';
import { Comments } from './comments';

@Component({
  selector: 'app-feed',
  imports: [LYNX_ELEMENTS, Comments],
  template: `
    <view (bindtap)="showComments.set(true)">
      <text>Show comments</text>
    </view>

    @defer (when showComments()) {
      <app-comments />
    } @placeholder {
      <text>Tap to see comments</text>
    }
  `,
})
export class Feed {
  readonly showComments = signal(false);
}
  • Before the trigger fires, Angular renders @placeholder. <app-comments> isn't created, so none of its native elements exist yet.
  • When showComments() becomes true, Angular creates <app-comments> and removes the placeholder.
  • It stays created after that, even if the signal turns false again.
  • Setting the signal in the tap handler is safe. Angular creates the deferred view after the handler returns.

Triggers

TriggerWorks on LynxNotes
when conditionYesThe most useful trigger on Lynx
on immediateYesAs soon as the template has rendered
on idleYesWhen the app has a moment to spare. The default if you give no trigger
on timer(500ms)YesAfter a delay
on viewportNoLynx has no IntersectionObserver. See below
on interactionNoListens for click and keydown, which Lynx never fires
on hoverNoListens for mouse and focus events, which Lynx never fires
prefetch …Not usefulThe code is already in the bundle, so there's nothing to fetch early

Load when an element scrolls into view

on viewport doesn't work, but the lynxExposure directive gives you the same signal. Put it on a wrapper and use when:

src/app/post.ts
import { Component } from '@angular/core';
import { LYNX_ELEMENTS, LynxExposure } from '@blotch/angular-lynx';
import { Comments } from './comments';

@Component({
  selector: 'app-post',
  imports: [LYNX_ELEMENTS, LynxExposure, Comments],
  template: `
    <view lynxExposure #area="lynxExposure" style="min-height: 200px">
      @defer (when area.visible()) {
        <app-comments />
      } @placeholder {
        <text>Comments</text>
      }
    </view>
  `,
})
export class Post {}
  • area.visible() turns true when the wrapper first appears on screen, for example when the user scrolls to it.
  • Give the wrapper a size. An element with no width or height never counts as visible.

The other blocks

  • @placeholder shows until the trigger fires. @placeholder (minimum 500ms) keeps it up for at least that long, to avoid a flash.
  • @loading shows while Angular loads the deferred code. Nothing downloads on Lynx, so this takes almost no time and @loading shows only briefly, if at all. Add minimum, as in @loading (minimum 500ms), if you want it visible. Otherwise leave it out.
  • @error shows if loading fails. With the code already in the bundle, that's rare.