Optimizing with Lazy Loading
In a server-side rendered application, one of the biggest performance wins is minimizing the amount of JavaScript sent to the browser on initial page load. RizzyUI integrates async-alpine behind the scenes, so you don’t have to manually manage IntersectionObservers or media-query watchers—you just declare when a component should load.
This can shave off tens or even hundreds of kilobytes of initial JavaScript, especially on pages with many offscreen widgets, leading to a much faster initial experience for your users.
The LoadStrategy Parameter
RizzyUI makes lazy loading simple through the LoadStrategy parameter on the <RzAlpineComponent>. This parameter tells async-alpine when to download and initialize your component's .razor.js module.
If you don't provide a LoadStrategy, the component defaults to **eager** loading, meaning its JavaScript is fetched as soon as possible. This is the right choice for critical, "above-the-fold" components like a main navigation menu.
<!-- No LoadStrategy means this loads immediately -->
<RzAlpineComponent For="this" Name="navMenu">
<RzNavbar />
</RzAlpineComponent>Supported Load Strategies
The LoadStrategy parameter accepts several values that correspond to async-alpine's loading strategies.
eager: The default mode. Loads the component as soon as possible.idle: Loads the component when the browser is idle (viarequestIdleCallback).visible: UsesIntersectionObserverto load the component only when it is about to enter the viewport.media(<media-query>): Loads only when a CSS media query matches (e.g.,media('(min-width: 1024px)')).event(<eventName>)orevent: Loads the component in response to a specific event.- Combining strategies: You can combine multiple strategies with a space (e.g.,
visible idle) or with boolean logic (e.g.,visible && media(...)).
Examples of Lazy Loading Strategies
Strategy: visible
This is the most common strategy. The JavaScript module will only be downloaded when the component is about to enter the viewport. It's perfect for any component that isn't visible on initial page load, like a heavy chart, an image gallery, or any third-party library.
<RzAlpineComponent For="this" Name="lazyChart" LoadStrategy="visible" AsChild
Props="@(new { ChartData = new { labels = new[] { "Jan", "Feb", "Mar" }, data = new[] { 12, 19, 3 } } })">
<div class="chart-container w-full bg-background p-4 border rounded-lg shadow-sm">
<canvas></canvas>
</div>
</RzAlpineComponent>// This example assumes a charting library like Chart.js is available globally
// or can be imported. For a real app, you would add Chart.js to your asset bundling.
export default () => ({
chartInstance: null,
async init() {
// This init() function will only run when the component becomes visible
// because of `LoadStrategy="visible"`.
console.log('LazyChart is now visible and its script is running!');
await Rizzy.require("https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.2.0/chart.umd.js")
.then(({ bundleId }) => {
console.log(`Bundle ${bundleId} loaded successfully!`);
const props = Rizzy.props(this.$el);
const canvas = this.$el.querySelector('canvas');
if (canvas && props.chartData) {
this.chartInstance = new Chart(canvas, {
type: 'bar',
data: {
labels: props.chartData.labels,
datasets: [{
label: '# of Votes',
data: props.chartData.data,
borderWidth: 1
}]
}
});
}
})
.catch(err => {
console.error("Failed to load bundle:", err.message);
});
},
destroy() {
// If you created a chart instance, you would destroy it here
if (this.chartInstance) {
this.chartInstance.destroy();
}
}
});Strategy: idle
This strategy loads the component's script when the browser has finished higher-priority tasks. It's great for components like a search modal that aren't immediately needed but should be ready for the first user interaction.
<RzAlpineComponent For="this" Name="searchModal" LoadStrategy="idle">
<!-- This ensures the modal's JS is ready after page load, without blocking first paint. -->
<RzDialog Title="Search" ... />
</RzAlpineComponent>Strategy: event
This strategy defers loading until a specific event is dispatched. There are two ways to use it:
Method 1: Using a Named Event
Provide the event name in parentheses: LoadStrategy="event(my-custom-event)". The component will load when that event is dispatched on the window.
window.dispatchEvent vs. $dispatch
Alpine's $dispatch creates a bubbling DOM event, ideal for communication between Alpine components. However, async-alpine's named event() strategy listens on the global window object. Therefore, you **must** use plain JavaScript's window.dispatchEvent(new Event('...')) to trigger it.
@attribute [RzAlpineCodeBehind]
<!-- The promo banner's JS loads only when the 'loadPromo' event fires -->
<RzAlpineComponent For="this" Name="promoBanner" LoadStrategy="event(loadPromo)">
<RzAlert Title="Special Offer!" x-show="visible" x-transition />
</RzAlpineComponent>
<!-- A simple button to trigger the event -->
<RzButton x-data onclick="window.dispatchEvent(new Event('loadPromo'))">
Show Promo
</RzButton>Method 2: Using the Generic `async-alpine:load` Event
Use LoadStrategy="event" and dispatch the async-alpine:load event with an object containing the ID of your component. This special event *does* work with Alpine's $dispatch.
@attribute [RzAlpineCodeBehind]
<RzAlpineComponent For="this" Name="loadByIdBanner" LoadStrategy="event" Id="my-banner-component">
<RzAlert Variant="ThemeVariant.Success" Title="Loaded by ID!" x-show="visible" x-transition />
</RzAlpineComponent>
<div x-data class="mt-4">
<RzButton x-on:click="$dispatch('async-alpine:load', { id: 'my-banner-component' })">
Load Component by ID
</RzButton>
</div>Advanced Combining Strategies
Async Alpine supports boolean expressions with &&, ||, and parentheses, allowing for complex loading conditions.
<!-- Loads when visible AND (either a 'triggerWidget' event fires OR the screen is wide) -->
<RzAlpineComponent For="this" Name="complexWidget"
LoadStrategy="visible && (event(triggerWidget) || media('(min-width: 1024px)'))">
<div> ... </div>
</RzAlpineComponent>Best Practices for Lazy Loading
- Be Aggressive with `visible`: When in doubt, use
LoadStrategy="visible". If a component is not critical for the initial view, lazy-loading it is almost always the right choice. - Avoid Lazy-Loading Critical UI: Interactive components that are immediately visible and essential (like a main navigation menu) should be loaded eagerly.
- Provide a Good User Experience: For components that might take a moment to become interactive, provide a visual cue like a skeleton loader or a placeholder.
- Be Mindful of Observer Costs: If you have hundreds of small widgets on one page, avoid assigning
visibleto each one. Instead, consider grouping them in a parent container or usingidleto reduce `IntersectionObserver` overhead. - Ensure Events Will Fire: When using the
eventstrategy, make sure the event will actually be dispatched. If the event never fires, the component's JavaScript will never load.
Next Steps
Remember, the init() method only runs once the module loads. With lazy loading, you control both when the script downloads and when the component’s lifecycle begins.
Next, we'll look at the pre-packaged interactivity that RizzyUI provides with its built-in Plugins & Directives.