Skip to content
This repository was archived by the owner on Sep 16, 2022. It is now read-only.

Optimizing for immutability with AngularDart #797

Closed
7 tasks
matanlurey opened this issue Jan 24, 2018 · 12 comments
Closed
7 tasks

Optimizing for immutability with AngularDart #797

matanlurey opened this issue Jan 24, 2018 · 12 comments

Comments

@matanlurey
Copy link
Contributor

There is increasing requests to help optimize for immutable data structures in AngularDart.

(Somewhat related to documentation in #464.)

There are some optimizations we (should) do already today (and should test for):

  • We (should?) understand that const objects are deeply immutable:
@Component(
  template: 'Version: {{version}}'
)
class Comp {
  static const version = 'v1.0.0';
}

... should be easily compilable to just 'Version v1.0.0' without change detection.

  • We (should?) understand that final fields and getters that return const objects are immutable:
@Component(
  template: 'Version {{version}} of {{product}}',
)
class Comp {
  final version = 'v1.0.0';
  String get product => 'MyApp';
}

... again, similar to above, with or without a static keyword.

In this case, we can also make some assumptions around literal types (always const):

  • bool

  • num, int, double

  • null

  • String

  • const []

  • const {}

  • We (should?) understand that APIs that return literals are effectively const:

import 'version_service.dart';

@Component(
  template: 'Version {{version}}',
)
class Comp {
  final String version;
  Comp(VersionService s) : version = s.getCurrentVersion();
}

We can't inline version too eagerly, but we could still only (always) check it once.


Fun! But that's not everything. What about:

  • Custom user-types (i.e. not one of the literals above)

We could add a package:angular_meta and a @trustDeepImmutable, for example.

This could be used safely on types provided by built_value, or other value types. If we ever get @sealed (or similar), we could infer this automatically for some types, but it's not safe yet.

  • Types that are shallow immutable, but not deep immutable

For example, and immutable list where individual items are not immutable. Not sure yet.

  • Boolean values for *ngIf

For example *ngIf="true" could just be removed, and *ngIf=false could strip out the code.

  • Iterables or Lists that are immutable (for *ngFor or a *ngForOnce)

This is a tracking issue, and we can create additional issues as needed.

@matanlurey
Copy link
Contributor Author

/cc @hterkelsen who started some of these optimizations, they might already exist.

@leonsenft
Copy link
Contributor

final (and I'm guessing const) primitive fields (String, bool, int, etc.) are already only change detected once. It's not correct to make this optimization for any non-primitives fields, but we could perhaps create an annotation to express that a final object shouldn't be change detected more than once?

@harryterkelsen
Copy link
Contributor

We already optimize in the case of fields declared final and literals, as well as string interpolations where every interpolated expression is const. However, we do not optimize getters that return const because we don't have access to the getter bodies (maybe this is wrong?). If we could access getter bodies, we could also add Intl.message calls to the list of "effectively final" expressions.

@natebosch
Copy link
Contributor

we do not optimize getters that return const

Should we bother? Should we focus instead on writing a lint that tells you to use a final field instead?

@zoechi
Copy link

zoechi commented Jan 25, 2018

In Flutter they use @immutable a lot.
Not sure this is relevant.
It might make it easier to check the annotation instead of all fields of the whole class hierarchy.

@matanlurey
Copy link
Contributor Author

matanlurey commented Jan 26, 2018

@hterkelsen:

We already optimize in the case of fields declared final and literals, as well as string interpolations where every interpolated expression is const. However, we do not optimize getters that return const because we don't have access to the getter bodies (maybe this is wrong?). If we could access getter bodies, we could also add Intl.message calls to the list of "effectively final" expressions.

As a workaround, we could just require an annotation (i.e. @trustDeepImmutable or similar).

@natebosch:

Should we bother? Should we focus instead on writing a lint that tells you to use a final field instead?

It's not always straight forward, for example overriding a getter of a base class:

class A {
  String get name => 'A';
}

class B extends A {
  @override
  String get name => 'B';
}

@zoechi:

In Flutter they use @immutable a lot.

I don't know if that guarantees deep immutability

@harryterkelsen
Copy link
Contributor

I'm in support of adding an annotation for "effectively final" getters (for Intl.message calls, for example).

You can override a getter with a final field (or even a normal field). I think it would be a useful lint to detect getters that return literals.

@jonahwilliams
Copy link
Contributor

From what I understand from the React + Flutter world, the benefit of immutability is that you avoid rebuilding your component/widget (and the subsequent diff) tree if your inputs did not change. This check is fast since you can use identical for immutable objects.

But in the Angular world change detection is already using identical to check properties. The only optimization here would be that change detection could stop at a parent component if all inputs are identical. There is also no configuration tree construction that needs to be short-circuited.

Isn't this what OnPush and later ComponentState were meant to solve?

@harryterkelsen
Copy link
Contributor

The optimization here is that if you have a component with, for example, this template:

<b>{{message}}</b>

Normally, we will generate code that detects the value of message on every change detection loop to see if it has changed. However, if we know that message never changes, we can optimize the check out entirely.

@natebosch
Copy link
Contributor

@matanlurey

It's not always straight forward, for example overriding a getter of a base class:

class A {
  String get name => 'A';
}

class B extends A {
  @override
  String get name => 'B';
}

You can override a getter with a final field.

class A {
  String get name => 'A';
}

class B extends A {
  @override
  final name = 'B';
}

If a getter has a const expression on the RHS I think it could always be a final field. Perhaps there are times we prefer a get for other reason?

@jonahwilliams
Copy link
Contributor

Normally, we will generate code that detects the value of message on every change detection loop to see if it has changed. However, if we know that message never changes, we can optimize the check out entirely.

Okay, makes sense to me- I think I misunderstood what sort of Immutability. I was thinking of cases where @input objects were 'immutable' (built_value or something), not parts of the component class.

@matanlurey
Copy link
Contributor Author

Closing as not actionable right now. We expect to focus more on change detection in the coming months and might have other issues that try to tackle things like this.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

6 participants