Understanding Angular Ivy Renderer

Valentyn Yakymenko

Ivy Renderer is one of the awesome things that was presented to us by Angular Team! Unfortunately, documentation is small, and I was trying to understand and find some information about how Ivy Renderer works.

And I found very interesting answer post at zhiu. After I read the original post, I was so impressed with Ivy Renderer and decided to translate that post, add additional changes from myself and tell you about Ivy Renderer.

Ivy Renderer Example

Let’s take a look for this wonderful example provided by Angular :

https://ng-ivy-demo.firebaseapp.com/

How to comment on the fact that Angular’s new Ivy render engine produces 3.2KB compiled JavaScript?

Let’s first answer several frequent questions:

  1. 3.2KB is the result after minify+ gzip (a convention to compare framework size), which is the exact payload size we send to the browser. (if we use more advanced compression like Brotli, the size will be smaller on modern browsers).
  2. Ivy renderer won’t be default in Angular v6. You need to enable it by switching on specific compiler option manually. It might be default in v7 if no further issues are found.
  3. Code completion isn’t proportional to feature completion. 50% code completed doesn’t imply 50% feature completed. For now, Ivy has seemingly decent code completion rate, but it still has a long way before ready for production. It can’t even complete a simple app for the benchmark.
  4. Compared with previous Angular’s compiled code, Ivy’s result is almost like hand-written. And actually, you can write it by hand! (Though in practice you won’t.)
  5. NgModule is challenged, again.
  6. Ivy won’t promise you a plunge in code weight. Your bundle will still be bulky if a lot of CSS is inlined in your components.

Since the question is to comment on 3.2KB compiled file, I will focus on payload size optimization, thus, answering the question “What optimization has Ivy Renderer done?”.

It can never be overstated that the ADVANCED mode of Closure Compiler doesn’t achieve the extreme size. Rollup can achieve the same level optimization (within less than 1KB difference). Therefore Ivy is arguably the true building-tool friendly renderer, not Closure-Compiler-only renderer.

Removing dependence on platform-browser

As a platform-independent framework, can we run the application without platform-specific code? The answer is NO, of course. Ivy inlines DOM Renderer to its core. If you only need run your application on the browser (taking no account of WebWorker), you can run Angular without including platform-specific code. For example, the code for binding text is:

The fallback code is obvious: if no renderer exists (translator note: the condition is better explained as therenderer isn’t a ProceduralRenderer3), Ivy will directly modify DOM element’s textContent.

Currently, no Ivy code relies on the platform. Thus common problems arise:

  • No support for Event Plugin. e.g. syntactic sugar for keyboard event (like keyup.enter) and events from hammer.js, which are all provided by platform-browser.
  • No Sanitizer. Though Angular isn’t string template by itself and is naturally immune to XSS, it still cannot survive abhorrent abuse of innerHTML. Sanitizer, again currently bestowed by platform-browser, comes to rescue by filtering user content. Without platform hardly can we achieve the same level security.
  • No support for Component Style; No View Encapsulation. They are all implemented by different Renderers in platform-browser package. Only inline style is available for component styling (Another better way is independent CSS and dynamic class).
  • No support for SVG.

Note: You can always watch for the development process and newly supported features here.

No NgFactory file anymore

Under the new Ivy mode, the compiled template will be stored in the static fields in the class, instead of generating new wrapper class (NgFactory). For example:

Component -> ngComponentDef

Directive -> ngDirectiveDef

NgModule -> ngInjectorDef

Due to the colocation of compiled template and class, the new compilation can thoroughly enjoy building tool’s standard optimization. (Tree-Shaking for the most part), waiving most special process in build-optimizer.

In fact, this improvement is more significant for building than for size optimization. The new style compiler enables single file compilation, one ts file to one js file. We no longer need to worry about mapping from the source file to ngfactory.

Further more, this unifies library consumption in JIT and AOT. Once the compilation is stabilized, all libraries can compile code before publication, rather than at end uses’ bundling phase. This can lead to the faster building.

Greatly simplify bootstrap code

All Angular applications need to configure bootstrap component (actually it isn’t mandatory, but alternatives are more complex), and initialize one NgModule[Factory]. Something like:

New bootstrapping code is based on component:

Ivy is based on NgModule-less bootstrapping for now. Though not completed yet, compilers can make bootstrapping accept NgModule (if it still exists) to provide Injector. In another word, renderComponent can take an optional configuration argument.

By the way, this is also friendlier to bootstrapping multiple Angular instances in one page.

Redesigned DevMode configuration and checking

In the previous Angular, DevMode is disabled by function call dynamically.

In another word, whether DevMode is on is Angular’s internal state. Thus all debug related code is dynamically used and cannot be excluded at compile time. (Even closure compiler is ineffective).

But in Ivy DevMode is purely compile-time configuration, all debugging code will be executed after checking the global variable ngDevMode. For example:

So building tool can replace the corresponding variable (e.g Webpack’s DefinePlugin), and corresponding debug code can be detected as dead code, and then optimizer (e.g UglifyJS) can remove it.

The Feature named as Feature

Ivy mode has added a new feature called Feature. Or, Ivy introduces a new concept called Feature. Simply put, Feature is a pre-processor for DirectiveDef, which can be thought as a “Decorator pattern” tailored for DirectiveDef. (More simply, you can provide a custom function to Angular, and Angular will apply it against ComponentDef metadata. So you can extend ComponentDef‘s Feature in your function. Source).

Let’s take an example in Ivy. OnChanges isn’t implemented by Renderer but a predefined Feature. The Feature uses defineProperty to listen on the property decorated by @Input, and automatically stores previousValue in the instance to generate SimpleChanges for change detection, and finally triggers OnChanges by intercepting OnInit and DoCheck without Angular core’s help.

This means OnChanges‘ code is also tree-shakable! If no component ever declares the lifecycle ngOnChanges, no DirectiveDefwill import NgOnChangesFeature. So optimizer can reasonably eliminate dead code.

So it is notable that OnChanges in Ivy is not a real lifecycle any more.
Put differently, users can extend lifecycle themselves, do as they please.

But the direct aim of Feature concept is for the incoming LifeCycle as Observables. Users don’t need to declare methods (ngOnInt) in class, but rather use the lifecycle observables directly. Indeed. by intercepting DirectiveDef, users and third-party library authors can modify lifecycle hooks in runtime. We can even use decorators to declare lifecycles.

In summary, Feature can be thought as a variant of Higher Order Component (compared with class factory): it modifies component type itself based on runtime requirement, instead of changing component’s internal state according to component logic.

New Injectable API

Actually this isn’t related to the hello-world demo, nor related to even Ivy (since it can be used in non Ivy mode). However, the new Injectable API has a huge impact on Angular’s size.

Besides the XXXDef properties listed above, we have another one called ngInjectableDef:

In a classical sense (if anyone ever cared), we can easily find dependency injection, and dead code elimination is contradictory in principle.

  • DI’s nature is side effect: Provider changes execution context via configuration, and Consumer retrieves content from context.
  • DCE’s assumption is side-effect-free: Consumer should import Provider directly, and if Consumer doesn’t exist in application, Provider can be removed at all.

Apparently, all DI based code cannot be effectively DCE optimized.

In current Angular pattern, we will configure Provider in NgModule:

The dependence flow is: (translator note: in the below items parentheses after is added by translator for Angular newcomer)

  • Library Module imports Library Service (for exposing interface and providing implementation)
  • Application Component imports Library Service (for consuming interface)
  • Application Module imports Application Component
  • Application Module imports Library Module (for configuring which implementation to inject)

Even if service isn’t used in application component, service cannot be removed. This is because we introduce additional dependence during DI configuration. (translator note: consider app component doesn’t import libService. It is desirable libService is excluded from final build. But we cannot eliminate it because libModule imports it, and libModule is further imported by application module)

To solve this problem, Angular allows to invert the dependence of configuration. The new API is like:

The new dependence flow is:

  • Library Service imports Library Module
  • Application Component imports Library Service
  • Application Module imports Application Component
  • Application Module imports Library Module

Thus, as long as application component is removed, library service is not depended at all and can be safely removed together.

WAT? So why is libModule there? As a cosmetic?

We can optionally choose providers like useClass, useFactory to set implementation to other values instead of current class.
(translator note: they are Angular’s alternative DI functions and are DCE ready)

Of course, the new Injectable API only handles one ideal situation where dependency declared is dependency used. Nevertheless this is the most common situation. If intermediate Injector nodes overrides Provider, side effect is still ineluctable and thus adds code size. (But the injected dependency is probably used when one does override)

New template compilation

Template compilation, at macro level, has only two types:

  • (structural) data
  • (operational) instructions

Take a simple example. If we compile template (or equivalent) to some render method and if:

  • calling render doesn’t make view changed, but its return value is what to be rendered. This is the former type.
  • calling render does update view, and thus no return value is needed. This is the latter type.

The most early Angular, v2 version, compiles template to instructions. One can refer to this zhihu answer for compilation behavior. This style is very similar to Svelte: compile as much detail as possible, in order to reduce common runtime dependency (shared code).

But the problem of this approach is obvious. With more and more template authored, compiled code size will easily exceed shared code (Off-topic: the 0kb-boasting framework Svelte also provides a shared compile option to use library).

Later Angular v4 compiles template to data and introduces View Engine as common dependency. Its compilation is explained here. This style is very close to virtual dom, except that dom-like data is stored per type, not per instance.

Ivy renderer in v6 re-chooses compiling to instruction approach. Different from v2, v6 employs a strategy to minimize compiled code size.

Compiling to instructions has another advantage over compiling to data: common dependency is still DCE friendly.
Used instructions will be directly depended, and non-used instructions will be removed by optimizer.
On the other hand compiling to data requires all operations used by template processor, and thus cannot be optimized statically.

Therefore, Ivy’s new compilation strategy should be the one with minimal code size (not regarding of the cost to implement compiler).

Memory and Speed

  • New view layer employs compact binary frame design (and more bitwise operations).
  • New view layer uses as many sequences (arrays) as possible rather than key-value pairs (objects) to store data.
  • New view layer prefers adding expando properties on exposing types rather than new wrapping type.
  • DI uses bloom filter to speed up Directive searching

Summary

The new Ivy mode push “building-friendliness” too extreme, but you can also say it is not “non-building-friendly”. It won’t be useful for projects built with bare <script> tags.

Ivy’s most important target is cooperation with Angular Element. By encapsulating Angular components to custom elements (web components), we can achieve standalone publication, independent importing and independent usage of Angular as widgets. (To some degree this strangulates Svelte?)

The most important part of this strategy is code size. Even without common runtime library, Angular should work with minimal size.
Components used as Angular Elements will not be depended on external packages like forms and router, hence creating significant value for size reduction.

Whether should NgModule still exist? It certainly has value for organizing code. That said, Dart version didn’t ever have NgModule. It is sure that application structure can be well-formed without NgModule (at least for Googler).
With decreasing usage of NgModule in real world API, it might be optional in future. But I’m sure v6 won’t change NgModule. (I personally favor NgModule, but oppose enforcing it in app).

For applications plumbing many third-party libraries, the main size problem might not come from Angular but rather from, for example, incorrect use of RxJS, importing moment.js inadvertently, or writing all styles in “component styles”. For specific size problem, one needs to analyze it specifically. Don’t pray to Angular’s advanced optimizer.

The worst part of Ivy is OnChanges. It is now implemented by Object.defineProperty! It is no longer right to say Angular is based solely on the dirty check. All articles about change detection need updating! And every section requires a separate explanation!

Lifecycles might have the big change in near future. But it will await Ivy being the default, no earlier than v7. In fact, Angular’s overall extensibility and runtime preprocessing have been greatly improved.

References

Valentyn Yakymenko

Written by

Front-End Architect at Codeminders | Passionate about web performance optimization. | Code hard, learn more.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade