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 :
Let’s first answer several frequent questions:
- 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).
- 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.
- 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.
- 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.)
NgModuleis challenged, again.
- 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
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 the
renderer isn’t a
ProceduralRenderer3), Ivy will directly modify DOM element’s
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.
Sanitizer. Though Angular isn’t string template by itself and is naturally immune to XSS, it still cannot survive abhorrent abuse of
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:
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
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
DoCheck without Angular core’s help.
OnChanges‘ code is also tree-shakable! If no component ever declares the lifecycle
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.
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.
XXXDef properties listed above, we have another one called
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:
Providerchanges execution context via configuration, and
Consumerretrieves content from context.
- DCE’s assumption is side-effect-free:
Providerdirectly, and if
Consumerdoesn’t exist in application,
Providercan be removed at all.
Apparently, all DI based code cannot be effectively DCE optimized.
In current Angular pattern, we will configure
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
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:
renderdoesn’t make view changed, but its return value is what to be rendered. This is the former type.
renderdoes 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
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
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.