Deep Dive into the Angular Compiler

Alex Rickabaugh

About me

Software Engineer @ Google


Working on Angular Compiler update to Ivy 🍃

 

5th year at Angular Connect!

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,556