Preloading

Lazy routes keep the initial bundle small, but the chunk only downloads after the click, so the user has to wait. Preloading closes that gap: fetch the chunk while the user is still deciding, on hover or as the link scrolls into view, so the navigation feels instant.

@mmstack/router-corenpm

Angular ships two blunt strategies: preload nothing, or preload everything after load. Neither is quite right. Preloading everything undoes the point of code-splitting on a large app; preloading nothing means every lazy route pays a download on first visit. What you usually want is in between: preload the route the user is about to click, because their pointer is already on the link. That is what these three pieces give you.

#The setup

PreloadStrategy is the engine. It is a PreloadingStrategy that, does nothing until something asks it to preload a specific route. On its own it is inert. It listens for requests that mmLink and injectTriggerPreload issue, so you install it once and drive it from the other two.

import { PreloadStrategy } from '@mmstack/router-core';
import { provideRouter, withPreloading } from '@angular/router';

export const appConfig: ApplicationConfig = {
  providers: [provideRouter(routes, withPreloading(PreloadStrategy))],
};

It is careful about when it actually fetches. It path-matches the requested URL against your route config (route params, matrix params, and wildcards all resolve), and it backs off on slow connections (effectiveType: '2g') or when the browser reports saveData, evaluated at request time so a connection that improves later is not locked out. A route can opt out entirely with data: { preload: false }, and data: { preloadDelay: 150 } debounces hover intent so a pointer skating across a menu does not fetch every chunk it passes. Each path is deduplicated, so it downloads at most once.

Link, used in templates as mmLink, is the request source you will use most. It wraps Angular's RouterLink and adds the preloading behavior, proxying all the standard inputs (queryParams, fragment, state, relativeTo) through unchanged. In most codebases you can rename routerLink to mmLink and be done.

import { Component } from '@angular/core';
import { Link } from '@mmstack/router-core';

@Component({
  selector: 'app-navigation',
  imports: [Link],
  template: `
    <nav>
      <!-- preload on hover (default) -->
      <a [mmLink]="['/features']">Features</a>
      <!-- preload when scrolled into view -->
      <a [mmLink]="['/pricing']" preloadOn="visible">Pricing</a>
      <!-- no preload -->
      <a [mmLink]="['/contact']" [preloadOn]="null">Contact</a>
    </nav>
  `,
})
export class NavigationComponent {}

The two inputs worth learning are preloadOn and preload. Think of them as WHEN and WHAT.

  • preloadOn is the WHEN: 'hover' (the default), 'visible' for links that should warm as they scroll into view, or null to switch preloading off for that link.
  • preload is the WHAT: 'all' (the default) warms both the lazy chunk and the route's data, while 'code' warms only the chunk, for links to routes whose data is expensive or should not fire speculatively.
  • preloading is an output that fires the moment a route is queued, before the JavaScript actually loads, handy for a subtle loading affordance.
  • useMouseDown starts navigation on mousedown instead of click, which shaves the 50 to 100ms a user spends holding the button down. The press's own click is swallowed so navigation still runs exactly once, and keyboard activation is unaffected.
  • beforeNavigate runs a hook just before an SPA navigation this link kicks off. Modified clicks, middle clicks, and target="_blank" are left to the browser and skip it.

If you want the same behavior everywhere, set it once with provideMMLinkDefaultConfig({ ... }) rather than repeating preloadOn and friends on every link.

#Recipe: warm a heavy route on hover

Say your dashboard sits behind a fat chunk, charts, a grid library, the works, and first-time visitors feel the download. You do not want it in the initial bundle, but you also do not want the click to stall. The fix is one input on the link into it.

<!-- the chunk downloads on hover; the click feels instant -->
<a [mmLink]="['/dashboard']" preloadOn="hover">Dashboard</a>

<!-- for a hero CTA below the fold, warm it as it scrolls into view -->
<a [mmLink]="['/reports']" preloadOn="visible">View reports</a>

As soon as the pointer rests on the link, the chunk starts downloading. By the time the click lands, it is usually in cache and the route mounts immediately. If the connection is slow the strategy quietly skips the warm, so you never make a struggling connection worse. Combine it with the route-data prefetch and the same hover warms the route's data too, not just its code.

#injectTriggerPreload

Not every preload hangs off a link. You might want to warm a route when a command palette opens, when a keyboard shortcut is armed, or from a signal effect watching what the user is hovering elsewhere. injectTriggerPreload() hands you a function that runs the exact same pipeline as mmLink, so you get all the same connection checks and deduplication without a directive.

import { Component, effect, signal } from '@angular/core';
import { injectTriggerPreload } from '@mmstack/router-core';

@Component({ /* ... */ })
export class CommandPaletteComponent {
  private readonly triggerPreload = injectTriggerPreload();
  protected readonly highlighted = signal<string | null>(null);

  constructor() {
    effect(() => {
      const target = this.highlighted();
      if (target) this.triggerPreload(target); // warm the highlighted result
    });
  }
}

It needs the same PreloadStrategy installed. Use it as the escape hatch; reach for mmLink whenever a link is the natural home for the intent.