Free tools Windows power users keep installed
One-click scans. No signup required.
For Angular library authors, the main way to optimize injection tokens for bundle size is to stop the library from holding a runtime reference to an optional component. Replace that reference with a small abstract class that acts as a token, have the concrete component extend it, and register the component under that token with useExisting. When an application never uses the optional component, its implementation code can become eligible for tree-shaking. Angular’s official guide describes this mechanism; it does not promise a specific size saving, so treat the gain as something to measure in your own build.
Why a token can keep a component in consumer bundles
TypeScript removes type-only references when it compiles to JavaScript. A reference that must exist at runtime, such as passing a component class to a content query or to inject(), survives compilation. Bundlers can only drop code that nothing references, so a library that queries for an optional header component by its class keeps that class, its template, and its styles in the output even when no consumer ever renders the header. The consuming application cannot fix this from the outside, because the reference lives inside the library it imports. The fix belongs in the library’s design. Angular’s guide on optimizing client application size with lightweight injection tokens describes this exact problem.
As an Amazon Associate I earn from qualifying purchases.
The lightweight-token pattern
The pattern splits the library’s API into two parts: a small abstract declaration that the parent uses, and a concrete implementation that the application may never load. Build it in four steps.
-
Declare an abstract class as the token
Create an abstract class containing only the members the parent needs. Keep it small, because this declaration is what stays in the bundle.
#1 Best Overall
export abstract class CardHeader { abstract title: string; } -
Have the optional component extend the token
The concrete component implements the abstraction by extending it. Its template, styles, and logic are the parts that can be removed when unused.
@Component({ selector: 'lib-card-header', template: `<h2>{{ title }}</h2>`, providers: [{ provide: CardHeader, useExisting: LibCardHeaderComponent }], }) export class LibCardHeaderComponent extends CardHeader { title = 'Untitled'; } -
Register the component under the abstract token
The
useExistingprovider tells Angular that a request forCardHeadershould resolve to the component instance that is already created. The component is therefore reachable through the abstract class, and no separate instance is made. -
Have the parent query or inject the abstract token
The parent refers only to
CardHeader, never to the concrete class.Recommended: Update Every Outdated Driver on Your PC in One Scan - Free →Recommended: Fix Windows Errors and Clear Junk Files in Minutes - Free Scan →Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.@Component({ selector: 'lib-card', template: `<ng-content />`, }) export class LibCardComponent { @ContentChild(CardHeader) header?: CardHeader; }If the application places a
lib-card-headerinside a card, the query finds it. If it does not, the query returns nothing, and the concrete component never needs to be part of the bundle.
Two conditions decide whether the saving actually happens. The concrete component must not be referenced at runtime anywhere else in the library, for example in an exported array of default components or in a module-level side effect. Any such reference keeps it in the output. Also, the library must not import the concrete component from a file that is itself always loaded.
Use InjectionToken for values without a runtime class
Interfaces, object shapes, and primitive configuration have no runtime representation, so they cannot serve as a token. For these, define an InjectionToken with a type parameter. The generic gives the injected value its type, and the token itself is the runtime identifier.
Rank #4
export interface CardOptions {
compact: boolean;
}
export const CARD_OPTIONS = new InjectionToken<CardOptions>('CARD_OPTIONS', {
providedIn: 'root',
factory: () => ({ compact: false }),
});
Consumers then call private options = inject(CARD_OPTIONS); in a field initializer. A factory-backed token like this can be provided in the root injector, and its factory can call inject() because the factory runs in an injection context. The defining dependency providers guide covers the provider options in more detail.
Token identity is object identity
Angular matches a token by the exact object reference, not by its description string. Two InjectionToken instances with the same description are different tokens. A common failure looks like this: a provider file creates new InjectionToken('CARD_OPTIONS'), and a component file creates a second token with the same text. The component’s injection fails with a NullInjectorError, even though the names match.
The fix is structural. Declare each token once as an exported const in a single file, import that same instance wherever it is provided or injected, and avoid creating tokens inside functions or components. If you re-export tokens through a barrel file, the barrel must re-export the original instance rather than a copy.
Choose provider scope separately from the token
The token pattern decides what the library references. Where the provider is registered decides how many instances exist and how long they live. Angular resolves a dependency by walking up the injector hierarchy, from the element injectors of the component tree to the environment injector. The hierarchical injectors guide explains that walk.
| Registration | Instances and lifetime | Typical fit | Tree-shaking note |
|---|---|---|---|
providedIn: 'root' on the service or token |
One instance shared across the application | Globally shared, stateless or app-wide services and configuration | Root provisioning can support tree-shaking when the service or token is unused |
Component providers array |
One instance per component, visible to its child tree | Isolated state, or overriding a parent’s implementation for a subtree | Not stated in the cited guide; the provider exists only where its component is compiled in |
| Route-level or environment providers | One instance per route or environment injector | Route-scoped features or per-feature configuration | Not stated in the cited guide |
Use root provisioning when the service should be shared and tree-shakable. Use a narrower registration when each subtree needs its own instance or a different implementation. A narrower provider does not make a library smaller by itself, so choose scope for correctness first and bundle size second.
The Tool Desk
Outbyte Driver Updater FREEScan for outdated or missing drivers - takes under a minuteDriver Scan →Outbyte PC Repair FREERepair Windows errors before they cause bigger problemsFix Now →Where inject() is allowed
The inject() function works only in an injection context. That includes the constructor of a DI-managed class, field initializers, and factory functions for providers or InjectionTokens. Calling inject() from an arbitrary method, such as ngOnInit, is outside that context and fails. Move the call into a field initializer, which runs during construction, and keep the pattern consistent across the library.
What the official guidance establishes
Angular’s lightweight-token guidance describes a mechanism for client bundle size and tree-shaking. It is not a runtime speed technique, and it does not change how a component renders once it is loaded. The guidance does not include a bundle-size percentage, a benchmark, or a dated measurement. How much you save depends on the size of the optional component, what else references it, and how your library is built and consumed. To check the result, compare production bundles before and after the change with a bundle analyzer, and confirm that the concrete component’s files no longer appear when the component is not used.
Quick Recap
Review checklist for library code
- Does any runtime code reference the concrete optional class directly, through a query, an
inject()call, an array, or a side effect? - Does the abstract token expose only the members the parent actually uses?
- Is every
InjectionTokendeclared once as an exportedconstand imported everywhere it is used? - Is root provisioning used only for services that should be shared app-wide?
- Have you compared production bundles with the optional component unused?
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.




