Improving Angular 2 Load Times and a 29KB Hello World App

At the beginning of this year, Lucidchart rebuilt its editor in Angular 2. The new editor delivered a better experience with far fewer lines of code. Both engineers and product managers enjoyed it because it made writing features easier.

The old (left) and new (right) Lucidchart editor

The old (left) and new (right) Lucidchart editor

But, there was a big problem: load times. The Angular 2 version consistently took several seconds longer to load than the non-Angular version. Over the summer, we worked with the Angular team to improve our load times. The end result of these efforts is that our new editor loads five seconds faster than it did before and one second faster than our old editor. We even created a 29KB Hello World Angular 2 app along the way.

The Problem

When we released the beta of the Angular 2 version of our editor, we consistently saw load times about four seconds slower than our old editor.

Load times before improvement

Load times for our old editor (purple) vs our new editor (blue) for the week of July 10th.

While analyzing our load times, we found that a large amount of time was being spent bootstrapping the Angular app with the runtime template compiler. We saw mentions of an offline compiler, but it did not appear to be available or documented yet. In an attempt to improve our load times, we reached out to the Angular team in March about Ahead of Time (AoT) compilation.

They kept us up to date on the progress of the AoT compiler and informed us that it only worked with TypeScript. Problem was, we were using the JavaScript version of Angular 2 in concert with the Google Closure Compiler to typecheck and minify our JavaScript.

When we mentioned to the Angular team our inability to use TypeScript due to the Closure Compiler, they suggested two tools they built at Google to solve this same problem: Tsickle and Clutz.

The Solution

The Angular team built two tools to use TypeScript with the Closure Compiler: Tsickle and Clutz. Tsickle produces Closure annotated JavaScript from TypeScript, and Clutz produces TypeScript definition files from Closure annotated JavaScript. Together, they enable you to use TypeScript in a Closure compatible codebase, and vice versa.

At Lucid, we modified Angular 2, its dependencies, and the AoT compiler in order to produce Closure compatible JavaScript from Angular 2 TypeScript. The end result is a five second improvement in load time for our new editor.

Load times before and after improvement

The above figure shows load times from May to September for our old editor (purple) and our new editor (blue). We switched from JavaScript to TypeScript with simple optimizations from the Closure compiler in the middle of August, which is when we see the first dip in load times. The second dip comes at the end of August, when we got Closure’s advanced optimizations working with TypeScript.

Currently, we see load times about five seconds lower, on average, with AoT compilation than with runtime compilation. The new load times are even about one second faster, on average, than our old editor.

Load times after improvement

Load times for our old editor (purple) vs our new editor (blue) for the week of Aug 29th.

When we compare flame graphs collected while loading our new editor with AoT and runtime compilation, we see a large difference. The biggest difference is, as expected, between the Angular 2 bootstrap and the first Angular 2 tick. The ahead of time compiler cuts out a large amount of template parsing and compilation that otherwise happens at runtime. Accordingly, things are much faster. Fair warning, the scales on these graphs are not equal.

Runtime compiler flame graph

Runtime compiler flame graph

AoT compiler flame graph

AoT compiler flame graph

How We Did It

In this section and the next, we discuss what we did to make this happen and provide an example, in case you would like to reproduce our efforts.

For those who want to skip straight to the example, you can find it here: https://github.com/lucidsoftware/closure-typescript-example

Tsickle

Tsickle is a project from the Angular team that produces Closure compatible JavaScript from TypeScript. It wraps the TypeScript Compiler and uses the TypeScript Compiler API to add Closure compatible annotations to TypeScript, compiles TypeScript to JavaScript, and converts CommonJS modules to goog.modules.

You can use Tsickle to create TypeScript dependencies in your Closure JavaScript codebase.

Tsickle transforms something like this:

import Statement from 'goog:Statement';

export class Greeter {
    constructor(public statement: Statement) {}
    greet(): string {
        return this.statement.getStatement();
    }
};

Into this:

goog.module('js.app.ts.greeter');var module = module || {id: 'js/app/ts/greeter.js'};

class Greeter {
    /**
     * @param {?} statement
     */
    constructor(statement) {
        this.statement = statement;
    }
    /**
     * @return {?}
     */
    greet() {
        return this.statement.getStatement();
    }
    static _tsickle_typeAnnotationsHelper() {
        /** @type {?} */
        Greeter.prototype.statement;
    }
}

exports.Greeter = Greeter;
;
//# sourceMappingURL=greeter.js.map

Clutz

Clutz is a project from the Angular team that produces TypeScript definition files from Closure compatible Javascript. It uses the Closure Compiler’s API mode and performs a full closure compilation in order to produce a definition file that lets you use all your existing Closure JavaScript in a TypeScript project.

Symbols or modules from your Closure JavaScript are available in TypeScript under the “goog:” prefix. For example, if you had “goog.provide(‘lucid.model.CustomShape’);” in your JavaScript, you could import it in TypeScript using “import CustomShapeModel from ‘goog:lucid.model.CustomShape’;”

Clutz can be used to turn something like this:

goog.provide('Statement');

/**
 * @constructor
 * @param {string} statement
 */
var Statement = function(statement) {
    /** @private {string} */
    this.statement = statement;
}

/**
 * @return {string}
 */
Statement.prototype.getStatement = function() {
    return this.statement;
}

/**
 * @param {string} statement
 * @return {string}
 */
Statement.prototype.setStatement = function(statement) {
    this.statement = statement;
}

Into this:

declare namespace ಠ_ಠ.clutz {

    class Statement extends Statement_Instance {
    }

    class Statement_Instance {
      private noStructuralTyping_: any;
      constructor (statement : string ) ;
      getStatement ( ) : string ;
      setStatement (statement : string ) : string ;
    }
}

Putting it All Together

Using Tsickle and Clutz, we modified our build system to support TypeScript dependencies in JavaScript and vice versa. It works like this: TypeScript and JavaScript build targets can mark other build targets as dependencies. Those build targets are then built, using either Tsickle or Clutz, depending on the type of dependency.

Example application using Tsickle and Clutz

Example application using Tsickle and Clutz

In order to support Angular 2 generating Closure compatible JavaScript with AoT compilation, we modified Angular 2 and RxJS. We built a custom version of both projects and currently use them in our build process. If you would like to know more about the modifications we made to get this working, please see this GitHub issue.

The 29 KB Hello World App

Lucid built a proof of concept Hello World Angular 2 application using Tsickle and Clutz before we tried implementing the same thing in our large JavaScript codebase. You can find the example on GitHub at https://github.com/lucidsoftware/closure-typescript-example.

The purpose of the project is to provide an example of Tsickle, Clutz, and Angular 2 working together. Things are built from source, so you can see how we go from modified Angular 2 all the way to the final product. Comments, questions, and pull requests are welcome.

When we compress the resulting bundle using Brotli, the final bundle size is right around 29KB. gzip is not far behind:

$ ll --block-size=KB
-rw-rw-r-- 1 james james 115kB Sep 27 02:45 main.js
-rw------- 1 james james 29kB  Sep 27 02:45 main.js.brotli-11
-rw-rw-r-- 1 james james 35kB  Sep 27 02:45 main.js.gz

How to Run the Example

More information can be found in the example’s README, but to run the example, either:

1. Docker

docker pull jjudd/closure-typescript-example
docker run -t -i -p 8000:8000 --net=host jjudd/closure-typescript-example
localhost:8000/index.html?compiled=1

2. Build it Yourself

make run

Note: You will need to make sure you have all the dependencies installed for this to work. It has only been tested on Ubuntu 14.04.

27 Comments

  1. […] Source: Improving Angular 2 Load Times and a 29KB Hello World App – Lucidchart […]

  2. I especially like the way you have named a variable:

    declare namespace ಠ_ಠ.clutz {
    // …
    }

    Awesome job! Keep it up guys = )

  3. […] most performance issues come from the initial parsing time to bootstrap the app. Also, use an ahead-of-time compiler36 to offload some of the client-side rendering37 to the server38 and, hence, output usable results […]

  4. […] most performance issues come from the initial parsing time to bootstrap the app. Also, use an ahead-of-time compiler36 to offload some of the client-side rendering37 to the server38 and, hence, output usable results […]

  5. […] most performance issues come from the initial parsing time to bootstrap the app. Also, use an ahead-of-time compiler38 to offload some of the client-side rendering39 to the server40 and, hence, output usable results […]

  6. […] most performance issues come from the initial parsing time to bootstrap the app. Also, use an ahead-of-time compiler38 to offload some of the client-side rendering39 to the server40 and, hence, output usable results […]

  7. […] 应用一些为初始化时间提速的技术,如tree-shaking和code-splitting。还可以使用一个前置编译器来通过服务端降低客户端渲染的耗时,尽快显示有意义的内容。 […]

  8. […] most performance issues come from the initial parsing time to bootstrap the app. Also, use an ahead-of-time compiler38 to offload some of the client-side rendering39 to the server40 and, hence, output usable results […]

  9. […] most performance issues come from the initial parsing time to bootstrap the app. Also, use an ahead-of-time compiler38 to offload some of the client-side rendering39 to the server40 and, hence, output usable results […]

  10. […] most performance issues come from the initial parsing time to bootstrap the app. Also, use an ahead-of-time compiler38 to offload some of the client-side rendering39 to the server40 and, hence, output usable results […]

  11. […] 提前编译 ,从而减轻部分 客户端的渲染过程 ,从而快速输出结果。最后,考虑使用 […]

  12. What do you use to track your load times?

  13. @Joshua – We track our load time information using the Performance Timing and User Timing APIs: https://developer.mozilla.org/en-US/docs/Web/API/PerformanceTiming and https://developer.mozilla.org/en-US/docs/Web/API/User_Timing_API

    Metrics are recorded on page load and sent to Datadog for aggregation and visualization.

  14. […] 在一些应用中,可以在渲染页面前先初始化应用。最好先显示框架,而不是一个进度条或指示器。使用可以加速初始渲染时间的模块或技术(例如tree-shaking和code-splitting),因为大部分性能问题来自于应用引导程序的初始分析时间。还可以在服务器上提前编译,从而减轻部分客户端的渲染过程,从而快速输出结果。最后,考虑使用Optimize.js来加快初始加载速度,它的原理是包装优先级高的调用函数(虽然现在已经没什么必要了)。 […]

  15. […] you using an ahead-of-time compiler?Use an ahead-of-time compiler to offload some of the client-side rendering to the server and, hence, output usable results […]

  16. […] you using an ahead-of-time compiler?Use an ahead-of-time compiler to offload some of the client-side rendering to the server and, hence, output usable results […]

  17. […] you using an ahead-of-time compiler?Use an ahead-of-time compiler to offload some of the client-side rendering to the server and, hence, output usable results […]

  18. […] you using an ahead-of-time compiler?Use an ahead-of-time compiler to offload some of the client-side rendering to the server and, hence, output usable results […]

  19. […] you using an ahead-of-time compiler?Use an ahead-of-time compiler to offload some of the client-side rendering to the server and, hence, output usable results […]

  20. […] 在一些應用中,可以在渲染頁面前先初始化應用。最好先顯示框架,而不是一個進度條或指示器。使用可以加速初始渲染時間的模組或技術(例如tree-shaking和code-splitting),因為大部分效能問題來自於應用載入程式的初始分析時間。還可以在伺服器上提前編譯,從而減輕部分客戶端的渲染過程,從而快速輸出結果。最後,考慮使用Optimize.js來加快初始載入速度,它的原理是包裝優先順序高的呼叫函式(雖然現在已經沒什麼必要了)。 […]

  21. […] 在一些应用中,可以在渲染页面前先初始化应用。最好先显示框架,而不是一个进度条或指示器。使用可以加速初始渲染时间的模块或技术(例如tree-shaking和code-splitting),因为大部分性能问题来自于应用引导程序的初始分析时间。还可以在服务器上提前编译,从而减轻部分客户端的渲染过程,从而快速输出结果。最后,考虑使用Optimize.js来加快初始加载速度,它的原理是包装优先级高的调用函数(虽然现在已经没什么必要了)。 […]

  22. […] 在一些应用中,可以在渲染页面前先初始化应用。最好先显示框架,而不是一个进度条或指示器。使用可以加速初始渲染时间的模块或技术(例如tree-shaking和code-splitting),因为大部分性能问题来自于应用引导程序的初始分析时间。还可以在服务器上提前编译,从而减轻部分客户端的渲染过程,从而快速输出结果。最后,考虑使用Optimize.js来加快初始加载速度,它的原理是包装优先级高的调用函数(虽然现在已经没什么必要了)。 […]

  23. […] you using an ahead-of-time compiler?Use an ahead-of-time compiler to offload some of the client-side rendering to the server and, hence, output usable results […]

  24. […] you using an ahead-of-time compiler?Use an ahead-of-time compiler to offload some of the client-side rendering to the server and, hence, output usable results […]

  25. […] you using an ahead-of-time compiler?Use an ahead-of-time compiler to offload some of the client-side rendering to the server and, hence, output usable results […]

  26. […] you using an ahead-of-time compiler?Use an ahead-of-time compiler to offload some of the client-side rendering to the server and, hence, output usable results […]

  27. […] you using an ahead-of-time compiler?Use an ahead-of-time compiler to offload some of the client-side rendering to the server and, hence, output usable results […]

Your email address will not be published.