Why Ivy Was Built
At the NgConf 2018 keynote, the core Angular team highlighted two principles that shaped Ivy's design: locality and tree shaking. These are areas where the existing implementation falls short. Although not explicitly called out, the Angular Elements initiative also played a role in steering the architecture.
Locality
Locality means the compiler may rely only on what the component decorator and its associated class expose. Today's compiler, however, must perform a global analysis of the whole application, which leads to several complications.
For starters, merging build outputs from separate projects becomes tricky. Integrating an AOT-compiled library into a JIT-compiled application demands deep compiler expertise. By restricting the compiler to component-level metadata, Ivy allows AOT code to be published to NPM as a self-contained library, making the mix of JIT and AOT packages much simpler.
The old pipeline also generates extra files like metadata.json and component.factory.js, which require careful handling. Ivy does away with these artifacts entirely.
Finally, with the new design, generating dynamic components on the fly should be more straightforward than the current method.
Tree shaking
Tree shaking, the second pillar, means dead code gets stripped from the final bundle. We're used to shaking our own application code, but the Angular team took the idea further: why not shake the framework itself? If your app never uses view queries, the framework code that propagates those queries shouldn't reach the browser. No content projection needs? Then that code is dropped as well. This is precisely where the notable bundle size savings originate — you no longer ship the whole framework, just the functionality you actually consume. Smaller bundles naturally lead to faster startups.
This outcome is achievable because of the instruction-based approach now used in compiled components. The same technique also simplifies debugging and yields other side benefits.
How the Runtime Works
The runtime is built around Incremental DOM — a strategy for expressing DOM mutations as a series of instructions. Since updating the DOM is the heart of Angular's change detection, this model fits naturally. Those interested in deeper detail can read about the rationale and how it stacks up against React's Virtual DOM in a dedicated write-up. While Incremental DOM exists as a separate library, Ivy does not consume it; instead, Angular implements its own version.
During the keynote, the team described the logic for instantiating components, creating DOM elements, and executing change detection as an Angular Interpreter working as an atomic unit. The compiler's job ends at generating metadata for the component and its template; the interpreter consumes that data to do the heavy lifting.
The current execution model looks like this:

In the diagram, the Template Data corresponds to the view definition the compiler emits. It acts as a blueprint for constructing the component view. In today's engine, however, that definition is far from human-friendly. For a template like <span>My name is {{name}}</span>, the generator produces something like the following:
viewDef(0,[
elementDef(0,null,null,1,'span',...),
textDef(null,['My name is ',...])
]
That definition encodes two view nodes: one span element and a text node containing the static string My name is. At runtime, this metadata is consumed to physically create two DOM nodes and to support the text binding update during change detection. Now let's look at the same component compiled under Ivy:
// create mode
if (rf & RenderFlags.Create) {
elementStart(0, 'span');
text(1);
elementEnd();
}
// update mode
if (rf & RenderFlags.Update) {
textBinding(1, interpolation1('My name is', ctx.name));
}
Notice how the elementStart instruction carries no metadata. Instead, it directly creates a DOM node. Behind the scenes, Ivy still leans on the Renderer abstraction — a concept explored in detail during the NgVikings talk. So familiar touchstones remain.
The same pattern applies to the text instruction, which generates a text node. There's also the textBinding instruction, which performs one of the fundamental change detection operations — updating a binding on a text element. This piece-by-piece DOM update is what makes the approach "incremental."
The new execution flow in Ivy is shown here:

The responsibilities that used to live in the monolithic interpreter — component instantiation, DOM node creation, and change detection — are now distributed across individual instructions.
Some instructions are dedicated to creating standard DOM nodes. They run during the creation phase of change detection. This is a notable shift: DOM creation now happens as part of change detection itself, which differs from how the older engine operated. Interestingly, several creation-focused instructions also prepare the context for later update cycles. The elementEnd instruction, for instance, queues lifecycle hooks and registers entries with query lists. Another class of instructions handles Angular-specific logical view nodes, including directives, view containers, and queries.
A second category of instructions executes during the update phase of change detection. Notable examples are textBinding, which refreshes a text node, and elementProperty, which sets a property on an element.
Since these are just importable functions, an application that never uses property bindings like
<span [textContent]="value">won't import the corresponding instruction. As a result, that runtime code remains excluded from the bundle — achieving tree shaking.
The Compilation Process
Just like the previous system, Ivy's compiler takes the metadata from a component decorator and outputs a component definition. Here's a minimal definition generated for the template <my-app [name]=”name”></my-app>:
const componentDefinition = {
type: MyApp,
selectors: [['my-app']],
template: (rf: RenderFlags, ctx: MyApp) => {
if (rf & RenderFlags.Create) {
elementStart(0, 'span');
elementEnd();
}
if (rf & RenderFlags.Update) {
elementProperty(0, 'name', bind(ctx.name));
}
},
factory: () => new MyApp()
}
The definition records the component type, the selectors it targets, and a factory function for instantiating the class. Many more properties exist within a definition, all discoverable in the source code. The most relevant one for this discussion is the template property — a function invoked on every change detection cycle. Inside it sit the creational and update instructions introduced earlier. Angular executes this template function in either create or update mode. So, for the definition shown above, Angular will construct a span element during the create pass, and refresh its bindings during the update pass.
One of Ivy's great appeals is how approachable change detection debugging becomes. Drop a breakpoint inside the template function, and you're instantly inspecting the change detection run for that component.
The component definition storage upgrade
With the compiler that exists today, template data for a component definition lives apart from the component class itself, in dedicated factory files commonly seen as *.component.factory.ts when working with AOT. The Ivy compiler changes this arrangement entirely. It attaches the component definition straight onto the component class, using static fields, and no longer emits extra compilation artifacts. As of this writing, that definition resides in the static field ngComponentDef:
export class MyApp {
name: string;
static ngComponentDef = defineComponent({
type: MyApp,
selectors: [['my-app']],
template: function() {...},
factory: () => new MyApp()
});
}
Details on this design surface in a document outlining the compiler's new architecture. Having such documentation available makes the work of decoding the internals far more manageable. Credit goes to Chuck Jazdzewski for compiling it. The document contains excerpts that clarify the intended approach.
…the Ivy model is that Angular decorators (
@Injectable, etc) are compiled to static properties on the classes (ngInjectableDef)… Each of the class decorators… creates a corresponding static member on the class that describes to the runtime how to use the class. For example, the@Componentdecorator creates anngComponentDefstatic member,@Directivecreate anngDirectiveDef, etc…
Consequently, in addition to the static field ngComponentDef found on a component class, we can anticipate other static members such as ngInjectableDef and ngPipeDef. These will carry the definitions for the providers and pipes that a component relies upon.
Each of the class decorators can be thought of as class transformers that take the declared class and transform it…This operation must take place without global program knowledge, and in most cases only with knowledge of that single decorator… Internally, these class transformers are called a “Compiler”
The implication here is that the new Angular compiler will run a series of independent TypeScript class transforms against the AST that represents a component class. These transformers act much like pure functions, accepting decorator metadata as input and outputting a definition as a static field on the class. Several of these class transformers, which are internally labelled as compilers, are already present in the codebase. Notably, the document stresses that transformation must proceed without any awareness of the overall program, a constraint rooted in the principle of locality.
Change detection under Ivy
The Change Detection mechanism has always been my primary focus within Angular. Having researched it deeply and written a fair amount about it, I was eager to see what shifted and whether my prior conclusions still stand. As it turns out, much like the core principles that guided AngularJS persisted into Angular, the foundational concepts and steps of change detection remain intact. They have simply been relocated from the Angular interpreter into the component template function, as discussed. In Ivy, they manifest as discrete instructions, i.e., functions, rather than forming a unified block resistant to tree shaking. The precise ordering may differ, and the lifecycle hook mechanism has been reworked, yet the mental framework I outlined in my previous pieces still applies to Ivy.
Internally, Ivy executes change detection by invoking the detectChanges function, passing the component class as its argument. It is unlikely that this function will enter the public API; instead, it will likely be wrapped by a familiar shell, such as ChangeDetectorRef.
That function mainly serves as a thin wrapper around detectChangesInternal, which takes a component view and carries out the actual check:
export function detectChangesInternal(view, hostNode, def, comp) {
const oldView = enterView(view, hostNode);
const template = def.template;
try {
template(getRenderFlags(view), component);
refreshDirectives();
refreshDynamicChildren();
} finally {
leaveView(oldView);
}
}
The wrapper function itself is now quite minimal. This stands in contrast to the current implementation, the runtime “interpreter” mentioned earlier, which takes a view and executes all change detection operations:
export function checkAndUpdateView(view: ViewData) {
// update child element and components inputs
Services.updateDirectives(view, CheckType.CheckAndUpdate);
// run change detection for embedded views
execEmbeddedViewsAction(view, ViewAction.CheckAndUpdate);
// update ContentChild & ContentChildren queries
execQueriesAction(...);
// calls AfterContentInit & AfterContentChecked lifecycle hooks
callLifecycleHooksChildrenFirst(...);
// update bindings
Services.updateRenderer(...)
// run change detection for child components
execComponentViewsAction(...);
// update ViewChild & ViewChildren queries
execQueriesAction(...);
// calls AfterViewInit & AfterViewChecked lifecycle hooks
callLifecycleHooksChildrenFirst(...);
...
}
With Ivy, the bulk of that logic is superseded by a straightforward invocation of the template function:
try {
// template function defined by a component definition
template(getRenderFlags(view), component);
...
}
And, as established, this template function contains numerous instructions that carry out operations tied to change detection, notably the updating of text nodes and input bindings.
Aside from invoking the template function, detectChangesInternal also makes two other calls: refreshDirectives and refreshDynamicChildren. Through refreshDirectives, Angular triggers change detection for child components. References to these child components are stored on a component view, similar to how it functions now. This same function is also responsible for initiating the NgOnInit lifecycle hook. Meanwhile, refreshDynamicChildren handles change detection for any embedded views housed in a view container.
Thus, if we were to annotate the new detectChangesInternal function, it would look akin to this:
export function detectChangesInternal(view, hostNode, def, comp) {
...
try {
/*
runs template function that executes instructions:
- updating child elements, directives and components inputs
- updating text bindings
- refreshing view and content queries
*/
template(getRenderFlags(view), component);
// runs change detection for child components
// and executes init and content life cycle hooks
refreshDirectives();
// runs change detection for embedded views
refreshDynamicChildren();
}
...
}
All the familiar operations remain, as is evident. However, the sequence of actions seems altered. For instance, it appears that Angular now processes child components before tackling embedded views. Since no compiler output exists yet to substantiate my assumptions, I cannot be certain of the exact order. I plan to hold off on a detailed breakdown of the new change detection flow until Ivy reaches at least a beta stage. Watch this space!
Examining NgOnChanges
I was equally curious to inspect the NgOnChanges hook implementation in Ivy, especially given claims in this translated piece suggesting it is no longer a genuine lifecycle hook:
So it is notable that
OnChangesin Ivy is not a real lifecycle any more… Put differently, users can extend lifecycle themselves, do as they please… It is no longer right to say Angular is based solely on dirty check
That statement comes across as somewhat puzzling. In the current implementation, the hook fires during change detection right when input bindings are updated. In Ivy, the hook gets called within the refreshDirectives function, once Angular has run the template function and refreshed the bindings for child components and elements. Therefore, the NgOnChanges call remains embedded within change detection and can still be trusted as a signal that some input bindings have altered.
The way this particular hook operates does diverge from the other lifecycle hooks. As that article points out, it is implemented as a feature—a means to intercept the directive definition and alter it. Essentially, the NgOnChangesFeature that provides the hook adds a wrapper surrounding the ngDoCheck lifecycle hook. That wrapper first invokes NgOnChanges if a change is detected, and then proceeds to the ngDoCheck lifecycle hook, labelled delegateHook in the snippet below:
componentDefinition.doCheck = onChangesWrapper(definition.doCheck);
...
function onChangesWrapper(delegateHook: (() => void) | null) {
return function(this: OnChangesExpando) {
let simpleChanges = this[PRIVATE_PREFIX];
if (simpleChanges != null) {
// calls NgOnChanges hook
this.ngOnChanges(simpleChanges);
this[PRIVATE_PREFIX] = null;
}
// calls NgDoCheck hook
delegateHook && delegateHook.apply(this);
};
}
Regardless, the ngOnChanges hook runs immediately before ngDoCheck, which preserves behavior consistent with the existing system.
My reading of the statement, "it is no longer right to say Angular is based solely on dirty check," is that the author intended to note that any feature could attach to the ngDoCheck hook and trigger NgOnChanges even absent any input changes. However, that would presuppose external parties are permitted to introduce their own features. Should that be the case, I imagine the framework will incorporate safeguards to prevent arbitrary calls to NgOnChanges.
Bootstrapping simplification
Ivy brings a more straightforward API for bootstrapping a component. With the current setup, an NgModule is required to declare the bootstrap component for an application:
@NgModule({
...
bootstrap: [AppComponent]
})
export class AppModule {}
platformBrowserDynamic().bootstrapModule(AppModule);
Under Ivy, the bootstrap function accepts the bootstrap component directly:
renderComponent(AppComponent);
