Skip to main content Skip to footer

Build Times, Bundle Sizes, and Other Improvements Made by Angular Ivy

With the release of Angular 9 came Ivy, Angular’s new compiler. It was initially released with Angular 8, but was made the default compiler for new Angular applications with the version 9 release. Ivy is replacing the View Engine compiler, which had been Angular’s default compiler since its release with Angular 4. However, the Angular team has made it possible to still use the View Engine in Angular 9, but you will have to opt-in to use it over the Ivy compiler.

Ivy is a large step for Angular. It is a complete redesign of how the framework functions internally, without changing how we, as developers, write our Angular applications. The redesign of Angular with Ivy serves two primary purposes:

  • Reach better build times through more incremental compilation
  • Reach better bundle sizes through improved tree-shaking compatibility

An essential part of all of this is that, as developers, we will not have to change the way that we write our applications. Ivy aims to be compatible with existing applications, and using it will be as easy as flicking a switch for most projects.

A big part of the push for reducing bundle sizes, though it improves loading performance across the board, is to help improve the user experience for those who are accessing the applications that we build through mobile devices, such as smartphones or tablets. As time has gone on, mobile device usage has increased, accounting for 53% of all internet traffic within the United States in 2019.

Unfortunately, mobile devices often suffer from a bad or slow internet connection, making it harder for us to deliver the application as fast as possible. We have solutions that can help us with this, such as using CDNs to serve files or PWAs (Progressive Web Apps) to cache assets. Still, the best opportunity that we as developers have is to reduce the bundle sizes that our applications produce.

Ivy has also introduced a host of other improvements along with the two mentioned above, such as:

  • Improved build errors
  • Faster Testing
  • Better Debugging
  • Improved CSS & style bindings
  • Improved internationalization

We will be looking at each of these features in a little more depth throughout the blog. We will also take some time to go over any potential changes that developers may need to make and build libraries compatible with the View Engine and Ivy compilers and consume those libraries within your Ivy application.

Ivy Features

Improved Bundling with Tree Shaking

The first improvement that we will be looking at is how Ivy helps us shrink our bundle sizes. Just as a quick refresher, bundling in JavaScript is an optimization technique that allows us to merge multiple JavaScript files into fewer files. This allows us to reduce the number of page requests that users must make to the server, meaning that we can render our pages more quickly for our users.

Now, what is tree-shaking? Tree shaking is a term commonly used in the JavaScript context for dead-code elimination. The idea is that all execution flows of a program can be represented as a tree of function calls so that functions that are never called can be eliminated.

Note that if you are using CommonJS to bundle your application, Angular will now throw a warning when you build your application. The reason for this warning is that using CommonJS modules can result in larger bundle sizes when performing a production build. This is because CommonJS was designed to be used on server-side applications, meaning it was not properly optimized to run on client-side applications. If your application is still using CommonJS, it is recommended that you switch to using ES modules to bundle your application.

To provide an example, CommonJS allows you to pass a variable into the path when importing functionality from another module. In contrast, ES modules require it to be a string literal.

code 1

The first line is a CommonJS require statement, and the second is an ES module import statement. As you can see, the ES modules import statement uses a string literal, whereas the CommonJS require statement can take a variable in as a value.

CommonJS can take this dynamic approach because of how the dependencies are loaded. CommonJS needs to load modules at runtime. The modules are loaded, instantiated, and evaluated all at once. This is what allows you to do things like pass variables into file paths. Since this occurs at runtime, it makes it harder for applications to perform tree-shaking.

ES modules, however, do not have the same problem. Because ES modules are more static, they are more statically analyzable. The loading, instantiating, and evaluation that occurs when broken up into three distinct phases can be done separately, instead of being required to perform them together, like with CommonJS. This allows us the ability for more efficient tree-shaking.

Now, how does Ivy allow tree-shaking when the View Engine does not, and while making use of ES2015? It has to do with how Ivy updates the DOM versus how the View Engine does. When the View Engine first processes the application code, it notes all associations between the HTML properties and the TypeScript properties.

Then it goes about creating bindings – a set of instructions for each HTML-TS-property association. Since these bindings are component-specific, it is easier to optimize them than some generic instructions. The resulting template data is then taken by the Angular Interpreter and transformed into the DOM.

template HTML 2

Whenever the bindings are created, Angular then only deals with the bindings for change detection. When Angular receives notice of a change, all bindings are re-evaluated. This process is called dirty-checking. After all bindings are re-evaluated, the binding for which property has changed is then marked and updated. Finally, the template data is interpreted and transformed into the DOM with the latest bindings.

With Ivy, instead of generating template data and passing it to an interpreter that decides which operations to run, a set of template instructions is now generated directly. The template instructions are where the logic that instantiates components creates DOM nodes and runs change detection now lives. Look at the second image to get a look at the new rendering pipeline:

template HTML 3

The final thing to look at in regards to bundling is how much of a decrease in bundle size we see. The Angular team says that we should see a reduction in bundle size of roughly 20-30% for small and large applications and that we should expect to see slightly lower numbers for a medium-sized application. You can see the results of our internal tests here:

test results 4

These tests show bundle sizes using the View Engine versus the Ivy compiler. The first column shows the Wijmo controls that we have included in each project. The second column is our bundle size output from the Ivy compiler, the third is the bundle size output from the View Engine, and the last column shows the bundle size percentage decrease between the two Angular compilers.

There is over a 30% decrease in some of the smaller applications from our tests, and as we get into medium-sized applications, it hovers at around a 25% decrease. When everything is said and done, we see a 30% decrease in overall bundle sizes, which is in line with the angular team's numbers saying that we should expect to see.

Improved Build Times & Errors

The second large change that we will take a look at is the improvements that the Angular team has made in both application build times and improved build errors with the release of Ivy. When building your application, each build would force Angular to recompile everything inside the module and would do so to check and see changes in your code. It did this because the generated code of a component could be using another component's internal details.

Now, each component references the directives and components it uses only by their public APIs. If you modify an internal detail of a component or directive, your application will know which other components use the modified code and only recompile those components. This can lead to massive benefits in build times for applications with many components and directives. Instead of being required to recompile the entire application, you are only recompiling those components that need it.

Let us take a quick look at a small example to see what some of these changes look like in the compiler. In this sample, we will be going over our mock component, CarComponent, that takes an input value called car when the component is created.

code 5

Now, when the CarComponent component gets used inside of another component template, the code will look something like this:

code 6

code 7

Let's look at the code that both the View Engine and the Ivy compiler generate due to our component's input. For Ivy, the code will look like this:

code 8

As you can see here, the Ivy code is referencing the CarComponent’s public name. Contrast this to the code generated by the View Engine compiler, which you can see here:

code 9

Here, the View Engine-generated code is only referencing the private field of the CarComponent. The difference that we see here is because of Ivy's Principle of Locality.

The Principle of Locality in Ivy means that to compile a component in Ivy, Angular only needs information about the component itself, except for the name and package name of its declarable dependencies. Most notably, Ivy doesn’t need metadata of any declarable dependencies to compile a component.

Now, this may be a small sample, but this applies to large applications, as well as small.
All in all, the Angular team expects build times to improve by roughly 40%.

The cool thing about these improvements that the Angular team has made to Ivy is that Angular’s AOT (Ahead-of-Time) builds are noticeably faster. This means that the Angular team has made AOT build the default build for Ivy applications, both in developer mode and production mode.

I want to touch quickly on AOT builds because the changes that allow for these kinds of builds in development mode, and not just production mode, have made other improvements to our applications mentioned later in the presentation. It's essential to have at least a simple understanding AOT builds.

AOT, or Ahead-of-Time compilation, converts your code from Angular HTML and TypeScript into efficient JavaScript code during the build phase before the browser downloads and runs your code. Compiling your application during the build process will provide faster rendering in the browser.

Some reasons that you would want to use AOT include:

  • Faster rendering – With AOT, the browser downloads a pre-compiled version of the application. The browser loads executable code to render the application immediately, without waiting to compile the app first.
  • Fewer asynchronous requests – The compiler inlines external HTML templates and CSS style sheets within the application JavaScript, eliminating separate ajax requests for those source files.
  • Smaller Angular framework download size – There's no need to download the Angular compiler if the app is already compiled. The compiler is roughly half of Angular itself, so omitting it dramatically reduces the application payload.
  • Detect template errors earlier – The AOT compiler detects and reports template binding errors during the build step before users can see them.
  • Better security – AOT compiles the HTML templates and components into JavaScript files long before they are served to the client. There are fewer opportunities for injection attacks with no templates to read and no risky client-side HTML or JavaScript evaluation.

The Ivy compiler not only includes changes to improve build times, but it also has made improvements on error messages that Angular displays during the build process. For example, take a look at the following error output by Angular:

error 10

Now, when we’re using the Ivy compiler, the same code will produce this error when building the application:

error 11

There's a pretty big difference in the error message's readability between the View Engine and the Ivy compilers. The error message's bullet points are the same; they let the developer know that the user element that is trying to load may be a component, but it has not been included as a part of the module as it builts. However, the Ivy error consists of the file that the code causing the error is and the line that it appears. This makes it much easier for us to address our code errors, especially for more complicated errors.

Faster Testing

Now, we'll get into some of the changes that Ivy has made to test our applications faster and more efficiently.

With Ivy's release, Angular now enables us to use the AOT compilation in all phases of our projects, including the testing phase. This allows us to catch errors more quickly since we have fewer differences between the testing environment and the production environment.

The Angular team has also taken this opportunity to revamp Angular’s implementation of the TestBed class in Ivy to improve its efficiency.

Previously, TestBed would recompile all components between each test's running (between each statement), regardless of whether any changes were made to components (for example, through overrides).

In Ivy, TestBed doesn’t recompile components between tests unless a component has been manually overridden, which allows it to avoid recompilation between a majority of developers’ tests.

With this change, the Angular team has seen the framework’s core acceptance test speeds improve roughly 40%. They’ve stated that they expect users to see their own application test speeds to be around 40-50% faster than before.

The Angular CDK also introduces a testing sub-package with version 9, which implements a component harness for test cases.

A component harness hides the implementation details of a component. It exposes an API that can retrieve important DOM attributes such as ARIA attributes, interact with the component as a user would, without querying the component's DOM, and harness related components such as child components dialogs and menus triggered by the current component.

Component harnesses are used in unit tests, integration tests, and end-to-end tests. The Angular CDK comes with two harness environments out of the box:

  • TestbedHarnessEnvironment is used for unit tests and integration tests
  • ProtractorHarnessEnvironment is used for end-to-end tests driven by Protractor

The TestbedHarnessEnvironment is first and foremost meant to be used with Jasmine and Karma, the DeFacto Angular unit testing stack, but should work with other test runners and test frameworks as well.

Improved Debugging

We have many tools at our disposal to debug web applications; from using simple logging statements in our code to browser development tools and web extensions, there are plenty of ways for us to tackle the issue of debugging our applications. But the Angular team has given us a new tool with the release of Ivy, a global variable called ng.

This new variable allows us to access the corresponding Angular components. We can not only see the component properties and functions, but we can also modify them in our browser’s console and trigger change detection.

To use the ng object, you’ll need to have your application running in dev mode. With the ng object, you can:

  • Ask Angular for access to instances of your components, directives, and more
  • Manually call methods and update the state
  • Trigger change detection with applyChanges when you want to see the results of change detection

By doing this, you can avoid inserting unnecessary logging statements in your code because you can see the component state directly in your browser without any additional tools.

One of the errors that everyone hated to see during Angular development was the ExpressionChangedAfterItHasBeenChecked error. The error output was messy, making it hard to debug the issue and find the root cause.

error 12

With Ivy, the error message outputs a much more useful stack trace, allowing you to jump directly to the template instruction with the expression that has been changed.

For example, if you click on AppComponent_Template in the stack trace below, you can see the specific line in the generated code where the error is thrown.

error 13

error 14

If you’d like, you’re also able to step into any of these framework instructions to walk through how the framework creates and updates your components.

Improved CSS & Style Bindings

The Ivy compiler and runtime provide improvements for handling styles and the ability to use style variables inside the HTML template.

In previous versions of Angular, whichever style binding was evaluated last on the HTML element would be the one that would be the applied style. Take a look at the following code snippet:

code 15

code 16

Here, we have three different ways to apply a color to our element; through the style attribute, using style.color and assigning a value to it, and setting the color property with a variable. In this scenario, the final style to be applied would be the one that would take effect. If myColor and otherColor were both undefined, then the static 'red' would be ignored because it was overwritten by the style setting that came later.

With the Ivy compiler, you can use a clear and concise order of precedence to determine which style will affect your code is compiled. With Ivy, the most specific styles always have the highest priority. For example, binding the element's color through [style.color] will override a conflicting binding to [style].

Another change included is the ability to use CSS variables inside your HTML templates.

code 17

From this code snippet, you can see that we're setting our CSS variable inside of the div and then using it inside of the following paragraph element.

Better Template Type Checking

Like how TypeScript catches type errors in your code, Angular checks the expressions and bindings within your application's templates and can report any type of errors that it finds. With the move from the View Engine to Ivy, the angular team has made some improvements to allow for easier error detection with the fullTemplateTypeCheck setting and added a new setting: strictTemplates.

Before we get into any new changes for type checking in Ivy, we’ll do a quick review of template type checking in Angular 8. The following component displays a list of heroes in the template:

code 18

code 19

Next, we’ll navigate over to our tsconfig.json file. In this file, we will take a look at the fullTemplateTypeCheck property, which, in Angular 8, is set to true by default:

code 20

When building the application with ng build, everything compiles successfully, but there are some issues with how it is displayed in the browser.

countries 21

You'll see that we're missing out on the title that we want to display, and we're also not showing each countries country code in the list. However, the application throws no errors during the build process to let the developer know these issues.

However, we run into an issue when trying to build the application while using AOT by running the ng build --prod command. When we run the command, you'll see that Angular reports that the title property does not exist in our component. When both AOT and the fullTemplateTypeCheck property are set to true, Angular can detect when we refer to a missing model from our component.

error 22

When the fullTemplateTypeCheck property is set to true, Angular is more aggressive in its type-checking within templates.

  • Embedded views, such as those within ngFor and ngIf are checked
  • Pipes are checked to make sure they have the correct return type
  • Local references to directives and pipes are checked to make sure they have the correct type (except for any generic parameters)

Now, we'll take a look at how Ivy handles type checking when building a project. While using Ivy, we'll navigate back to the tsconfig.json file, and once again make sure that fullTemplateTypeCheck is set to true. When we run the application now, we'll see a similar error to the one that we had seen previously using ng build.

error 23

The first thing you'll notice is the improvement in the error message. You'll see here that it includes all of the previous message's information, albeit a little better organized. It also shows us the exact line of code that is causing the error; in this case, it’s the line of HTML that tries to display the text held in the title variable.

Remember, Ivy uses AOT by default, so we're now going to be seeing these errors without having to specify a production build. Because of the change and AOT utilization in all forms of builds, developers will now be able to catch these template-type checking errors sooner rather than later. If you go back into your app.component.ts file and add a title property, and fix the country code property ID in the HTML, you’ll be able to run the application without issue.

countries 24

Now, we’ll take a look at the new setting added to allow for more type checking in Ivy: strict mode with strictTemplate. Strict mode is a superset of full mode and can be used by setting the strictTemplate setting to true. In strict mode, Ivy adds checks that go beyond those in full mode, including:

  • Verifying that component/directive bindings are assignable to their @Input()s
  • Obey TypeScript’s strictNullChecks flag when validating the above.
  • Infers the correct type of components/directives, including generics
  • Infers template context types were configured (for example, allowing correct type-checking for ngFor)
  • Infers the correct type of $event in component/directive, DOM, and animation event bindings.
  • Infers the correct type of local references to DOM elements, based on the tags name (for example, the type that document.createElement would return for that tag)

To enable strict mode, first you’ll need to open up the tsconfig.json file again. Navigate down to the fullTemplateTypeCheck setting and replace that with the strictTemplate setting. You’ll then want to make sure that the strictTemplate setting is set to true so that your application will perform strict mode template type checks.

code 25

Improved Internationalization and Localization

The Ivy compiler also makes improvements on internationalization and localizing your applications. Internationalization is the process of designing and preparing your app to be usable in different languages. Localization is the process of translating your internationalized app into specific languages for particular locales.

To mark items that you wanted to add localizable messages to, you need to include the i18n attribute to the element:

The Angular compiler will replace this text when compiling the template with different text if a set of translations was provided in the compiler configuration. The i18n tags are compelling. They can be used in attributes and content, they can include complex nested ICU expressions, and they can have metadata attached to them.

However, some issues arose with localization and internationalization. The most significant concern was that translation had to happen during template compilation, which occurs right at the start of the build pipeline. The result of this is that the full build, compilation-bundling-minification-etc., had to happen for each locale you wanted to support in your application.

This means that if a single build took 2 minutes, then the total build time to support a dozen languages would take 24 minutes.

Moreover, it was impossible to mark text in application code for translation, only text in component templates. This resulted in awkward workarounds where developers would create artificial components that existed solely to hold the text translated.

Finally, it was impossible to load translations at runtime, which meant applications couldn't be provided to an end-user who might want to provide translations of their own.

The Ivy compiler has been redesigned to generate $localize tagged strings rather than doing the translation itself. This means that after the Angular compiler has completed its work, all the template text marked with i18n attributes have been converted to $localize tagged strings, which can be processed just like any other tagged.

Using the Ivy Compiler

If you're using Angular 9, or a later version, your project will have Ivy enabled by default. If you're currently working in Angular 8, the View Engine is enabled by default, but you can allow the Ivy compiler if you'd like. However, it is recommended to upgrade to a newer version of Angular if you intend to use the Ivy compiler. That will enable you to get all of the most up-to-date features and fixes that the Angular team has released.

If you’re upgrading an already-existing application to use Ivy, the Angular team has done their best to make it so that developers have to do the minimal amount of work to get their application running with Ivy. However, there still may be a few changes that you have to make.

If you see errors while using Ivy, the first thing that you'll want to do is turn off Ivy. This can be done in the tsconfig.json file, underneath the angularCompilerOptions setting. Set it to false, and then restart your application. If you do not see any errors, then you know the issues are Ivy-related.

Some of the more common changes that you may see include:

  • By default, @ContentChildren queries will only search direct child nodes in the DOM hierarchy (previously, they would search any nesting level in the DOM as long as another directive wasn’t matched above it).
  • All classes that use Angular DI must have an Angular decorator like @Directive() or @Injectable (previously, undecorated levels were allowed in AOT mode only or if injection flags were used).
  • Unbound inputs for directives (e.g., name in ) are now set upon creation of the view before change detection runs (previously, all inputs were set during change detection).
  • Static attributes set directly in the HTML of a template will override any conflicting host attributes set by directives or components (previously, static host attributes set by directives/components would override static template attributes if conflicting.

There are also several fewer common changes that developers may see, including:

  • Properties like host inside @Component or @Directive decorators can be inherited.
  • HammerJS support is opt-in through importing the HammerJSModule.
  • @ContentChild and @ContentChildren queries will no longer be able to match their directive's host node.
  • If a token is injected with the @Host of @Self flag, the module injector is not searched for that token.
  • When accessing multiple local refs with the same name in template bindings, the first is matched.
  • Directives that are used in an exported module are exported publicly.
  • Foreign functions or foreign constants in decorator metadata aren’t statically resolvable.
  • Forward references to directive inputs accessed through local refs are no longer supported by default.
  • If there is both an unbound css attribute and a [class] binding, the unbound attribute classes will also be added.
  • It is now an error to assign values to template-only variables like an item in ngFor=”let item of items”.
  • It’s no longer possible to overwrite lifecycle hooks with mocks on directive instances for testing.
  • Special injection tokens return a new instance whenever they are requested. This primarily affects tests that make identity comparisons of these objects.
  • ICU parsing happens at runtime, so only text, HTML tags, and text bindings are allowed inside the ICU cases.
  • Adding text bindings into i18n translations that are not present in the source template itself will throw a runtime error.
  • Extra HTML tags in i18n translations that are not present in the source template itself will be rendered as plain text.
  • Privoders formatted as {provide: X} without a useValue, useFactory, useExisting, or useClass property are treated like {provide: X, useClass: X}.
  • DebugElement.attributes returns undefined for attributes that were added and then subsequently removed.
  • DebugElement.classes returns undefined for classes that were added and then subsequently removed.
  • If selecting the native option element in a select where the options are created via *ngFor, use the [selected] property of an option instead of binding to the [value] property of the select element.
  • Embedded views are now inserted in front of the anchor DOM comment node rather than behind it as was the case previously.

Building and Consuming Libraries with Ivy

With the change from the View Engine compiler to the Ivy compiler, developers may wonder if they will continue to use libraries built using the View Engine. Thankfully, the Angular team has created the Angular compatibility compiler to allow us to use these View Engine-compiled libraries with Ivy easily.

The Angular compatibility Aompiler (or ngcc), parses the pre-compiled JavaScript code produced by the View Engine compiler. It will then update that code so that it is as if the Ivy compiler had compiled it.

If you are using libraries built within the View Engine, you will need to enable the compatibility compiler. To do so, you’ll want to open your project’s package.json file and adding a postinstall npm script:

{  
  “scripts”: {  
    “postinstall”: “ngcc”  
  }  
}

The postinstall script will run on every installation of node_modules, including those performed by ng update and ng add.

The compatibility compiler works by scanning the node_modules folder for packages that are in Angular package format (APF) by checking the .metadata.json files. The compatibility compiler will then produce Ivy-compatible versions for each of these packages.

If you are a library author, or are supporting an Angular library, the Angular team still recommends building your libraries using the View Engine compiler over Ivy. The reason for this is that Ivy-generated code is not backward compatible with the View Engine, so apps that are using the View Engine won't be able to make use of them.

Furthermore, the internal Ivy instructions are not yet stable, which can potentially break applications using a different version of Angular than the one used to build the library. It is recommended to let the Angular CLI use the compatibility compiler to convert your View Engine generated code into Ivy compatible code.

Final Thoughts

That covers all of the major changes that the Angular team has been able to make with switching over from the View Engine compiler to the Ivy compiler. I know that we've covered a lot in this presentation, so I'd like to do a quick rehash of the most significant changes and reasons for switching to the Ivy compiler:

  • Improved bundling: Ivy has changed how it handles rendering versus how the View Engine felt rendering. The View Engine needed to create template data from the template HTML, which it then handed off to the Angular compiler which used that to make the DOM. The Ivy engine does away with the template data and the compiler needed to translate it to the DOM, and instead uses a set of template instructions created from the HTML to create the DOM. This has allowed for the bundlers to more easily perform tree-shaking, the act of removing unused code from your files when bundling. This has made it so that our applications' bundled files are smaller, increasing our users' render speed.
  • Improved build times: The Angular team now makes use of the principle of locality when building applications. The principle of locality in Ivy means that, to compile a Ivy component, Angular only needs information about the component itself, except for the name and package name of its declarable dependencies. Most notably, Ivy doesn't need metadata of any declarable dependencies to compile a component. This has made AOT compilation much faster, meaning that the Angular team has made AOT the default build option in your applications, making it easier to see build errors that may appear during the production build.

As we have seen, some small changes will need to be made by some developers in the upgrade process from the View Engine compiler to the Ivy compiler. Still, outside of any potential changes, the change to Ivy is done in a way that we as developers won't need to change the way that we build our applications while giving us all the benefits of the changes that the Angular team made on the backend.


Joel Parks - Product Manager

Joel Parks

Technical Engagement Engineer
comments powered by Disqus