Hook — 06 : 07 a.m., Puerto Plata (Dominican Republic) ↔ Panamá City code-review
Warm coastal wind rustled my hammock as Daniela, hacking from a rooftop cowork in Panamá, shared her screen: the CMS dashboard gasped under 80 dormant widgets shipped in the main bundle. “Drag-and-drop is smooth on fiber,” she said, “but hotel Wi-Fi turns it into molasses.” I opened DevTools over café-con-leche bandwidth, saw two megabytes of unused JavaScript, and introduced her toViewContainerRef
—Angular’s doorway to runtime component injection. Within half an hour we spawned widgets only when editors dropped them onto the canvas, trimmed the first paint by half a second, and spared every prepaid SIM along our Latin-American flight path.
Why Runtime Loading Still Matters
Single-page apps succeed when they feel quick and flexible. Shipping every potential component up front sabotages Core Web Vitals and burns bandwidth, yet users expect modular dashboards, plug-in editors, and A/B test slots. Angular offers a pair of low-level APIs—ViewContainerRef
and ComponentRef
—that let you instantiate any standalone or declared component at the exact moment you need it. Couple that with lazy imports and you have a recipe for both snappy loads and true runtime composability.
The Mental Model
Dynamic loading is a two-step dance:
- Resolve the component type (compile-time or via
import()
). - Create an instance inside a
ViewContainerRef
, which acts like an outlet you control in code.
Every instance returned is a live ComponentRef
, giving you access to inputs, outputs, and lifecycle hooks until you call destroy()
.
Imagine ViewContainerRef
as an empty hotel room: you can check in any component, manage its stay, and free the room when the guest leaves—keeping housekeeping (change detection) automatic.
Setting Up the Host Directive
First you need a place where runtime components will live. A structural directive keeps templates clean and interacts directly with the host view.
tsCopyEdit@Directive({
selector: '[widgetHost]',
standalone: true,
})
export class WidgetHostDirective {
constructor(public vcr: ViewContainerRef) {}
}
In your host component’s template:
htmlCopyEdit<section class="canvas">
<ng-container widgetHost></ng-container>
</section>
The ng-container
leaves no stray markup, yet grants code behind the scenes full control.
Loading a Component Synchronously
For components already compiled into the bundle—handy in admin-only builds—you inject the directive and call createComponent
.
tsCopyEdit@Component({
selector: 'app-dashboard',
templateUrl: './dashboard.html',
standalone: true,
imports: [WidgetHostDirective],
})
export class DashboardComponent {
@ViewChild(WidgetHostDirective, { static: true })
host!: WidgetHostDirective;
loadClock() {
const ref = this.host.vcr.createComponent(ClockWidgetComponent);
ref.setInput('timezone', 'America/Santo_Domingo');
}
clear() {
this.host.vcr.clear(); // destroys all
}
}
Every call yields a new instance—ideal for multi-widget layouts.
Bringing in Lazy-Loaded Widgets
Synchronous loading still ships code up front. Combine import()
with Angular’s Ivy JIT to keep bytes off the wire until the last second.
tsCopyEditasync loadSalesChart() {
const { SalesChartComponent } = await import(
'./widgets/sales-chart/sales-chart.component'
);
const ref = this.host.vcr.createComponent(SalesChartComponent);
ref.setInput('range', '30d');
}
Webpack splits the sales-chart chunk automatically. On 4 G, the pay-as-you-go traveler downloads it only if they open the analytics tab.
Communicating with Dynamically Created Components
Inputs are easy with setInput
. Outputs use the changeDetectorRef
or manual subscriptions:
tsCopyEditconst ref = this.host.vcr.createComponent(NotificationWidgetComponent);
ref.instance.dismiss.subscribe(() => ref.destroy());
Always remember to unsubscribe or destroy the whole ComponentRef
to avoid memory leaks—especially when your code runs for hours in a kiosk.
Remote-Work Insight: Snapshot Previews over Spotty Wi-Fi
While reviewing Daniela’s PR from a rooftop bar in Cartagena, we noticed widget previews flickered during low bandwidth. We solved it by loading lightweight skeleton components first, then swapping them out when the heavy chart finished streaming.
tsCopyEditconst skeleton = this.host.vcr.createComponent(SkeletonWidgetComponent);
import('./widgets/heavy-chart/heavy-chart.component').then(({ HeavyChartComponent }) => {
this.host.vcr.clear(); // remove skeleton
this.host.vcr.createComponent(HeavyChartComponent);
});
The user sees a shimmering card immediately, even on 1 Mbps hotel Wi-Fi.
Performance and Accessibility Checkpoints
- Lighthouse → JS execution time should shrink as chunks move to late imports.
- Largest Contentful Paint improves because above-the-fold code renders without waiting for dashboard widgets.
- Verify that newly inserted components announce themselves correctly to screen readers. Use
aria-live="polite"
on host elements or emit focus events on creation.
Common Pitfalls and Their Fixes
Symptom | Underlying Cause | Quick Repair |
---|---|---|
NG04157: Component is not standalone | Forgot to declare component or mark standalone: true in Ivy world | Export through a shared NgModule or add standalone: true |
Styles bleed between dynamic widgets | Shadow DOM or encapsulation mismatch | Switch encapsulation to ViewEncapsulation.Emulated or provide unique CSS vars |
Memory climbs after tab hopping | Never destroyed old refs | Call ref.destroy() or vcr.clear() on hidden panels |
Change detection doesn’t trigger | Mutation done outside Angular zone | Wrap imperative updates in NgZone.run() or use signals |
Call-Out Commands
Command | Purpose |
---|---|
ng g c widgets/sales-chart --standalone | Generate lazy-friendly widget |
ng build --stats-json | Inspect new chunk sizes |
ng profiler timeChanges | Measure change-detection impact after refactor |
Diagram Blueprint
Picture a flow where the host component presses a “+ Widget” button → import()
fetches chart-chunk.js
→ ViewContainerRef
injects SalesChartComponent
→ Component emits data → Host manages lifecycle. A side-by-side before/after shows bundle size shrinkage.
Wrapping Up
Dynamic component loading marries flexibility with performance. By anchoring widgets in a ViewContainerRef
, deferring heavy imports with import()
, and minding lifecycle hygiene, you can deliver modular experiences that still respect mobile caps across Latin America. Next time a teammate asks to preview a feature without inflating the bundle, hand them a host directive, share this guide, and sip your espresso as their FCP trips Google’s “good” line.
–– Drop your runtime-rendering wins or woes in the comments; I’ll reply between airport layovers and late-night arepa sessions.