[Performance] Does initTree need to walk over elements with no Alpine directives? #4657
Replies: 5 comments 23 replies
-
|
It's likely that most of those things don't matter much, since realistically, there is very little happening in those most of the time if the element has no directives. It still ends up using a queue, since it just walks the elements and collects the directives. It doesn't store the elements as it goes, so stack or queue, doesn't matter, since you still have to GET the elements in the first place. Like if you do a stack, how are you building the stack? how are you building the queue? you need to go element by element at the end of the day. The walk skipping initialized elements is related to elements that can be moved, or CLONED. So that they aren't double initialized. It's most likely that if you have lots of x-for, that those are the things causing the slowdown. There is an open PR for optimizing x-for that maybe you can test how that impacts your situation. But mostly, to answer your question, there isn't really a way for Alpine to just ONLY look at elements with directives. It has to start from x-data elements and walk the trees. There isn't a way around that. the cost of processing elements without directives will be virtually nothing, especially when compared to processing directives. Some things in your template that could increase the processing time would be using django variables directly in expressions, like Since this will necessitate parsing and creating a function for every time this appears. since in the render it will be |
Beta Was this translation helpful? Give feedback.
-
|
In my opinion, a big problem with alpine is the fact that it creates callbacks for everything. This is nice and simple from an architectural point of view, but it is not efficient when there are a lot of elements. I have a page where replacing |
Beta Was this translation helpful? Give feedback.
-
|
Oops, I got a little distracted. My main point was that alpine is very voracious for the initial rendering. When it walks the dom and does all its magic. During the work itself, I could not find any bottlenecks in any case. Well, only the memory consumption is very high, but there are no CPU costs after initialization and everything works quite smoothly. Although with a huge number of objects and callbacks, the work of gc becomes noticeable in the form of micro freezes during animations. My comment about |
Beta Was this translation helpful? Give feedback.
-
|
Few more perf ideas:
|
Beta Was this translation helpful? Give feedback.
-
|
I started testing different ideas and gradually it led me to create my own framework. Very similar to alpine.js, but not drop-in-replacement. Only alpha version yet, but it already works much faster than alpine and consumes about 3 times less memory. Here is a comparison on rendering 5000 elements: alpine.js vs jbind.js |
Beta Was this translation helpful? Give feedback.


Uh oh!
There was an error while loading. Please reload this page.
-
Context
Hi, as mentioned in this comment, yesterday I was playing around with Alpine, because it felt like it was loading too slowly.
x-...), which at runtime expand to ~650 Alpine directives throughx-for.Alpine.start()andinitTree()takes ~250ms.My issue was that the time between the page load (First contentful paint) and interactivity (Alpine and all x-for loaded) was long enough (~350-400ms) that one could make 1-2 clicks before Alpine jumped up.
Non-Alpine perf issues
Sharing to help others:
Another unrelated part was chrome extensions. The page JS loaded ~30% faster after disabling a bunch.
Lastly, about 60-70ms (~25%) of the 250ms taken in
initTree()is spend on recalculating / repainting the layout.My guess is that this is due to all the
x-for(on the page, there is I think 3 levels ofx-fornested in each other).Since I'm using Django, AKA server-side rendering, this repaint could be avoided if I could pre-render the initial state on the server and then "hand over" that to
x-for. I've mentioned this in the past in Pre-render x-for entries #4415.By the time I found the issues above, I was already too deep in the Performance tab, and so I kept digging.
Alpine perf analysis
I looked into where is the most of the time spent. It showed that the
walk()andflushHandlers()are two biggest performance hotspots:Perf improvement ideas
1. Use stack / queue in
walk()Before:
walk()was recursively calling itself in order to walk all HTML elements that are nested under anyx-...directive.After: I tried refactoring it to use a stack/queue instead of recursion.
This seemed to have made a 10-20% perf gain yesterday. But I'm unable to reproduce that number today. So maybe red herring.
However, it brings me to another idea:
2. Skip walking over HTML elements that don't have Alpine directives? (AKA set
_x_markerONLY for HTML elements with Alpine diretives)Right now,
walk()will walk over ALL elements, one by one, and will run following code for each of them:alpine/packages/alpinejs/src/lifecycle.js
Lines 96 to 107 in df63fe2
My question is - do we NEED to run this for HTML elements that do NOT have
x-...Alpine directives?In other words - What is the purpose of
intercept()andinitInterceptors()? Are they intended to run for ALL HTML elements, or only those withx-...Alpine directives?If we do NOT need to do so, then maybe we could use
document.querySelectorAll()to find only the nested HTML elements that have Alpine directives?For context, my HTML has those 670 Alpine directives. The whole HTML has ~2600 HTML elements and
walk()is called on ~1925 HTML elements. So ~1300 of those calls towalk()could have been avoided.Instead of walking node by node, we could use
document.querySelectorAll()(dunno, but I assume it would be faster).By that logic, and using the graph above, it could save ~15% from the overall 250ms, by reducing the running time of
walk()from 60ms to 20ms.Notes:
walk()ininitTreealso sets_x_markerto the HTML element (source).onElRemovedandonElAdded)_x_markeris thatonElAddedruns ONLY if it was NOT processed yet.onElAddedis used ONLY byinitTree()_x_markercheck in here, which would mean:initTreewould be always triggered when ANY HTML node is added.walk()insideinitTreewould doquerySelectorAllto find nested (or itself) elements with Alpine directivesx-...x-...directives for which we would run the original callback of walker in initTree check if they have_x_marker(source)_x_markercheck tha ensures thatonElRemovedruns only HTML elements with Alpine directives would stay.destroyTree(), which only removes private alpine attributes and acts upon them if present.injectMagics/getUtilities, but as I just checked, magics seem to be only ever applied to HTML elements that HAVE Alpine directives on them.3. Does it make sense to walk down HTML tree if we come across an element that was already walked?
This relates to the conditional again in the
walk()function:alpine/packages/alpinejs/src/lifecycle.js
Line 94 in df63fe2
From my understanding of the code, what this conditional does is that:
intercept()andinitInterceptors()anddirectives()on the HTML element.Now, I wonder if we could get away with calling
skip()when the element already has_x_marker? (Same way as it's done a few lines later)alpine/packages/alpinejs/src/lifecycle.js
Line 107 in df63fe2
This would mean that, if an element has
_x_markerattribute, then we won't check / walk down its children.My naive intuition tells me that this would make sense:
initTree()starts from the first / top-most HTML element that has an Alpine directive.walk()walks down to reach all leaf nodes._x_marker.What is unknown to me is how that works with
Alpine.clone()orx-teleport.Results
The above is the theory, here's actual results:
walk()walk()already DOES run only once per HTML element, no changes needed.Beta Was this translation helpful? Give feedback.
All reactions