Menu

Under the Hood of the Language Service

February 4th, 2021

3 pillars of the language service: Performance, Accuracy and IDE Features

State of the Language Service

The Angular Language Service was created back in 2016 when the ecosystem for editor experience (VS Code + TypeScript) was still in its infancy. In order to provide rich language features for Angular users, the Angular team had to overcome quite a few low-level architectural challenges. The View Engine compiler had to be adapted to the language service use case, which was not a design goal when the compiler was conceived. Since then, we have made tremendous progress in bringing more features and improved stability to the language service. However, there are fundamental problems like incremental compilation performance and more robust type checking. These are impossible to solve without overhauling the compiler. We realized this early on and devised a roadmap to pave the way for a brand new editor experience integrated with the Ivy compiler.

How to Get Started

Here’s how you can opt-in to the Ivy Language Service beta:

  • Install the latest extension in the VSCode Marketplace
  • Set strictTemplates: true in tsconfig.json Angular compiler options
  • (might cause diagnostics to appear if project is not strictTemplates compatible)
  • Open an Angular project in Visual Studio Code
  • Go to Preferences -> Settings -> Extensions -> Angular Language Service -> Check “Experimental Ivy”

Compiler-based Architecture

The Ivy powered Angular Language Service features better performance because it is built from the ground up for incremental builds. It is a complete rewrite of the previous View Engine-based language service that leverages the Ivy compiler.

Central to the new language service’s design is deep integration with the Angular Compiler, which is an architectural pattern used in many modern language services. Our Compiler already has a thorough understanding of Angular code and we’ve been able to leverage its capabilities. In some cases, we’ve been able to expand them — to deliver a faster, more accurate, and more powerful language service experience.

Incremental Build performance

Have you ever noticed that running `ng build` after making a change takes a while, but `ng serve` is able to rebuild the app much faster? This is due to the compiler’s support for incremental compilation. When you first run `ng serve`, the compiler reads your code and gathers information about each component and its template. Once it’s finished building the application, the compiler doesn’t shut down and forget all of this information — it stays loaded and waits until a change is made to the application. When this happens, the compiler looks at the change and determines which components might have been affected. It’s able to quickly decide which components need to be recompiled and which ones it can reuse prior work for.

The Angular Language Service pushes incremental compilation to its limits. While ng serve compiles code each time you save, the Language Service is often recompiling over and over as you type. As part of integrating the compiler into the language service, we are able to take advantage of improvements in Ivy’s incremental compilation to deliver a faster, more fluid IDE experience.

Investing in compiler optimizations for the Angular Language Service can improve compilation performance in general, too. For example, the language service is primarily concerned with changes happening inside Angular templates. These changes have a unique property: changing a component’s template does not affect any other component. To ensure the best performance in the IDE, we’re leveraging this property to minimize the amount of work the compiler has to do when reacting to a template-only change. The impact of this optimization is not limited to the Angular Language Service. For example, ng serve will also get faster when recompiling after template-only changes.

Sharing the Template Engine

One of the Angular compiler’s toughest challenges is performing type-checking of Angular templates. TypeScript has a complex and continuously improving type system, and duplicating that functionality for templates would be a prohibitive amount of work. Instead, the compiler does something clever: behind the scenes, it generates TypeScript code for each template, and then it asks TypeScript to check the code for errors. This generated code is called a “type-check block”, or TCB. Errors that TypeScript finds in the TCB indicate real problems in the template.

Consider a template for a UserComponent, which shows a user’s name and address:

<p>Name: {{user.name}}</p>
<address-view [address]=”user.address”></address-view>

A simplified TCB for this component might look like:

function UserComponent_TCB(ctx: UserComponent) {
    ‘’ + ctx.user.name;
    let c0: AddressView = (null!);
    c0.address = ctx.user.address;
}

Each piece of the TCB represents a binding or expression present in the template:

function UserComponent_TCB(ctx: UserComponent) {
    ‘’ + ctx.user.name; // {{user.name}}
    let c0: AddressView = (null!); // <address-view> component
    c0.address = ctx.user.address; // [address]=”user.address”
}

When redesigning the Language Service, we realized we could leverage these TCBs not just for type-checking, but to implement many of the Language Service’s features as well. For example, suppose you’re typing in the [address] binding and want Language Service to autocomplete for you. You might write:

<address-view [address]=”user.”></address-view>

Language Service should be able to autocomplete the properties of the user object here for you. To do this, we translate the incomplete expression into a TCB:

c0.address = ctx.user.;

By asking TypeScript to autocomplete this expression in the TCB, the Language Service can provide accurate results for the expression within the template itself.

Advanced Features

Leveraging the compiler in this way paves the road for some long-requested Language Service features. We’ve been focused on achieving feature parity with the previous Language Service engine, but the 11.1 release contains a great new feature: Find References.

By taking advantage of the TCB engine in the compiler we are now able to search for references across both TypeScript and templates. From within a template, Find References can locate declarations or other usages of any value in an expression. This works both in the template and in the application’s TypeScript code. Find References also works in the other direction: template references will show up when searching for references of a symbol in TypeScript code.

We’re excited about the possibilities that our new approach creates. We believe that the leveraging integration of the Angular compiler in the Angular Language Service will lead to more new features in the future.

Experimental release in 11.1

Angular 11.1 releases a beta version of the Ivy Language Service. It gives Angular developers improved performance and more accurate diagnostics. It also makes a handful of refinements such as supporting generic pipes, structural directives, and directives with compound selectors.

As mentioned above, this release adds a major feature to the language service — Find References. This is the first major feature in the new ivy language service, and there will be more to come down the road.

Want to see the Angular Language Service in action? Check out this video for a walkthrough:

What’s Next

We are going to continue to improve and refine the Angular Language Service. We want to create the best developer experience possible.. Today, the Ivy Language Service is opt-in, and in the future it will be the default language service for new projects.

The original post Under the Hood of the Language Service.