skip to content

You are migrating a large AngularJS app to Angular one screen at a time with UpgradeModule; how do you bootstrap and sequence it, and what pitfalls do you expect?

level: seniorimportance: should knowfreq 35%

answer

  1. Angular boots first, AngularJS owns root
  2. services before screens
  3. leaves before containers
  4. digest coupling costs cycles
  5. maintenance-mode package

basics

~20 s

Bootstrap Angular first and let UpgradeModule bootstrap AngularJS in ngDoBootstrap, so AngularJS owns the root. Move shared services first, then screens from leaf components upward, bridging each boundary. Expect extra change-detection cycles, unsupported directive features, and a maintenance-mode package.

solid answer

~40 s

The app boots as an Angular `NgModule` importing `UpgradeModule`; in `ngDoBootstrap()` it calls `upgrade.bootstrap(document.body, ['legacyApp'])`. **Angular starts first, AngularJS second, and AngularJS owns the root component**, including its router at first. Then I migrate bottom-up: shared **services** first (rewrite in Angular, `downgradeInjectable` them back), then per screen the **leaf components**, downgraded into the AngularJS templates, then the screen container, and finally hand routes to Angular's router. Pitfalls: `UpgradeModule` runs a `$digest` whenever Angular's change detection settles, so there are more cycles than needed (and `downgradeModule()` trades that coupling for manual notification); upgraded AngularJS components cannot use `replace`, `terminal` or uncached `templateUrl`; two frameworks ship in the bundle; Trusted Types needs the `angular#unsafe-upgrade` policy; and `@angular/upgrade` only gets security fixes.

code

ts · 17 lines
ts
import { DoBootstrap, NgModule, inject } from '@angular/core';
import { BrowserModule, platformBrowser } from '@angular/platform-browser';
import { UpgradeModule } from '@angular/upgrade/static';

@NgModule({
  imports: [BrowserModule, UpgradeModule],
})
export class AppModule implements DoBootstrap {
  private readonly upgrade = inject(UpgradeModule);

  ngDoBootstrap(): void {
    // Angular is up; AngularJS starts second and owns the root.
    this.upgrade.bootstrap(document.body, ['legacyApp']);
  }
}

platformBrowser().bootstrapModule(AppModule);

go deeper

for a junior

Know that a hybrid runs both frameworks, that UpgradeModule bootstraps AngularJS from Angular, and that migration goes piece by piece.

for a middle

Explain the bootstrap order, root ownership, and why services and leaf components move before containers and routes.

for a senior

Lay out the plan with its pitfalls: digest coupling, unsupported directive features, template cache, Trusted Types, and removing bridges as you go.

for a principal

Set an end date and a metric for the hybrid period, and decide when UpgradeModule's coupling cost justifies switching to downgradeModule.

A hybrid migration keeps the AngularJS app running in production while code moves to Angular piece by piece. It works well when it is sequenced deliberately and badly when both frameworks drift around each other for years. ## Bootstrapping the hybrid With `UpgradeModule`, the order is fixed by the package's own mental model: **Angular is bootstrapped first, AngularJS second, and AngularJS owns the root component**. 1. Create an Angular `NgModule` that imports `BrowserModule` and `UpgradeModule` and implements `DoBootstrap`. 2. Bootstrap that module (for example with `platformBrowser().bootstrapModule(AppModule)`). 3. In `ngDoBootstrap()`, call `upgrade.bootstrap(document.body, ['legacyApp'])` to start the AngularJS app on the page, instead of `ng-app`. Importing `UpgradeModule` also provides the AngularJS core services to Angular's root injector, and the app runs in the Angular zone, so AngularJS code no longer needs `$apply()` for changes made in Angular. `UpgradeModule` itself is an `NgModule` and its providers set up zone-based change detection; keep zone.js in a hybrid rather than assuming the v21 zoneless default applies. ## Sequencing the migration A bottom-up order keeps every step small: 1. **Prepare the AngularJS code.** Move to `.component()`-style components and a build that can compile TypeScript, so both halves live in one toolchain. 2. **Shared services first.** Rewrite a service in Angular and expose it back with `downgradeInjectable`, or keep it in AngularJS and give Angular a provider that reads it from `$injector`. 3. **Leaf components next.** Rewrite presentational components and use them from AngularJS templates via `downgradeComponent`. 4. **Then the screen.** When a screen's children are Angular, rewrite its container. 5. **Routing last.** Hand routes to Angular's router once whole screens are Angular, and eventually remove the AngularJS root. 6. **Delete the bridges.** Each `UpgradeComponent` wrapper and downgrade registration is temporary; remove them as soon as both sides of a boundary are Angular. ## Pitfalls to plan for | Pitfall | Why it happens | Mitigation | | :-- | :-- | :-- | | Extra change-detection cycles | `UpgradeModule` runs a `$digest` whenever Angular's change detection settles, and downgraded components check on every digest by default | Set `propagateDigest: false` where inputs are enough; consider `downgradeModule()` | | Upgraded directive refuses to work | `compile` without `link`, `replace` and `terminal` are unsupported | Rewrite the directive as a plain AngularJS component first | | "Loading directive templates asynchronously is not supported" | An upgraded component's `templateUrl` is not in `$templateCache` | Inline templates or pre-populate the cache at build time | | Service not available | With `downgradeModule()`, downgraded services exist only after their Angular module is bootstrapped | Use them only inside the Angular part that guarantees it | | Attribute syntax confusion | The framework owning the template parses attributes: kebab-case in AngularJS templates | Code-review checklist for bridge boundaries | | Trusted Types violations | The upgrade package needs its own policy | Allow the `angular#unsafe-upgrade` policy in the CSP | | Bundle size | Two frameworks ship together | Keep the hybrid period short; lazy-load Angular parts with `downgradeModule()` if needed | ## `UpgradeModule` or `downgradeModule()` `downgradeModule()` bootstraps the Angular part **lazily**, on first use, and does not tie the change-detection systems together as tightly: it does not bootstrap AngularJS in the Angular zone and does not run a `$digest` automatically when Angular detects changes. It is faster in change-detection-heavy apps but leaves notifying each framework to you. The two cannot be combined in one hybrid. ## The support context - **AngularJS** has been out of support since **January 2022**. - **`@angular/upgrade`** is in **maintenance mode**: security fixes only, no new features. Both argue for a hybrid period with an end date, tracked like a project, rather than an open-ended coexistence. A useful metric is the number of remaining bridges, which should only go down.

  • Why is a hybrid app built on UpgradeModule slower in change detection than either framework alone?
    UpgradeModule triggers an AngularJS $digest whenever Angular's change detection settles, and downgraded components by default run detectChanges on every digest. Each change can therefore cause work in both frameworks, which is more change detection than strictly needed.
  • Why migrate services before components in a hybrid app?
    Components on both sides depend on services. Once a service lives in Angular and is downgraded back, new Angular components can inject it natively while AngularJS code keeps working, so later component moves do not need service bridges in both directions.
  • Can you use UpgradeModule and downgradeModule together to get the best of both?
    No. The documentation states they cannot be used in the same hybrid application; you pick one. UpgradeModule couples change detection tightly and bootstraps eagerly, while downgradeModule bootstraps Angular lazily and leaves cross-framework notification to you.

saying these in an interview costs you the question

  • AngularJS is bootstrapped first and then boots Angular inside it.
  • A hybrid app still needs $apply after every change made in Angular code.
  • UpgradeModule and downgradeModule can be combined in one hybrid.
  • Migrate the router first so the rest of the app follows.
  • @angular/upgrade is actively developed, so the hybrid can last indefinitely.