Think of any Angular application you've encountered. There are CSS, TypeScript, and HTML templates, all working in a delicate dance together to render a carefully crafted application. Now think deeper. All that databinding syntax, or those unique IDs in nested components that get reused multiple times don't just magically work. It's the Angular compiler that has injected smarts into them. There's executable JavaScript code that the Angular compiler must understand and run to be able to render your application properly.
There are two approaches that accomplish this. You can compile the app in the browser at runtime. This means that when the application loads, the JIT, or just-in-time compiler, does the compilation. You can imagine that this approach can have a huge performance penalty. Before any component can be rendered, its associated HTML template must be compiled. Not only that, but the application is bigger in size. This is because the Angular compiler, and a lot of library code that does this JIT compilation for you, need to be shipped with your application. Also, there may be issues in your application itself, issues that get discovered at run time.
Clearly, JIT has some shortcomings. The biggest advantage JIT has, of course, is that during development time, applications launch faster. Your debugging and development experience is better. However, this advantage will go away in future versions of Angular as Ahead-of-Time compilation gets faster and better. In fact, Ahead-of-Time (AOT) may become the default at some point.
AOT? Yes, Ahead of Time compilation. Ahead of time compilation is exactly the same as JIT, except it does the template compilation ahead of time instead of at runtime.
Advantages of AOT
The obvious question is: Why bother with AOT? There are some valuable advantages:
- Your application renders faster. This is an obvious win because the compilation that was being done right before application view rendering is now done ahead of time. You now download and run precompiled code.
- Your application needs fewer asynch requests. This is because the HTML and CSS is now part of a single JavaScript file. However, this advantage is largely negated by using bundlers such as Webpack.
- You'll have smaller Angular framework size and therefore smaller application size. The compiler (JIT or AOT) is roughly half the size of Angular itself. Imagine if you could leave half of it behind because you aren't doing this compilation at runtime. Yes, that is what AOT gives you. It's quite incredible.
- AOT helps you detect errors earlier. In fact, if you use JIT, you'll make some really silly mistakes that seem to work but are technically poor code.
- Also, because all of the HTML and CSS is precompiled, it's harder to reverse engineer. Your IP is safer, and hackers will have a harder time doing things such as injection attacks.
Clearly, the advantages make AOT worth considering. Not to mention that down the road, AOT may be the default choice.
Using AOT with Angular CLI
The easiest way to write Angular applications is to use Angular CLI. A typical Angular project has a lot of moving parts. There are templates, components, directives, and services. There are modules, there are some lazy-loaded modules, some eager-loaded modules, and there are routes. And there's testing infrastructure. And there's more!
You could write some very clever project templates with all of these features, using standard bundle managers such as Webpack. Or you could just trust that Angular CLI has figured all this out for us. It's worth mentioning that if Angular CLI doesn't fit your project needs, you could build an AOT-compliant project template using the @angular/compiler-cli
and @angular/platform-server
node packages.
If you haven't already done so, go ahead and install Angular using the command below. Note that if you're using a Mac, you need to prefix this command with sudo
.
npm install -g @angular/cli
Like any good Samaritan keeping up with constantly changing code, I'd also like to document the specific versions of software that I'm using for this article. It's quite probable that in the future, as AOT becomes the default, some of the steps here will change. You can get all such details by running the following command:
ng --version
My versions are shown in Figure 1.
Once angular CLI is installed, open terminal and run the following command to create a new Angular CLI project:
ng new aottest
Once the project is created and all node packages are installed, run it without AOT using the following command:
ng serve
Now, go ahead and open your browser and point to http://localhost:4200. Also open the dev tools and focus on the network tab. Take note of all the files being downloaded, along with their file sizes. My network tab details look like Figure 2.
Now, serve the same application using AOT with the following command:
ng serve --aot
Note that the application now takes slightly longer to compile. This may even not be noticeable on a very small application on a very fast computer. As the app gets bigger, it does become more apparent. Don't worry, the Angular team is hard at work in making AOT almost as fast as JIT, even for dev purposes.
With the AOT application running, go ahead and load the application again and observe the files and file sizes in the network tab of your browser's debug tools. They should look like Figure 3.
A quick comparison of Figure 2 with Figure 3 shows that the application size - specifically vendor.bundle.js
where all the node packages including Angular are - is almost half the size now. Not only that, the application is now precompiled, so the main.bundle.js
file is larger. And it's running a lot faster, almost twice as fast, with no code rewrite on your part. This is quite incredible: You can double the performance by adding a simple flag? Why don't we always do that?
The answer is: You should! You should only ship AOT code. But frequently, we don't. This is because when working with JIT, we sometimes write bad code that's not AOT-ready. And then, instead of fixing those issues, we ship the JIT versions. You can imagine that this is a bad idea.
In fact, as of January 2017, Angular CLI generates AOT code if you use the --prod option. If you want to revert back to JIT, you can specify the no-aot flag to your compiler. (See https://github.com/angular/angular-cli/pull/4105.)
In the rest of the article, I'll illustrate some of the most common AOT mistakes and their solutions, along with some dos and don'ts, so you can be AOT ready.
Using Exported Functions
For your NgModule to be statically analyzable, you can't use module-level private functions or lambdas. You have to use export
functions instead. For instance, let's say you had declarations for a module being returned as a lambda expression like this:
const declarations = () => [AppComponent]
In the declarations part of the module, you call the function:
@NgModule({
declarations: [declarations()],
.. }]
You would then most likely be met with an error message, such as, ERROR in Error encountered resolving symbol values statically. Reference to a local (non-exported) symbol 'declarations'
.
To rectify this error, change your code to use an export
function instead of a const
lambda expression, like this:
export function declarations() {return [AppComponent];}
Avoid Default Exports; Use Named Exports
Default exports are another common pitfall a lot of developers fall into. Although using default exports is considered completely valid TypeScript syntax, it makes your code not statically analyzable as far as Angular is concerned.
Let's say that you create a component and you choose to export it like this:
@Component({...})
class SomeComponent{}
export default SomeComponent;
You may be thinking that this is a very clever way of exporting your component. After all, if you have one component per file, this should be a very elegant way of exporting components. However, by doing so, you meet with an error message that looks a bit like this:
Can't resolve module ... from path .... Error: can't find symbol undefined exported from module ...
Luckily, the fix for this error is also quite simple. Simply change your code to this:
@Component({...})
export class SomeComponent{}
All Members Accessed from the Template Must be Public
A typical Angular component has a TypeScript
file and an HTML template file. It's quite typical to see data-bound elements in the HTML template. For instance, you could have something like <span>{{heading}}<span>
in the template. The expectation is that there's a variable in your underlying component class called heading
. Whatever value is shown in the span is the value of the heading variable. This also applies to functions and events.
The rule is that all such data-bound variables must be public. They cannot be private or protected.
This error is very surreptitious. The Angular compiler is getting better every day, but at the time of writing this article, this error doesn't show up in JIT code. In fact, the application compiles and even runs without errors.
However, as soon as you try to build the application with AOT, you'll be greeted with an error. For instance, let's say I had a variable called title
in my component that I'm databinding to the template like this:
export class AppComponent {
private title = 'app';
}
Notice in Figure 4 how the error doesn't show up when using JIT, and it does show up when using AOT.
The error reads: Property 'title' is private and only accessible within class 'AppComponent'
.
Although this error is sneaky, the solution is quite simple. You make all members accessed from the template public. That's all you need to do.
Don't Embed Logic in Component Templates
Another way to say this is: Don't use dynamic component templates. Angular's databinding syntax is quite powerful. Some crafty developers would go so far as embedding some simple logic statements that are impossible to be statically analyzable.
For instance, let's say that you have a public variable in your component called someValue
, and you decide to databind it conditionally, as shown here:
<span [ngIf]="someValue === ${someValue}">
Value is truthy
</span>
This looks like pretty nifty TypeScript code. That $ syntax allows you to access logic from within HTML templates, or anywhere you're doing string interpolation. Unfortunately, it also makes the code not statically analyzable. This is another one of those sneaky errors that works with JIT but breaks under AOT.
Luckily, the solution is quite simple. Simply change your template code to:
<span [ngIf]="someValue === 1">
Value is truthy
</span>
You do lose some capabilities here. My code example had a simple variable called someValue
, but you can imagine some clever developers pushing the boundaries of this technique by creating extremely dynamic component templates.
You can't have dynamic templates if you want your code to be AOT compliant.
Export Things Explicitly
This is an error that you'll most likely encounter when creating reusable libraries using Angular. The issue here is that it's quite common for developers to create “barrel” files. This is typically an index.ts
file that sits in a directory. The responsibility of this index.ts
file is to export numerous things from numerous underlying files. It's quite tempting to take a shortcut here, and write code like this:
export * from 'some-file';
First of all, this is bad code to begin with. You should never export "*", just everything, blindly. You should always be cognizant of what you're exporting so you can control the shape of your API.
Fortunately, Angular agrees, and when using AOT, you'll be met by this error message:
Uncaught Error: Unexpected value 'undefined' imported by module
.
The solution to this is very straightforward. Simply spell out everything you wish to export:
export {something} from 'some-file';
Does this mean that things could get very tedious for your library consumers? Luckily, no. Angular allows you to export and import an array. So instead of importing hundreds of components, the library consumer can simply import an array of components.
Additional Tips for Libraries
When using Angular to create reusable libraries, you need to follow some additional tips to keep your code AOT compliant.
- Add Angular as a
peerDependency
- Don't package
*.ngFactory.ts
files in your NP package. This can be achieved in two steps:- Generate
*.ngFactory.ts
files in their own directory. This can be achieved by creating a node calledangularCompilerOptions
in your tsconfig.json, and setting a value for thegendir
attribute. - Exclude that
gendir
from yournpm
package using a.npmignore
file
- Generate
- Follow all the other tips mentioned in this article.
Summary
If you're an Angular developer, AOT must be on your radar. You can't afford to ignore it. Not only are there sizable performance gains, you also end up writing cleaner and better code, and your overall application size reduces.
It's no wonder that the general developer community is leaning toward making sure that your code works with AOT. You should too. This article walked you through some common errors when working with AOT and their resolutions and you can easily make these a part of your coding habits.
More in the next article. Until then, happy coding.