-
Notifications
You must be signed in to change notification settings - Fork 27.4k
Conversation
Internally, `$animate` already wraps elements passed through with `jqLite`, so we can avoid needless duplication here.
Unlike jqLite, jquery scrapes the attributes of an element looking for data- keys that match the requested property. When many elements are being animated due to something like `ngRepeat` unrolling within one digest cycle, the amount of time spent in that one function quickly adds up. By changing our API to use the lower level data API, we can cut the time spent in this function by half when jQuery is loaded.
Another potential optimization for perf here is just to stop using jQuery or jqLite at all. No functionality from either library is used other than Doing that along with this PR looks to get us close to a 75% speed improvement with no behavioral changes in this function. |
Thanks for he PR. Good stuff. What's your test setup for the performance tests? |
At the moment, it's nothing overly complicated. I have some use cases internally that we know to be slow during initial compilation, so I just run those use cases 5 times with the profiler running, see where our time is being spent, and optimize. For reference: Before: After: So in this specific function, the self time is almost entirely unchanged which isn't too surprising since no actual logic was changed, but time spent in child calls was nearly eliminated compared to what it was previously Edit: The "after" contains changes not seen in this PR, but were proposed including the change to use expando properties on the element directly rather than |
I've also made some tests and your version is approximately 7% faster in an ngRepeat with a 500 elements (that don't actually have animations). The actual improvement depends very much on the number of animatable elements, but even 7% is nothing to sneeze at. |
I just noticed that one commit addresses jquery specifically. I haven't tested that yet. |
@Narretz If you're game, I'm happy to look into adding the other changes that I described in my previous commit. The one I'm most curious about is whether or not you'd be open to using expando properties on elements rather than The biggest concern that I have with using a Symbol or otherwise "private" property is codepaths like this which might miss those added properties. |
Btw, the jQuery path is where you're going to see huge performance gains. The reason it's so expensive is that part of the |
@dcherman What's the parentNode change about again? If it's straightforward, go ahead and add it (in another commit) |
Hi all,
The last point is the one that we should be more careful about when doing any change to Other than that, all these improvements are welcome! |
@lgalfaso Interesting, I didn't know about that de-opt when adding expando properties. Does the number of properties added matter, or even a single one? At least until a |
The `parentNode` property is well supported between all browsers. Since no other functionality was required here other than traversing upwards using `.parent()`, we can use the DOM API directly.
A single non-DOM property is enough for the object to be promoted. Do not On Tuesday, February 9, 2016, dherman [email protected] wrote:
|
@lgalfaso Happy to do some tinkering. Do you have any resources about that perf hit? I'm not sure what the observable side effects would be; slower DOM methods, higher memory usage, etc. The only references to expando property problems that I've found so far are related to the old memory leaks in IE < 8, but that's generally no longer relevant. FWIW, not only do jQuery/jqLite do this, but several places in Other than that, I pushed the change to use |
I believe every time you add or remove a property to a DOM node the browser essentially creates a class (see: jquery/jquery#2479 (comment)). |
@dcherman #13879 (comment) is one reference to this. This affects all modern browsers as there are many optimizations possible when DOM nodes are not JS object |
FWIW, calling |
Interesting. @lgalfaso Let's not let this investigation hold this PR up since we still get a lot of win out of this. I'm crawling through the Blink code to try and determine if this situation really is special cased, or if it just follows the standard rules of hidden classes with V8 objects (probably not since the DOM isn't implemented in JS...yet). I'd be curious to know if there's a difference if we were to pre-assign something like |
I think the bigger issue blocking the use of I'm not sure about modifying the prototype but if you find anything I'd be curious! Note that often the perf hit is with GC/memory usage - if you add/remove a property from a DOM node it seems to copy the node to a new object (which often consumes more memory then the original object). But maybe if the |
landed this PR |
The `parentNode` property is well supported between all browsers. Since no other functionality was required here other than traversing upwards using `.parent()`, we can use the DOM API directly. Closes: #13879
I think we can still use this PR for other performance changes, but what is already here was ready to be landed |
Cool stuff. I'm not going to pretend to be a v8/blink/C++ expert (or even a novice for that matter), but it looks like the bridge from DOM -> v8 is contained in https://chromium.googlesource.com/chromium/blink/+/master/Source/bindings/core/v8/V8DOMConfiguration.cpp. I also have no idea how this is implemented in either Firefox or IE. Unless I'm barking up the wrong tree which is entirely possible, the implementation on that file means that means that Blink will essentially generate V8 backed classes, so they should follow similar rules to plain old JS hidden classes after the initial construction. That means that setting a prototype property as in my previous example is entirely useless since prototypes and instances don't share the same hidden class. @jbedard Nope, WeakMap lookups are actually plenty fast. Just ran a jsPerf on them across Chrome, FF, and IE11 and each of them has perfectly acceptable ops/sec (in the millions or tens of millions). The bigger issue is that per the issue I linked earlier, WeakMaps aren't weak enough, they're kinda strong in v8. Apparently you need two full-scale GCs in order for keys to be cleaned up properly; incremental ones don't work. That means that while memory won't technically leak, you may end up with more bloat than necessary before things get GCed, and that GC pass would take longer than it normally should since it's got more work to do. |
So knowing that the bridge to v8 uses the same internals as other objects, we can do some simple tests using some of the v8 natives var a = document.createElement('div');
var b = a.cloneNode();
// True, indicating both A and B share the same hidden class
console.log(%HaveSameMap(a, b));
b.foo = "bar";
// False. B has a different hidden class
console.log(%HaveSameMap(a, b));
a.foo = "bar";
// True. A and B now share the same hidden class again
console.log(%HaveSameMap(a, b)); A lot of this exploration is just for fun; as of jQuery 3.0 where they started attaching data straight to the DOM as an expando rather than using Next up, would be interesting to know what the implications of changing the hidden class are memory-wise. That should be able to answer the question posed previously of whether or not all of the properties of the node are copied to whatever internal representation exists in V8, or if the existing structure is re-used. @jbedard Since you were interested. |
Last update. If you then inspect what exactly transitioning hidden classes entails, it looks like it's expected that as you transition from class A -> B, the in-memory layouts are compatible. If you transition from B -> A, that's a more generalized transition, the layouts should be compatible already. https://github.com/v8/v8/blob/master/src/objects-inl.h#L5350-L5368 Again I really have to stress than I'm not even a C++ novice to take everything I've said with multiple grains of salt, but since the JSObjects share the same layout, I don't believe that they need to make copies of the object; they're visiting the same location in the heap. If you think about it, that makes a ton of sense anyway. If doing Still nothing really actionable is going to come out of this, but it was definitely interesting to dive into the v8 internals a bit. |
Interesting stuff. I haven't looked at the C++ code or anything, but I don't think the in-memory layouts will always be compatible. When transitioning from the normal classes to custom ones (because you add a property to a DOM) it often consumes significantly more memory, so it couldn't possible be using the same block of memory...? |
The `parentNode` property is well supported between all browsers. Since no other functionality was required here other than traversing upwards using `.parent()`, we can use the DOM API directly. Closes: #13879
The `parentNode` property is well supported between all browsers. Since no other functionality was required here other than traversing upwards using `.parent()`, we can use the DOM API directly. Closes: #13879
@dcherman Have you tested the differences without jquery, too? I've tried some tests today, and for some reason with the 3 changes, it's always a bit slower. |
@Narretz I'll do some testing without jQuery loaded a bit later today. I would be very surprised if this had any negative side effects on jqLite performance since I'm literally doing a subset of the work that was done before |
@Narretz So I've done some testing, but it's kind of inconclusive. I've ran tests where it's slower, and I've ran tests where it's faster, probably attributed to just normal variation. The only thing I see in the code as a potential slow down is that we're changing the type of What do your tests look like? I'm just making a list of 1000 items, each of them calling |
In
ngRepeat
, we can avoid an extra level ofjqLite
wrapping by passing the raw node. Internally, it looks like$animate
already supports this as it re-wraps the node anyway.We can also optimize the calls to determine whether or not an animation should be allowed on a given element or not by using the lower level
.data()
api rather than.fn.data()
. When jQuery is loaded, using the latter represents a significant amount of overhead due to data attribute parsing.A more extreme change that I want to propose is that we bypass any type of data API entirely and simply start using
Symbol
and sticking the metadata on the node itself. There is already precedent for that in the codebase (see ngRepeat where it marks nodes to be deleted like that). In browsers that don't support Symbols, a simple polyfill can be used to generate a key that will not collide.