Angular recently announced a new render engine called Ivy.
Ivy is an amazing present from Angular team! It produces hello-world app in mere 3.2KB, on a par with minimal framework like preact. Unfortunately little documentation, if any, exists to explain how Ivy works.
This blog post strives to fill this gap, by translating an amazing answer from zhihu, a superior Q&A website to Quora. I hope this post can communicate Angular’s innovation to broader front-end world by breaking language barrier.
TL;DR: Ivy is an extremely building-tool friendly, web-first render engine that paves Angular’s way to web components.
Opinion is not mine. Words in parentheses are from the original author unless explicitly annotated as from translator. Translator notes are added for audience with no Angular background.
how to comment is a jargon on zhihu, meaning
what's your opinion)
Let’s first answer several frequent questions:
3.2KB is the result after minification + gzip (a convention to compare framework size), which is the exact payload size we send to 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 manually enable it by switching on certain compiler option. 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 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 extreme size isn’t achieved by the ADVANCED mode of Closure Compiler. 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.
Disclaimer: all the below is based on current (Feb, 2018) implementation, published roadmap. If following version changes leads to the following content outdated or incorrect, please pardon our no updating.
As a platform independent framework, can we run application without platform specific code? The answer is NO, of course. Ivy just inlines DOM Rendere to its core. If you only need run your application on browser (taking no account of WebWorker), you can run Angular without including platform specific code. For example, the code for binding text is:
Fallback code is obvious: if no
renderer exists (translator note: the condition is better explained as
renderer isn’t a
ProceduralRenderer3), Ivy will directly modify dom element’s
Currently no Ivy code relies on
platform, thus common problems arise:
No support for Event Plugin. e.g. syntactic sugar for keyboard event (like
keydown.enter) and events from Hammer, 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).
So the current Ivy renderer demoed in hello-world app isn’t a full featured Angular for some users.
Under the new Ivy mode, compiled template will be stored in the static fields in class, instead of generating new wrapper class (NgFactory). For example:
WAT? How can Your Majesty NgModule compiled to one single Injector?(
Due to the colocation of compiled template and class, the new compilation can fully enjoy building tool’s normal optimization. (Tree-Shaking for the most part), waiving most special process in build-optimizer.
In fact, this improvment 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 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 faster building.
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:
Hey, why we still need AppModule?
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 other word,
renderComponent can accept an optional configuration argument.
By the way, this is also friendlier to bootstrapping multiple Angular instances in one page.
In the previous Angular,
DevMode is disabled by function call dynamically.
In other word, whether DevMode is on is totally Angular’s internal state. Thus all debug related code is dynamically used and cannot be excluded in 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.
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. (Translator note: 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 own 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
DirectiveDef will import
NgOnChangesFeature. So optimizer can reasonably eliminate dead code.
WAT? How about the dirty check promise? From now on we have exception in Angular’s change detection?!
So it is notable that
OnChanges in Ivy is not a real lifecycle any more (Translator note: it is not listed in the
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.
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 classical sense (if anyone ever cared), we can easily find dependency injection and dead code elimination are contradictory in priciple.
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)
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.
(The density of information is almost as high as HTML.)
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).
- 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
(View layer is less friendly to pull request)
The new Ivy mode push “building-friendliness” to 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 crucial 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 wihout common runtime library, Angular should work with minial size.
Components used as Angular Elements will not depended on external packages like forms and router, hence creating great value for size reduction.
Whether should NgModule still exist? It certainly has value on organizing code. That said, Dart version didn’t ever has 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 “comonent 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 dirty check. All articles about change detection need updating! And every section needs separate explaination!
Lifecycles might have 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 has been greatly improved.