DI Internals

Preview — 3 of 10 questions

In Angular's Ivy (R3) DI system, how does the injector resolve a token?

javascript
// Conceptual internals of NodeInjector resolution:
function lookupToken<T>(injector: NodeInjector, token: Type<T>): T {
  // Step 1: Check bloom filter — O(1) negative check
  if (!bloomHasToken(token, injector.bloomBit)) {
    return lookupToken(injector.parent, token);  // definitely not here
  }

  // Step 2: Check the injector's records
  const record = injector.records.get(token);
  if (!record) {
    return lookupToken(injector.parent, token);  // false positive — go up
  }

  // Step 3: Instantiate if needed (lazy)
  if (record.value === UNINITIALIZED) {
    record.value = record.factory();
  }
  return record.value;
}
AIt performs a linear scan of a flat providers array
BIt uses a bloom filter for fast negative lookups, then resolves via a slot-indexed record array on the injector's _records map
CIt traverses the component tree using breadth-first search
DIt uses a WeakMap keyed by the token's class reference

What is the fundamental architectural difference between NodeInjector and EnvironmentInjector?

javascript
// Conceptually, in an LView (component view data):
// Index 0-9: reserved
// Index INJECTOR_INDEX: bloom filter bits for this node's injector
// Index INJECTOR_INDEX+1: bloom filter accumulated (inherited)
// Index INJECTOR_INDEX+2: parent TNode index

// When you call inject(SomeService) in a component constructor,
// Angular reads the current LView from a global stack:
const currentLView = getLView();
const tNode = getCurrentTNode();
// Then resolves via the NodeInjector protocol on this tNode/lView pair

// EnvironmentInjector is a true object:
class R3EnvironmentInjector implements EnvironmentInjector {
  private records = new Map<ProviderToken<unknown>, Record>();
  get<T>(token: ProviderToken<T>, notFoundValue?: T, flags?: InjectFlags): T { /* ... */ }
}
ANodeInjector is for components; EnvironmentInjector is for directives only
BNodeInjector supports signals; EnvironmentInjector supports only class-based services
CNodeInjector can only hold one provider; EnvironmentInjector supports unlimited providers
DNodeInjector is embedded in the view's tNode data structure (no separate object per node); EnvironmentInjector is a standalone object with its own records map

How does providedIn: 'root' achieve tree-shaking for services?

javascript
// What Ivy generates for @Injectable({ providedIn: 'root' })
class ReportService {
  static ɵprov = ɵɵdefineInjectable({
    token: ReportService,
    factory: () => new ReportService(ɵɵinject(HttpClient)),
    providedIn: 'root',
  });
}

// The root ɵinj only creates a dependency edge when ReportService is actually used:
// If no component/service imports ReportService → the factory is dead code → tree-shaken

// With NgModule providers (NOT tree-shakeable):
@NgModule({
  providers: [ReportService],  // ← always bundled, regardless of usage
})
AThe service's ɵprov static property contains the factory; the module's ɵinj references the factory only when the service is injected somewhere, allowing static analysis to prune unused entries
BWebpack/esbuild analyzes all @Injectable decorators and removes unused ones
CAngular compiles a manifest of all services and the CLI removes unused ones during ng build
DTree-shaking works because @Injectable classes extend a base class that is conditionally included

Sign up free to play

Answer all 10 questions (7 more), see explanations for every answer, and track your score.