Ahead-of-time complilation uses the declarations and annotations to pre-compile the templates in your application into the JavaScript instead transformation them at runtime. This allows your applications to load faster and requires less of the Angular framework to be bundled with your application.
To accomplish this the annotations that define your components and directives need to be extracted from your source code in a way that the compiler can digest it. This document describes how this is done and describes the subset of JavaScript that can be used in annotations that are AoT compliant.
A typical component declaration might look like the following:
@Component({
selector: 'typical',
template: '<div>A typical component for {{data.name}}</div>'
})
class TypicalComponent {
@Input() data: MyData;
}
When this component is used, instances of this component are created by calling
the factory for TypicalComponent
. In JIT mode the factory function is created
at runtime when application (or lazy loaded module) that uses the component is
loaded in the page.
The Component
annotation records the selector
and template
specified for
the TypicalComponent
using the Reflect API. The JIT compiler will
consults the information recorded using Reflect
to produce the factory
function.
This inforation is used by the compiler to generate a factory function that creates a constructor function that includes code like:
self._el_0 = self.renderer.createElement(parentRenderNode,'div',null);
self._text_1 = self.renderer.createText(self._el_0,'',null);
self._expr_0 = jit_UNINITIALIZED3;
which creates the div
and text node that are included in the template.
It also generates detect changes function detects if the expression
data.name
changes and updates the elements, in place.
var currVal_0 = jit_interpolate4(1,'A typical component for ',self.context.data.name,'');
if (jit_checkBinding5(throwOnChange,self._expr_0,currVal_0)) {
self.renderer.setText(self._text_1,currVal_0);
self._expr_0 = currVal_0;
}
Whenever instances of TypicalComponent
are needed, this factory function is
called to create one. This factory function does not change during the life of
your application but you page load will wait until it is created (and all the
other components referenced by the application are created) to being to render
the content of the page.
Ahead-of-time compilation runs the compiler when bundling the application, instead of when the page loads.
When using AoT, the Reflect API is not available and even if it was,
running the code to populate Reflect
above could have side-effects or rely
elsewhere in your application that would only run correctly in a web page. What
is needed is a way to record what is in the annotation without having to
execute it. This is where Angular metadata comes in.
Part of what ngc
does when it translates your modules into JavaScript is to
create a corresponding .metadata.json
file for each .d.ts
file. The
.metadata.json
file contains the information recorded in the Angular
decorators so the same information available at runtime is avaiable when
building the application. The annotations are translated into JSON
format of an abstract syntax tree (AST). The schema for the AST is
described in TypeScript interfaces in Angular's schema.ts.
The .metadata.json
does not try to interpret the meaning of the attribute, it
just records a representation of the source. The interpretation is done by the
StaticReflector
as part of ngc
discussed below. The
StaticReflector
places additional restrictions on what
the can be interpreted.
The metadata supports a wide subset of JavaScript, but it is a subset. Not
everything that can be supported by JIT can be supported by AoT. The
restrictions mainly come from what can be represented in the .metadata.json
file and what can be interpreted by the StaticReflector
.
In general, the restrictions are,
- The metadata must be an expression in the supported JavaScript subset.
- All unfolded reference must be to export symbol.
- Only calls of functions that are supported by the
StaticReflector
.
It is important to note that more can be encoded by the collector than can be
interpreted by the StaticReflector
. This is especially true for call and new
expressions. Only a small subset of functions and classes can be called. This
is enumerated in more detail below is the section about the StaticReflector
.
The subset of JavaScript supported by the collector includes:
Syntax | Example |
---|---|
Literal object | {cherry: true, apple: true, mincemeat: false} |
Literal array | ['cherries', 'flour', 'sugar'] |
Spread in literal array | ['apples', 'flour', ...the_rest] |
Calls | bake(ingredients) |
New | new Oven() |
Property access | pie.slice |
Array index | ingredients[0] |
Identifier reference | Component |
A template string | `pie is ${multiplier} times better than cake` |
Literal string | 'pi' |
Literal number | 3.14153265 |
Literal boolean | true |
Literal null | null |
Supported prefix operator | !cake |
Supported Binary operator | a + b |
Conditional operator | a ? b : c |
Parenthisis | (a + b) |
If the expression uses syntax that is not supported then an error symbol will
be written to the metadata. The StaticReflector
will report the error if the
metadata is used to interpret the content of an annotation.
If the metadata generated for a class would contain an error symbol then ngc
can report that as an error instead of generating an potentially unusable
.json.metadata
file. See the strictMetadataEmit
option for ngc
. This is
especially useful when building Angular libraries and is used to produce the
.metadata.json
files shipped as part of Angular.
Notable ommisions from this list include function expressions and lambdas. Including a function expression or a lambda in an annotation will report an error reqesting the lambda or function expression be turned into an exported function. For example, if you have the component annotation:
@Component({
...
providers: [{provide: server, useFactory: () => new Server()}]
})
the collector will generate an error node in place of the lambda and, when
interpreted by the StaticReflector
will generate an error. To avoid this
you can convert this to:
export function serverFactory() {
return new Server();
}
@Component({
...
providers: [{provide: server, useFactory: serverFactory}]
})
Even though function calls (including using new
) can be encoded by the
collector, only a small number of functions are supported by the
StaticReflector
.
If the static reflector trys to interpret a function it does not recognize an error will be produced.
The full list of supported functions and classes are listed below.
Only references to exported symbols can be resolved by the StaticReflector
but the collector allows limited use of non-exported symbols through folding.
A folded expression is one that is evaluated during collection and the
evaluated result is encoded instead of the expression's AST. For example,
expression 1 + 2 + 3 + 4
is foldable into 10
. In addition, references to
module local const
declarations and initialized var
and let
declarations
are foldable. For example, the following class,
const template = '<div>{{hero.name}}</div>';
@Component({
selector: 'hero-name',
template: template
})
class HeroComponent {
@Input() hero: Hero;
}
is collectable because the module local reference to template
can be
substituted for its value and it is processed as if it was:
@Component({
selector: 'hero-name',
template: '<div>{{hero.name}}</div>'
})
class HeroComponent {
@Input() hero: Hero;
}
Expressions can be folded:
const template = '<div>{{hero.name}}</div>';
@Component({
selector: 'hero-name',
template: template + '<div>{{hero.title}}</div>'
})
class HeroComponent {
@Input() hero: Hero;
}
and the template value would be emitted as
'<div>{{hero.name}}</div><div>{{hero.title}}</div>'
.
The following table describes what expressions that are supported by the collector can be folded:
Syntax | Foldable |
---|---|
Litearl object | yes |
Literal array | yes |
Spread in literal array | no |
Calls | no |
New | no |
Property access | yes if target is foldable |
Array index | yes if target and index are foldable |
Identifier reference | yes if it is a reference to a local |
A template with no substitutions | yes |
A template with substitutions | yes if the substitions are foldable |
Literal string | yes |
Literal number | yes |
Literal boolean | yes |
Literal null | yes |
Supported prefix operator | yes if operand is foldable |
Supported Binary operator | yes if both left and right are foldable |
Conditional operator | yes if condition is foldable |
Parenthisis | yes if the expression is foldable |
It an expression is not foldable it is written out as an AST and it is
left for the StaticReflector
to resolve.
At runtime, during JIT compile, the compiler will use the
Reflect API to retrieve the information supplied in the Angular
annotations. In particular it uses the Reflector
which is an
abstraction over the Reflect API.
During AoT the compiler uses the StaticReflector
which is
an implementation of Reflector
that uses the information recorded in
.metadata.json
files and TypeScript instead of the Reflect API.
The static reflector can evaluate all the syntax forms supported by the
collector but it can only support calls and new
expressions referencing a
strict subset of symbols that the StaticReflector
has built-in support for.
Symbol | Module |
---|---|
OpaqueToken |
@angular/core |
Symbol | Module |
---|---|
Attribute |
@angular/core |
Component |
@angular/core |
ContentChild |
@angular/core |
ContentChildren |
@angular/core |
Directive |
@angular/core |
Host |
@angular/core |
HostBinding |
@angular/core |
HostListener |
@angular/core |
Inject |
@angular/core |
Injectable |
@angular/core |
Input |
@angular/core |
NgModule |
@angular/core |
Optional |
@angular/core |
Output |
@angular/core |
Pipe |
@angular/core |
Self |
@angular/core |
SkipSelf |
@angular/core |
ViewChild |
@angular/core |
Symbol | Module |
---|---|
trigger |
@angular/core |
state |
@angular/core |
transition |
@angular/core |
style |
@angular/core |
animate |
@angular/core |
keyframes |
@angular/core |
sequence |
@angular/core |
groups |
@angular/core |
The StaticReflector
also support calling macros which are functions or static
methods that return an expression.
For example, consider the function,
export function wrapInArray<T>(value: T): T[] {
return [value];
}
wrapInArray
can be called in an annotation because it only returns a value
and the expression returned conforms to the supported JavaScript subset. An
example of this might be
@NgModule({
declarations: wrapInArray(TypicalComponent)
})
class TypicalModule {}
This is treated as if it was identical to:
@NgModule({
declarations: [TypicalComponent]
})
class TypicalModule {}
For example, the Router module exports macro static methods that help declare root and child routes.
The collector is simplistic in its determination of what qualifies as a macro
function; it can only contain a single return
statement.
thanks @chuckjaz i found this very useful!