Picture this: you slash your Angular app’s initial bundle by 40% in days, loading those heavy charts only when users scroll down — no routing headaches required. Angular’s @defer (dropped in v17) is your new best friend for smart, template-level lazy loading of standalone components, directives, and pipes—perfectly teaming up with router lazy loading to crush Core Web Vitals like LCP.
Whether you’re a beginner dipping into @defer magic, an expert tweaking performance, or a stakeholder chasing that ROI from snappier apps, this guide's got you. We'll roll through hands-on steps, code snippets, killer triggers (viewport scrolls, button clicks), placeholder tricks to kill layout jank, and error-handling smarts—skipping compiler deep dives.
Ready to prefetch on idle, render on hover, and watch your app fly? Let’s dive in and supercharge your Angular game!
@defer Fundamentals and Setup
Angular’s @defer block is a game-changer for performance, letting you split heavy template sections—like charts or dashboards—into separate JavaScript bundles that load only when triggered by events such as viewport entry, user interaction, or browser idle time. This slashes your initial payload, speeding up Largest Contentful Paint (LCP) and overall app startup, especially in large single-page apps where not every user needs every feature right away.
Requirements for Success
You’ll need Angular 17 or later, as @defer leverages the new control flow syntax and stable deferrable views from Angular 18 onward. Components, directives, and pipes inside the @defer block must be standalone and not referenced elsewhere in the same file—think no sneaky ViewChild queries or imports outside the block, or they'll eagerly load anyway. Transitive dependencies can mix standalone or NgModule-based, but keep placeholders lightweight since their deps load upfront.
Basic Syntax in Action
Kick off with something simple: @defer (on viewport) { <heavy-chart /> } @placeholder { <skeleton-loader /> }. Here, the chart bundle loads when the placeholder hits the viewport, swapping the skeleton for the real deal—triggers like idle (default), interaction, hover, timer(2s), or custom when showLargeChart give you precise control. Add @loading (minimum 1s) for spinners during fetch, or @error { Retry? } for fails, all while prefetching separately via prefetch on idle.
Verify Your Build Wins
Run ng build and scan the CLI output for new chunks like defer-heavy-chart.js—that's your proof the split happened. Fire up the dev server, hit the Network tab in Chrome DevTools, and watch lazy loads kick in on triggers; no extra bundle until viewport or click, confirming a leaner main payload. Pro tip: Test with TestBed.deferBlockBehavior = DeferBlockBehavior.Manual for unit tests simulating states.
Real-World Starter: Dashboard Magic
Wrap a data-heavy dashboard widget in @defer (on viewport; prefetch on idle) { <analytics-widget /> } @placeholder (minimum 500ms) { <chart-skeleton /> } and watch your bundle shrink by 30% or more on complex pages—perfect for below-the-fold metrics that users scroll to. This combo preloads smartly without bloating initial load, delivering buttery-smooth experiences for managers eyeing KPIs without waiting forever.
Core Triggers and Sub-Blocks
Angular’s @defer blocks shine with built-in triggers like idle (default, fires on browser idle via requestIdleCallback), viewport (loads on scroll into view using Intersection Observer), interaction (triggers on click or keydown), hover (on mouseover or focusin), immediate (right after non-deferred content renders), and timer (after specified ms or s). These use the on keyword and support multiple via semicolons as OR conditions, decoupling prefetch from display for smarter loading.
Prefetching adds punch — load code early with prefetch on idle (default) or others, separated by semicolon from main trigger, so resources wait ready without instant render. Picture an e-commerce dashboard: prefetch heavy charts on viewport entry, but show only on "View Analytics" button click—users get instant charts without upfront bloat.
Sub-Blocks for Seamless UX
@placeholder shows initial eagerly-loaded content (keep it lightweight—no heavy deps), with minimum 500ms to dodge flicker on fast networks; it's required for some triggers like viewport needing a single root. @loading kicks in post-trigger (replacing placeholder), eagerly loaded too, with after 100ms; minimum 1s params to time it right—wait before show, ensure min display.
@error handles fetch fails gracefully, also eagerly loaded for quick fallback display. Wrap in aria-live="polite" for screen reader announcements during swaps.
Custom Triggers with when
For bespoke logic, when takes signals or expressions like @defer (when isVisible())—one-time truthy check, no revert. Combine with prefetch: @defer (when userClicked(); prefetch when dataReady) for total control. In dashboards, signal on data fetch complete triggers charts post-interaction.
Advanced Patterns and Optimization
Mastering @defer goes beyond basics—it's about smart strategies that squeeze every millisecond from your app's performance. Think of it like a traffic controller for your JavaScript bundles: directing heavy loads only when needed, avoiding pile-ups that slow everything down. Let's dive into pro techniques that deliver real-world wins.
Nested @defer and Cascading Avoidance
Nested @defer blocks let you layer lazy loading for complex UIs, but stack them wrong and you trigger cascading requests—multiple bundles firing simultaneously, tanking load times. The fix? Stagger triggers: use viewport for outer blocks and interaction for inners. Combine with @if for post-load toggling, like hiding loaded content until a condition flips true. This keeps your LCP snappy while giving granular control.
@defer (on viewport) {
@defer (on interaction) {
<heavy-chart />
} @placeholder {
<div>Click to expand</div>
}
} @placeholder {
<section>Scroll to see more</section>
}
@if (isExpanded) {
<summary-panel />
}
SSR/SSG and Hydration Magic
In SSR or SSG, servers render @placeholder content (or nothing), skipping triggers since there's no viewport or idle state. Client-side, hydration kicks in, firing your configured triggers to make it interactive. Enable Incremental Hydration with hydrate on triggers for server-rendered main content—perfect for SEO without bloating client bundles. Pro tip: pair prefetch on idle to preload before hydration.
Bundle Analysis and HMR Pitfalls
Quantify @defer wins with source-map-explorer: install via npm i -D source-map-explorer, build with ng build --source-map, then run npx source-map-explorer dist/**/main.js for interactive treemaps showing chunk breakdowns. You'll see main bundles shrink as heavy standalone components split off—more accurate for Angular than alternatives. Watch for HMR gotchas—development servers eager-load all @defer chunks, ignoring triggers; disable with ng serve --no-hmr for accurate testing. Pitfall avoided, gains measured.
Accessibility with ARIA Live Regions
Screen readers miss @defer swaps—users hear placeholders but not loaded content. Wrap blocks in aria-live="polite" aria-atomic="true" divs to announce transitions automatically. This keeps deferred UIs inclusive without extra JS, ensuring managers love the perf and the ethics.
<div aria-live="polite" aria-atomic="true">
@defer (on hover) { <user-profile /> }
@placeholder { Loading profile... }
</div>
Real-World Case Study
One team slashed a 2MB Angular app into granular @defer splits: charts on viewport, analytics on interaction. Result? Production LCP under 2s, CWV scores soaring. They measured with bundle analyzer, staggered nests, and added ARIA—proving @defer scales from dashboards to enterprise dashboards.
Key Takeaways
@defer blocks deliver template-level code splitting through smart triggers like viewport, interaction, and idle, automatically creating separate JS chunks for standalone components, directives, and pipes — slashing initial bundle sizes and boosting Core Web Vitals such as LCP by up to 20–50% in real-world apps when paired with standalones.
This isn’t just theory: developers report main bundles dropping significantly (e.g., heavy chart components or third-party libs deferred until needed), leading to faster TTI and smoother user experiences without complex routing setups.
Unlike simple @if hiding — which still downloads everything — @defer truly lazy-loads, prefetching on demand while handling placeholders, loading spinners, and errors gracefully.
Get Hands-On Now
Fork this interactive @defer demo repo and implement a viewport-triggered heavy component today: github.com/ducin/angular-defer. Run ng build --source-map, analyze your bundle with source-map-explorer (npx source-map-explorer dist/**/main.js), and share those before/after stats in the comments—expect noticeable drops in main chunk size!
Dive Deeper
Explore the official Angular docs for full trigger syntax and SSR nuances. Check Angular University’s complete guide for prefetch patterns and custom when conditions. For advanced tweaks, pair with signals to build reactive triggers that respond to user data flows.
P.S. If you’re building a business, I put together a collection of templates and tools that help you launch faster. Check them out at ScaleSail.io. Might be worth a look.
Thanks for Reading 🙌
I hope these tips help you ship better, faster, and more maintainable frontend projects.
🛠 Landing Page Templates & Tools
Ready-to-use landing pages and automation starters I built for your business.
👉 Grab them here
💬 Let's Connect on LinkedIn
I share actionable insights on Angular & modern frontend development - plus behind‑the‑scenes tips from real‑world projects.
👉 Join my network on LinkedIn
📣 Follow Me on X
Stay updated with quick frontend tips, Angular insights, and real-time updates - plus join conversations with other developers.
👉 Follow me
Your support helps me create more practical guides and tools tailored for the frontend and startup community.
Let's keep building together 🚀

Top comments (0)