Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.

Angular 1.5.0's transclude lazy compilation is a breaking change #14343

Open
moneytree-doug opened this issue Mar 30, 2016 · 6 comments
Open

Comments

@moneytree-doug
Copy link

Do you want to request a feature or report a bug?
Bug.

Correct me if I am wrong, but I think this is related to the "Performance Improvements" from the CHANGELOG for 1.5.0-beta.1 dense-dispersion (2015-09-29)

Performance Improvements

$compile: Lazily compile the transclude function 

What is the current behavior?
Angular 1.4.7: http://plnkr.co/edit/7ZPBgqpPnbXF57rvezPf?p=preview

In Angular 1.4.7, you'll see that a service is available because the transcluded element is compiled first therefore it is available later. Please refer to the console.log.

If the current behavior is a bug, please provide the steps to reproduce and if possible a minimal demo of the problem via https://plnkr.co or similar (template: http://plnkr.co/edit/tpl:yBpEi4).

Angular 1.5.0: http://plnkr.co/edit/5Nh7EHLINDjMlaQ8dZbl?p=preview

In Angular 1.5.0, you'll see that a service is NOT available because the transcluded directive is lazily compiled. Please refer to the console.log.

What is the expected behavior?
The behaviour in Angular 1.4.7.

What is the motivation / use case for changing the behavior?
I want the child directive with transclude to populate a service with data for a parent directive to access in the link function.

Which versions of Angular, and which browser / OS are affected by this issue? Did this work in previous versions of Angular? Please also test with the latest stable and snapshot (https://code.angularjs.org/snapshot/) versions.
It stopped working in the Angular 1.5.X series, but it previously worked in the 1.4.X series.

Other information (e.g. stacktraces, related issues, suggestions how to fix)

@Narretz
Copy link
Contributor

Narretz commented Mar 30, 2016

I agree that this is unexpected behavior, but the "bug" can also be produced in 1.4.x by using a templateUrl instead of template. In that case, the transcluded content will also be compiled async, and the wizard won't be set in the link function. In general, it's safer to notify the parent when the child is actually available: http://plnkr.co/edit/bzliiP4lIWFm5auuAj6o?p=preview Basically, the parent should never assume that a child is available when the parent's link fn is called.
I guess we can add a BC notice for this.

@moneytree-doug
Copy link
Author

moneytree-doug commented Mar 30, 2016

@Narretz Yeah, I can agree with either changing my implementation or having Angular be more backwards compatible. On that note of templateUrl, then I would agree that the lazy compilation behaviour in Angular 1.5.0 is more consistent.

Edit: Thanks for the example solution. I didn't know you can do that... it's interesting. But that solution seems kind of brittle because of the ^^, wouldn't you agree?

@dcherman
Copy link
Contributor

dcherman commented Apr 1, 2016

@moneytree-doug Less brittle actually. Not only will you appropriately notify the parent when you're actually available, but you'll also get errors in your console if the directive is used outside of its expected parent->child structure.

@mattslocum
Copy link

"lazy transclusion" was originally pitched as a performance improvement. Most of the defense, that I see for the feature is about consistency with templateUrl when it doesn't have the template loaded. To me this seems like a slow down in performance.

In the past, the compile happened from parent to child, and then link from child to parent before any digest cycles were run on the parent. Now, if it decides to go lazy, it will compile, link, and digest the parent before moving on to the child. I haven't benchmarked it, but this sounds a lot slower.

Also, in my production environment, I always have my templates loaded. I don't want template calls slowing down my page. I'm sure there are some good use cases for loading html later, but I feel that in many situations, html templates are best loaded with the js module. So the argument of consistency is mute for people that pre-load templates.

Does an angular member have any proof that this was a helpful change for performance?

@moneytree-doug
Copy link
Author

@mattslocum Well said.

@gkalpak
Copy link
Member

gkalpak commented Jun 28, 2016

I don't quite agree 😃

My thoughts:

Most of the defense, that I see for the feature is about consistency with templateUrl when it doesn't have the template loaded.

  1. This is not meant as a defense of the feature. It might be a "defense" against the feature being a breaking change.
  2. This always affects templateUrl (regardless of pre-loaded/cached templates - see below).
  3. The obvious defense of the feature is that it improves performance - and it's a good one imo 😃

Also, in my production environment, I always have my templates loaded. I don't want template calls slowing down my page.

Just to be clear, what @Narretz said about templateUrl is true whether you pre-load your templates into the $templateCache or fetch them from the server at runtime. Only inlining your templates and using template will have the directives compiled/linked synchronously.
(I am not sure what you meant by "loaded" templates, just making it clear for other users that might read this in the future. AFAICT, the most common practice is to cache the templates, by putting them in the $templateCache, but still use templateUrl.)

In the past, the compile happened from parent to child, and then link from child to parent before any digest cycles were run on the parent.

That is not exactly true. (Note that we are talking about transcluding directives.) Basically, it depends on when the transcluding directive decided to call the transclude function. For example, ngTransclude (which is often used in custom transcluding directives) compiles the content during compile and links it during post-link. On the other hand, the structural built-in directives (which are pretty common in most codebases), such as ngIf, ngSwitch* etc, need to evaluate some expression first, so call the transclude function asynchroonously.

Now, if it decides to go lazy, it will compile, link, and digest the parent before moving on to the child. I haven't benchmarked it, but this sounds a lot slower.

The only difference WRT to eager vs lazy compilation, is when the compiling happens:

  • With eager compilation, the element is compiled "on sight" and linked when needed.
  • With lazy compilation, the element is compiled and linked when needed.

Nothing changes wrt to the work that is done, the only change is wrt to when this work is done and how it is being distributed throughout the lifecycle of the app.

See below for more details on the timing of the various phases before and after.

Does an angular member have any proof that this was a helpful change for performance?

The performance gains (both in terms of startup time and memory used) vary depending on the size of the app and directives used. The obvious benefit is that you don't have to compile subtrees that you aren't going to use (e.g. the contents of an ngIf that will be always false or an ngSwitch case that won't be displayed). Even if you will eventually need a subtree later, it won't add to the startup time for your app or views (and the memory used) - it will be compiled and linked only when/if necessary.
(Again, to put this into context, it always has been linked only when/if necessary, but previously it would have been compiled "on sight".)

Actually, there is one case where performance would be objectively worst: When actually fetching the HTML from the server at runtime. But this is not recommended anyway and it is still possible to fetch them eagerly and cache them anyway.

Here are a few demos to show the differences in the order of operations (wrt digests) with eager and lazy compilation

Example 1 - ngIf

Assume a test directive with the following template: <div ng-if="true"><test2></test2></div>

ngIf with v1.4.7

"compile: test"
"compile: test2"
"pre-link: test"
"post-link: test"
"--- $digest ---"
"pre-link: test2"
"post-link: test2"
"--- $digest ---"

ngIf with v1.5.7

"compile: test"
"pre-link: test"
"post-link: test"
"--- $digest ---"
"compile: test2"
"pre-link: test2"
"post-link: test2"
"--- $digest ---"

Example 2 - ngTransclude

Assume a transcluding test directive with the template <div ng-transclude></div>, used like this: <test><test2></test2></test>

ngTransclude with v1.4.7

"compile: test2"
"compile: test"
"pre-link: test"
"pre-link: test2"
"post-link: test2"
"post-link: test"
"--- $digest ---"
"--- $digest ---"

ngTransclude with v1.5.7

"compile: test"
"pre-link: test"
"compile: test2"
"pre-link: test2"
"post-link: test2"
"post-link: test"
"--- $digest ---"
"--- $digest ---"

As you can see, the operations (i.e. the amount of work) are exactly the same between the two versions. The only thing that changed is when compile: test2 happens - only when necessary (lazy compilation) vs at the beginning (eager compilation). You can see one of the optimizations in action, by changing the ng-if condition to false in the first example and observe that test2 is (unnecessarily) compiled in v1.4.7 but not in v1.5.7.

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