angular-cn/TOOLS_DART.md

13 KiB

Developer Tools for Dart

Use these tools and techniques to increase your app's performance and reliability.

Angular debugging tools

Starting with alpha.38, Angular provides a set of debugging tools that are accessible from any browser's developer console. In Chrome, you can get to the dev console by pressing Ctrl + Shift + J (on Mac: Cmd + Opt + J).

Enabling the debugging tools

By default the debugging tools are disabled. Enable the debugging tools as follows:

import 'package:angular2/tools.dart';

main() async {
  var appRef = await bootstrap(Application);
  enableDebugTools(appRef);
}

Using the debugging tools

In the browser, open the dev console. The top-level object is called ng and contains more specific tools inside it.

For example, to run the change detection profiler on your app:

// In the dev console:
ng.profiler.timeChangeDetection();

The Change detection profiler section has more details.

Code size

Code must be downloaded, parsed, and executed. Too much code can lead to slow application start-up time, especially on slow networks and low-end devices. The tools and techniques in this section can help you to identify unnecessarily large code and to reduce code size.

Finding contributors to code size

Options for investigating code size include the --dump-info dart2js option, ng2soyc, reflector.trackUsage(), and code coverage information from the Dart VM.

Use --dump-info

The --dump-info option of dart2js outputs information about what happened during compilation. You can specify --dump-info in pubspec.yaml:

transformers:
...
- $dart2js:
    commandLineOptions:
    - --dump-info

The Dump Info Visualizer can help you analyze the output. For more information, see the dart2js_info API reference.

Use ng2soyc.dart

ng2soyc is a utility for analyzing code size contributors in Angular 2 applications. It groups code size by library and, assuming your library names follow standard naming conventions (package.library.sublibrary...), gives the code size breakdown at each level. To reduce noise in the output of very large apps, ng2soyc provides an option to hide libraries that are too small, so you can focus on the biggest contributors.

Find unused reflection data

Your app might have types that are annotated with @Component or @Injectable but never used. To find these unused types, use reflector.trackUsage() and then, after exercising your app, reflector.listUnusedKeys(). For example:

import 'package:angular2/src/core/reflection/reflection.dart';
...
main() async {
  reflector.trackUsage();
  await bootstrap(AppComponent);
  print('Unused keys: ${reflector.listUnusedKeys()}');
}

When you run that code (in Dartium or another browser), you'll see a list of types that Angular can inject but hasn't needed to. Consider removing those types or their @Component/@Injectable annotation to decrease your app's code size.

Three conditions must be true for listUnusedKeys() to return helpful data:

  1. The angular2 transformer must run on the app.
  2. If you're running a JavaScript version of the app, the app must not be minified, so that the names are readable.
  3. You must exercise your app in as many ways as possible before calling listUnusedKeys(). Otherwise, you might get false positives: keys that haven't been used only because you didn't exercise the relevant feature of the app.

To run the angular2 transformer, first specify it in pubspec.yaml:

name: hello_world
...
transformers:
- angular2:
    entry_points: web/main.dart

Then use pub to run the transformer. If you use pub serve, it provides both Dart and unminified (by default) JavaScript versions. If you want to serve actual files, then use pub build in debug mode to generate Dart and unminified JavaScript files: pub build --mode=debug.

The reflector.trackUsage() method makes Angular track the reflection information used by the app. Reflection information (ReflectionInfo) is a data structure that stores information that Angular uses for locating DI factories and for generating change detectors and other code related to a given type.

Use code coverage to find dead code

When running in Dartium (or in the Dart VM, in general) you can request code coverage information from the VM. You can either use observatory or download the coverage file and use your own tools to inspect it. Lines of code that are not covered are top candidates for dead code.

Keep in mind, however, that uncovered code is not sufficient evidence of dead code, only necessary evidence. It is perfectly possible that you simply didn't exercise your application in a way that triggers the execution of uncovered code. A common example is error handling code. Just because your testing never encountered an error does not mean the error won't happen in production. You therefore don't have to rush and remove all the catch blocks.

Reducing code size

To reduce code size, you can disable reflection, enable minification, and manually remove dead code. You can also try less safe options such as telling dart2js to trust type annotations.

Disable reflection

dart:mirrors allows discovering program metadata at runtime. However, this means that dart2js needs to retain that metadata and thus increase the size of resulting JS output. In practice, however, it is possible to extract most metadata necessary for your metaprogramming tasks statically using a transformer and package:analyzer, and act on it before compiling to JS.

Enable minification

Minification shortens all your longMethodNames into 2- or 3-letter long symbols. dart2js ensures that this kind of renaming is done safely, without breaking the functionality of your programs. You can enable it in pubspec.yaml under $dart2js transformer:

transformers:
...
- $dart2js:
    minify: true

Manually remove dead code

dart2js comes with dead code elimination out-of-the-box. However, it may not always be able to tell if a piece of code could be used. Consider the following example:

/// This function decides which serialization format to use
void setupSerializers() {
  if (server.doYouSupportProtocolBuffers()) {
    useProtobufSerializers();
  } else {
    useJsonSerializers();
  }
}

In this example the application asks the server what kind of serialization format it uses and dynamically chooses one or the other. dart2js can't tell whether the server responds with yes or no, so it must retain both kinds of serializers. However, if you know that your server supports protocol buffers, you can remove that if block entirely and default to protocol buffers.

Code coverage (see above) is a good way to find dead code in your app.

Unsafe options

Dart also provides more aggressive optimization options. However, you have to be careful when using them and as of today the benefits aren't that clear. If your type annotations are inaccurate you may end up with non-Darty runtime behavior, including the classic "undefined is not a function" tautology, as well as the "keep on truckin'" behavior, e.g. null + 1 == 1 and {} + [] == 0.

--trust-type-annotations tells dart2js to trust that your type annotations are correct. So if you have a function foo(Bar bar) the compiler can omit the check that bar is truly Bar when calling methods on it.

--trust-primitives tells dart2js that primitive types, such as numbers and booleans are never null when performing arithmetic, and that your program does not run into range error when operating on lists, letting the compiler remove some of the error checking code.

Specify these options in pubspec.yaml.

Example:

transformers:
...
- $dart2js:
    commandLineOptions:
    - --trust-type-annotations
    - --trust-primitives

Performance

Change detection profiler

If your application is janky (it misses frames) or is slow according to other metrics, you need to find out why. This tool helps by measuring the average speed of change detection, a phase in Angular's lifecycle that detects changes in values that are bound to the UI. Janky UI updates can result from slowness either in computing the changes or in applying those changes to the UI.

For your app to be performant, the process of computing changes must be very fast—preferably under 3 milliseconds. Fast change computation leaves room for the application logic, UI updates, and browser rendering pipeline to fit within a 16 ms frame (assuming a target frame rate of 60 FPS).

The change detection profiler repeatedly performs change detection without invoking any user actions, such as clicking buttons or entering text in input fields. It then computes the average amount of time (in milliseconds) to perform a single cycle of change detection and prints that to the console. This number depends on the current state of the UI. You are likely to see different numbers as you go from one screen in your application to another.

Running the profiler

Before running the profiler, enable the debugging tools and put the app into the state you want to measure:

  1. If you haven't already done so, enable the debugging tools.
  2. Navigate the app to a screen whose performance you want to profile.
  3. Make sure the screen is in a state that you want to measure. For example, you might want to profile the screen several times, with different amounts and kinds of data.

To run the profiler, enter the following in the dev console:

ng.profiler.timeChangeDetection();

The results are visible in the console.

Recording CPU profiles

To record a profile, pass {record: true} to timeChangeDetection():

ng.profiler.timeChangeDetection({record: true});

Then open the Profiles tab. The recorded profile has the title Change Detection. In Chrome, if you record the profile repeatedly, all the profiles are nested under Change Detection.

Interpreting the numbers

In a properly designed application, repeated attempts to detect changes without any user actions result in no changes to the UI. It is also desirable to have the cost of a user action be proportional to the amount of UI changes required. For example, popping up a menu with 5 items should be vastly faster than rendering a table of 500 rows and 10 columns. Therefore, change detection with no UI updates should be as fast as possible.

Investigating slow change detection

So you found a screen in your application on which the profiler reports a very high number (i.e. >3ms). This is where a recorded CPU profile can help. Enable recording while profiling:

ng.profiler.timeChangeDetection({record: true});

Then look for hot spots using Chrome CPU profiler.

Reducing change detection cost

There are many reasons for slow change detection. To gain intuition about possible causes it helps to understand how change detection works. Such a discussion is outside the scope of this document, but here are some key concepts.

By default, Angular uses a dirty checking mechanism to find model changes. This mechanism involves evaluating every bound expression that's active on the UI. These usually include text interpolation via {{expression}} and property bindings via [prop]="expression". If any of the evaluated expressions are costly to compute, they might contribute to slow change detection. A good way to speed things up is to use plain class fields in your expressions and avoid any kind of computation. For example:

@View(
  template: '<button [enabled]="isEnabled">{{title}}</button>'
)
class FancyButton {
  // GOOD: no computation, just returns the value
  bool isEnabled;

  // BAD: computes the final value upon request
  String _title;
  String get title => _title.trim().toUpperCase();
}

Most cases like these can be solved by precomputing the value and storing the final value in a field.

Angular also supports a second type of change detection: the push model. In this model, Angular does not poll your component for changes. Instead, the component tells Angular when it changes, and only then does Angular perform the update. This model is suitable in situations when your data model uses observable or immutable objects.