-
-
Notifications
You must be signed in to change notification settings - Fork 4.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Improve hydration by reordering optimally #6395
Conversation
From what I understand #6392 is similar in that it implements a similar In the example that I gave with the original nodes: and the claim_element function calls: that pull request seems to do the following mutations (step-by-step states):
So A worst case for this pull request is when an early element is matched very late (since the first common subsequence will be very short). A worst case for #6392 is when a late element is matched very early (since all the other elements have to move in front of that element). |
From what I know & understand, given arbitrary So, in a sense, we could potentially implement the Then, we can theoretically implement insert & append in a way that they do the minimum number of moves given the claims. |
37b552f
to
84ec1b4
Compare
I have implemented optimal reordering and changed the New algorithmThe new algorithm computes the optimal amount of reorderings for a given set of claims. The claims are made in such a way that the optimal amount of reorderings is likely small. There are two steps in hydration that are relevant to us. The first step is the claim step where existing nodes are picked to be reused one by one (or created when no corresponding node already exists). The second step is the modification step where claimed nodes are reordered and newly created nodes are inserted. Let's say that our existing nodes are: And we claim: During the modification step, there are three operations that we do modify our existing nodes to match the claims.
What we can optimize in this approach is to minimize the total number of reorderings. There are two factors that effect the total number of reorderings: The first factor is the reordering operations that we actually choose to do. Luckily the optimal amount of reorderings can be computed algorithmically in O(n log n) time, which I have implemented in the latest commit. The optimal amount of reorderings is given by (number_of_nodes - longest_ordered_subsequence_of_claimed_nodes). So if we maximize the longest ordered subsequence of claimed nodes, we minimize the amount of reordering, since only the nodes that are not in the longest ordered subsequence are reordered. The second factor is, of course, the claiming order. If there are multiple elements with the same tag we are trying to claim, we can choose any one of those elements. This choice will effect the total number of reordering. What I do is, I remember the position of the last claim, and try to claim a node to its right side. If I cannot do it, I claim a node to the left (or create a new node when no matching node exists). I am hoping that this heuristic will maximize the longest ordered subsequence. (Additionally, if I am only claiming a text node, I do not update the position of the last claim, since I do not want text nodes to break a sequence of real elements. My notes under possible optimizations in the original post contain an example for this #6395 (comment)) New possible issues
|
I updated the title to better reflect what the algorithm is doing now. |
This is very clever. I ran some benchmarks on my site and this seems significantly faster than my PR #6392; maybe 15% faster or so. So I'd support this PR over mine. I'll try to add some detailed comments on the code later. |
d10b9ec
to
4f161fb
Compare
@hbirler we just checked in some new tests for hydration. Can you rebase this PR against |
I just tried this locally, and all of the new tests pass with these changes. |
During hydration, greedily pick nodes that exist in the original HTML that should not be detached. Detach the rest.
During hydration we track the order in which children are claimed. Afterwards, rather than reordering them greedily one-by-one, we reorder all claimed children during the first append optimally. The optimal reordering first finds the longest subsequence of children that have been claimed in order. These children will not be moved. The rest of the children are reordered to where they have to go. This algorithm is guaranteed to be optimal in the number of reorderings. The hydration/head-meta-hydrate-duplicate test sample has been modified slightly. The order in which the <title> tag is being generated changed, which does not affect correctness.
Not sorting children before executing the `insertBefore` calls in `init_hydrate` potentially caused extra `insertBefore` calls in `append`
79e5e1d
to
8baf905
Compare
I rebased the branch. The tests pass locally for me as well. |
@hbirler Thank you soo much for your effort on this. |
@hbirler thanks for this. Hydration performance has been a long standing issue, so it's great to see improvements! I just updated a SvelteKit site of mine to use it. I see in the Chrome performance tab screenshots that the logo on my site (https://c3.ventures/) momentarily disappears and then is rerendered. I set |
@benmccann I think I might have found out what the issue is. The idea is, if the source is already set correctly, do not update it. However, if I look at the actual values: So even though the values are theoretically equivalent, we still end up changing the src attribute to something else. I believe this might be the issue. The So the issue is not that the generated ssr html is wrong. Reading the src attribute client side somehow returns the full url. Maybe someone more experienced with web development than me can comment on this? Maybe a more proper equivalence check can be done rather than just |
Whatever the solution, the |
|
@benmccann Does https://github.com/hbirler/svelte/tree/better-hydration-src fix your issue? |
In my case, I have a stylesheet linked in |
I'll check with your branch if that fixes it. |
Tried your branch, does not work for me. |
I only tried to fix the src issue in the branch :( . |
@ankitstarski It might be that the header tags generated by the SSR somehow do not match the order the client side expects them to be in, but I do not know how this might happen. Edit: claiming elements from the header seems to work differently than normal nodes in Svelte. I am not sure what kinds of changes would be required to properly handle nodes under |
@ankitstarski Does the branch solve your problem now? I have committed another change. |
It could be that I'm using I'll check with your branch and update here. |
Thanks @hbirler. Unfortunately I get the same behavior on that branch |
The branch does improve a local test of mine for a similar situation. It's weird that it did not work for you, maybe your website hits another corner case. |
@hbirler It does fix the issue for me. But high LCP and high Time to Interactive are still there compared to a custom build of svelte that I'm using based on Avi's patch . Also, thanks for your contribution! 🥇 |
@ankitstarski You can try the branch again to see if there are any improvements. The claim_text change improved a local test of mine. |
No noticeable change. |
(Hopefully) Closes #4308.
I am new to working with Svelte so I apologize for any errors beforehand.
Explanation
I tried to improve #6204 based on the thread #4308. I personally had problems with elements with CSS animations flickering due to hydration detaching and reattaching nodes (implicitly by re-inserting them) as described in the thread. The page that I was having problems with was almost entirely static, so seeing all the static nodes refresh went against my intuition as to how hydrate would work. So I looked into how this situation could be improved.
Old algorithm
Edit: The algorithm has been improved significantly. See #6395 (comment) for the new algorithm
During hydration, this new implementation tries to pick a sequence of nodes that we are able to not detach: the first common subsequence of the original Nodes and the
claim_*
requests.For example, if the original nodes are:
B C D E F D G
and the
claim_element
function calls are as follows:A B C D D E F G H
the first common subsequence would look like
B C D D G
You can find this subsequence by iterating through the
claim_element
s and keeping a pointer on the original nodes that you are only able to move forward. For everyclaim_element
, if you are able to move the pointer to a matching node on the right, do it. Otherwise, do not move the pointer.The algorithm chooses to preserve the nodes within this subsequence (i.e. not detach them). The rest of the nodes that are requested by
claim_*
are handled as before: they are either found among existing nodes to be detached or created anew.Normally, after the claims are done,
append
andinsert
are called with every single child, since the caller assumes that all the children are detached. Since this is not the case anymore (the shared subsequence was not detached), we need to transparently make sure thatappend
does what is expected, which in this case might be to insert a node in the middle of existing non-detached children. Duringappend
, the algorithm tracks the last appended node, i.e., what the caller thinks is actually the last child of thetarget
. Thus, we can deduce where the caller actually wants to append the node. Theinsert
function works similarly.Possible optimizations
Note that the first common subsequence is not the longest common subsequence:
B C D E F G
We would prefer to not detach as many nodes as possible, so the longest common subsequence would theoretically be preferable. However, since the
claim_*
functions (if I understand them correctly) are expected to return Nodes directly (and greedily), I am not sure a change can be implemented without changing the compiler side.Even though finding the optimal (longest) subsequence to not detach is problematic, it should be possible to further improve the greedy approach based on heuristics. In the example above, the second
D
node that is claimed results in the two nodesE
andF
getting detached. If theD
node is just Text andE/F
are large Elements, that would be quite a sad occurrence. This could be optimized in the future handling Text Nodes differently. I did not implement such an optimization since I was not sure my assumptions are correct and would love to receive feedback.One might also look into what virtual DOM front-end frameworks are doing to find minimal differences https://github.com/localvoid/ivi/blob/2c81ead934b9128e092cc2a5ef2d3cabc73cb5dd/packages/ivi/src/vdom/implementation.ts#L1367 . But I am not sure how applicable those will be to our situation.
There may be many more possible optimizations! I am relatively inexperienced with front-end development so I might be missing many optimization opportunities.
Possible issues
Within the code, I added an additional property
actual_end_child
toNode
objects duringappend
andinsert
calls to make tracking last appended nodes easier. If this is a problem, we can switch to using aMap<Node, Node>
.Tests
The code passed all the tests but I did not add any new tests since I wasn't sure how to test for whether nodes are being detached. Maybe a total number of nodes detached counter could be used to ensure that the algorithm is working and further optimizations do not cause regressions. Any feedback on this is appreciated.