Deep Dive into the Angular Compiler
Alex Rickabaugh
About me

Why does Angular have a compiler at all?
Templates → Code
<user-view [user]="loggedInUser">
</user-view>
// Need to create the element and the component.
const el = document.createElement('userVierw');
const cmp = new UserViewCmp(/* dependency injection? */);
// Initial rendering.
renderComponent(el, cmp);
// Run change detection.
if (ctx.loggedInUser !== oldUser) {
// Binding has changed, need to re-render the view.
cmp.user = ctx.loggedInUser;
oldUser = cmp.user;
ng.updateView(el, cmp);
}
Declarative
Imperative
The Angular Compiler takes declarative Angular syntax
and converts it to imperative code
@Injectable({...})
export class Service {...}
@Component({...})
export class FooCmp {}
<button [tooltip]="'Open the panel'"></button>
Example
@Component({
selector: 'popup-panel',
template: '<h1>{{title}}</h1>',
})
export class PopupPanel {
@Input() title: string;
}
<popup-panel [title]="'Details'">
Example
export class PopupPanel {
title: string;
static ngComponentDef = ng.defineComponent({
selectors: ['popup-panel'],
inputs: {
title: 'title',
},
template: function(rf, ctx) {
if (rf & 1) {
ng.elementStart(0, 'div');
ng.text(1);
ng.elementEnd();
}
if (rf & 2) {
ng.advance(1);
ng.textInterpolate1('', ctx.title, '');
}
}
});
}
JIT
Compile your code with plain TypeScript (tsc)
Decorators (@Component, etc) invoke the compiler and generate Ivy static fields
Truly just in time - compilation happens when the field is read
AOT
ngc does at build time what the decorators would have done at runtime
Avoid the cost of runtime compilation
Topics
Architecture
Compilation Model
Features of the Compiler
Template Type-Checking
Architecture
Program Creation
Type Checking
Emit




Program Creation
Analysis
Resolve
Type Checking
Emit





Architecture

Program Creation
Starts with tsconfig.json
Expands to set of .ts and .d.ts files to compile
Shims (.ngfactory files)





Analysis
Go through your TS code, class by class
Looking for Angular "things"
Try to understand each component/directive/etc in isolation
Gathering information about the structure





Resolve
Look at the whole program at once
Understand structure, dependencies
Make optimization decisions





Type Checking
Validate expressions in component templates
Done using code generation ("type check blocks")
More on this later





Emit
Generate Angular code for each decorated class and "patch" it
The most expensive operation!





Compilation Model
Compilation Model

export class UsefulLib {
doSomethingUseful(): boolean {
console.log('I helped!');
return true;
}
}
"use strict";
Object.defineProperty(exports, "__esModule",
{ value: true });
var UsefulLib = /** @class */ (function () {
function UsefulLib() {
}
UsefulLib.prototype.doSomethingUseful =
function () {
console.log('I helped!');
return true;
};
return UsefulLib;
}());
exports.UsefulLib = UsefulLib;
lib.ts
lib.js
lib.ts
lib.d.ts
Compilation Model

export class UsefulLib {
doSomethingUseful(): boolean {
console.log('I helped!');
return true;
}
}
declare class UsefulLib {
doSomethingUseful(): boolean;
}
app.ts
lib.d.ts
Compilation Model

import {UsefulLib} from 'useful-lib';
...
const o = new UsefulLib(...);
o.doSomethingUseful();
declare class UsefulLib {
doSomethingUseful(): boolean;
}
lib.ts
lib.js
Compilation Model

@Component({
selector: 'useful-cmp',
template: '...',
})
export class UsefulCmp {
@Input() value: string;
}
(Ivy)
var UsefulCmp = /** @class */ (function () {
function UsefulCmp() {
}
UsefulCmp.ngComponentDef =
ng.defineComponent({
selectors: ['useful-cmp'],
template: function () {...},
inputs: {
value: 'value',
},
});
return UsefulCmp;
}());
exports.UsefulCmp = UsefulCmp;
lib.ts
lib.d.ts
Compilation Model

@Component({
selector: 'useful-cmp',
template: '...',
})
export class UsefulCmp {
@Input() value: string;
}
(Ivy)
export declare class UsefulCmp {
value: string;
static ngComponentDef: ng.ComponentDef<
UsefulCmp,
'useful-cmp',
{value: 'value'},
...
>;
}
app.html
lib.d.ts
Compilation Model

<useful-cmp [value]="something">
I'm useful!
</useful-cmp>
(Ivy)
export declare class UsefulCmp {
static ngComponentDef: ng.ComponentDef<
UsefulCmp,
'useful-cmp',
{value: 'value'},
...
>;
}
Features of the Compiler
NgModule scopes
Partial evaluation
Template type-checking
NgModule scopes
app.html
app.js
NgModule scopes
<user-view [user]="currentUser">
</user-view>
export class AppCmp {
static ngComponentDef =
ng.defineComponent({
...,
directives: [???]
});
}
What component is this?
app.module.ts
Compilation Scope
@NgModule({
declarations: [
AppCmp,
UserViewCmp,
],
})
export class AppModule {}
AppModule
AppCmp
UserViewCmp
Compilation scope of AppModule
user.module.ts
Export Scope
@NgModule({
declarations: [UserViewCmp],
exports: [UserViewCmp],
})
export class UserModule {}
UserModule
UserViewCmp
Export scope of UserModule
app.module.ts
Compilation + Export Scopes
@NgModule({
declarations: [AppCmp],
imports: [UserModule],
})
export class AppModule {}
AppModule
AppCmp
UserViewCmp
UserModule
Compilation scope of AppModule
app.html
app.js
Template Scoping
<user-view [user]="currentUser">
</user-view>
import {UserViewCmp} from 'user-lib';
export class AppCmp {
static ngComponentDef = ng.defineComponent({
directives: [UserViewCmp]
})
}
Partial Evaluation
Partial Evaluation
@NgModule({
declarations: [FooCmp, BarCmp],
exports: [FooCmp, BarCmp],
})
export class SomeModule {}
some.module.ts
Partial Evaluation
const COMPONENTS = [FooCmp, BarCmp];
@NgModule({
declarations: [...COMPONENTS],
exports: [...COMPONENTS],
})
export class SomeModule {}
some.module.ts
Partial Evaluation
The Angular compiler will evaluate:
- Property chains (foo.bar.baz)
- Literals (objects, arrays)
- Constants/variables
- Binary/ternary/logic expressions
- String templates
- Function calls
- Imports/Exports
but, only within the current compilation!
Partial Evaluation
import {COMMON_MODULES} from './common';
@NgModule({
declarations: [FooCmp, BarCmp],
exports: [FooCmp, BarCmp],
imports: [...COMMON_MODULES],
})
export class SomeModule {}
some.module.ts
export const COMMON_MODULES = [
CommonModule,
FormsModule,
RouterModule,
];
common.ts
Dynamic Expressions
import {CONFIG} from './config';
@NgModule({
imports: [
CONFIG.modules,
],
})
export class AppModule {}
export const CONFIG = {
modules: [CommonModule, FormsModule],
// not available statically!
viewportSize: {
x: document.body.scrollWidth,
y: document.body.scrollHeight,
},
}
app.module.ts
config.ts
What is the value of CONFIG at compile time?
export const CONFIG = {
modules: [
CommonModule,
FormsModule
],
// not available statically!
viewportSize: {
x: document.body.scrollWidth,
y: document.body.scrollHeight,
},
}
config.ts
CONFIG: {
"modules": [
Reference(CommonModule),
Reference(FormsModule),
],
"viewportSize": {
x: DynamicValue(document.body.scrollWidth),
y: DynamicValue(document.body.scrollHeight),
},
}
Dynamic Expressions
import {CONFIG} from './config';
@Component({
styles: [`
:host {
width: ${CONFIG.viewportSize.x}
}
`]
})
export class AppCmp {}
app.component.ts
styles: [DynamicValue(
from: DynamicValue(
document.body.styleWidth
)
)]
We can produce an error that the 'styles' could not be resolved, and explain why.
Dynamic Expressions
Template
Type-Checking
<account-view
[account]="getAccount(user.id, 'primary') | async">
</account-view>
<account-view
[account]="getAccount(user.id, 'primary') | async">
</account-view>
1. <account-view> should be a component with an 'account' @Input
<account-view
[account]="getAccount(user.id, 'primary') | async">
</account-view>
1. <account-view> should be a component with an 'account' @Input
2. getAccount() should take two arguments and should return an Observable/Promise
<account-view
[account]="getAccount(user.id, 'primary') | async">
</account-view>
1. <account-view> should be a component with an 'account' @Input
2. getAccount() should take two arguments and should return an Observable/Promise
3. 'user' should be an object with an 'id' property
<account-view
[account]="getAccount(user.id, 'primary') | async">
</account-view>
1. <account-view> should be a component with an 'account' @Input
2. getAccount() should take two arguments and should return an Observable/Promise
3. 'user' should be an object with an 'id' property
4. [account] should accept nulls
Approach
1. Transform Angular template expressions into TS code
2. Set up a TypeScript program and feed it these "type check blocks"
3. Gather any errors and map them back to the original template
function typeCheckBlock(ctx: AppComponent) {
}
<account-view
[account]="getAccount(user.id, 'primary') | async">
</account-view>
Type Check Blocks
function typeCheckBlock(ctx: AppComponent) {
let cmp!: AccountViewCmp;
let pipe!: ng.AsyncPipe;
}
<account-view
[account]="getAccount(user.id, 'primary') | async">
</account-view>
Type Check Blocks
function typeCheckBlock(ctx: AppComponent) {
let cmp!: AccountViewCmp;
let pipe!: ng.AsyncPipe;
cmp.account = ...;
}
<account-view
[account]="getAccount(user.id, 'primary') | async">
</account-view>
Type Check Blocks
function typeCheckBlock(ctx: AppComponent) {
let cmp!: AccountViewCmp;
let pipe!: ng.AsyncPipe;
cmp.account = pipe.transform(
ctx.getAccount(ctx.user.id, 'primary'));
}
<account-view
[account]="getAccount(user.id, 'primary') | async">
</account-view>
Type Check Blocks


Template Error Mapping
cmp.account /*273,356*/ = (pipe.transform(
ctx.getAccount(
(ctx.user /*311,315*/).id /*311,318*/,
"primary" /*320,329*/)
/*300,331*/)
/*300,338*/) /*289,339*/;

HTML Templates
What happens if the template is in an external file?
In View Engine...

HTML Templates
What happens if the template is in an external file?
In Ivy...

*ngFor
<div *ngFor="let user of users">
<account-view [account]="getAccount(user.id, 'primary') | async">
</account-view>
</div>
*ngFor
NgFor is generic:
@Directive({
selector: '[ngForOf]'
})
export class NgFor<T> {
@Input() ngForOf: T[];
}
*ngFor
How do we determine the type of the directive instance
NgFor<?>
What type is 'user'?
<div *ngFor="let user of users">
<account-view [account]="getAccount(user.id, 'primary') | async">
</account-view>
</div>
function typeCheckBlock(ctx: AppComponent) {
let ngFor!: NgFor<?>;
let user!: ?;
}
Type Constructors
"Type constructors" for directives set the type based on inputs:
function NgFor_Type<T>(ngForOf: T[]): NgFor<T> {...}
function typeCheckBlock(ctx: AppComponent) {
// NgForOf<User>
let ngFor = NgFor_Type(ctx.users);
}
Template Context Type
export interface NgForContext<T> {
$implicit: T;
}
@Directive(...)
export class NgFor<T> {
constructor(
private template: TemplateRef,
private vcr: ViewContainerRef) {}
renderRow(value: T) {
this.vcr.createEmbeddedView(
this.template,
{$implicit: value},
);
}
}
<account-view ngFor [ngForOf]="users" let-user="$implicit">
Template Context Type
export interface NgForContext<T> {
$implicit: T;
}
@Directive(...)
export class NgFor<T> {
...
static ngTemplateContext<T>(dir: NgFor<T>): NgForContext<T>;
}
<account-view ngFor [ngForOf]="users" let-user="$implicit">
*ngFor Type Checking
function NgFor_Type<T>(ngForOf: T[]): NgFor<T> {...}
function typeCheckBlock(ctx: AppComponent) {
// NgFor<User>
let ngFor = NgFor_Type(ctx.users);
// NgForContext<T>
let ngForCtx = NgFor.ngTemplateContext(ngFor);
// User
let user = ngForCtx.$implicit;
...
}
*ngFor Type Checking

Special Thanks
Joost Koehoorn

Pete Bacon Darwin
Georgios Kalpakas


Also see:


Kara Erickson: How Angular Works
Tomorrow, 1:35pm
Track 1
Misko Hevery: How we make Angular fast
Today, 2:10pm
Track 1
Thank you!
https://bit.ly/2kp1uIl
@synalx

Deep Dive into the Angular Compiler
By Alex Rickabaugh
Deep Dive into the Angular Compiler
- 2,772