Skip to content
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

Keyed children diff optimization #663

Merged
merged 2 commits into from
Apr 6, 2018
Merged

Keyed children diff optimization #663

merged 2 commits into from
Apr 6, 2018

Conversation

SkaterDad
Copy link
Contributor

@SkaterDad SkaterDad commented Mar 25, 2018

Summary of the change

  1. Determine if the new keyed child matches the next old child (thanks to preact for the inspiration here).
  2. If the current old child is keyed, leave it alone.
  3. If the current old child is non-keyed, remove it. This keeps the "mixed keyed/nonkeyed" test happy.
  4. Increment the old children index and go back to the start of the while loop.

Benefits

  • Closes Keyed <input> loses focus when earlier sibling is removed #660: Keyed <input> stays focused if siblings before it are removed (Proof on Codepen). I tried writing test cases for this, but the <input> never fired a "blur" event like it did in-browser.

  • Better keyed list modification performance. Results in fewer "insertBefore" calls when a keyed item is removed but other items are in the same order. Open the "Details" below to see a screenshot from JS Framework Benchmark. Row removal is 2x faster. Row swap is 5x faster.

    image

Bytes

Minified file size changes from 3.45KB to 3.51KB.
Gzipped file size changes from 1.54KB to 1.56KB (using default options in linux gzip command).

Edit: Added benchmark screenshot.

 * Detect when keyed child matches the next old child
 * Results in fewer "insertBefore" calls
 * Keyed <input> stay focuses
@SkaterDad SkaterDad added bug Something isn't working website hyperapp.dev labels Mar 25, 2018
@codecov
Copy link

codecov bot commented Mar 25, 2018

Codecov Report

Merging #663 into master will not change coverage.
The diff coverage is 100%.

Impacted file tree graph

@@          Coverage Diff          @@
##           master   #663   +/-   ##
=====================================
  Coverage     100%   100%           
=====================================
  Files           1      1           
  Lines         161    166    +5     
  Branches       50     52    +2     
=====================================
+ Hits          161    166    +5
Impacted Files Coverage Δ
src/index.js 100% <100%> (ø) ⬆️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 808f42d...e2abf67. Read the comment docs.

@SkaterDad
Copy link
Contributor Author

SkaterDad commented Mar 26, 2018

I've done a little more experimenting to see how flexible this change is.

As the code implies, this is an optimization for single item removals. It also handles cases like this, where single items are removed in multiple parts of the list. Here's another demonstration which is automated: https://codepen.io/SkaterDad/pen/WzZPxr

[ 1, 2, 3 ] ==> [ 2, 3 ]
[ 1, 2, 3, 4, 5] ==> [ 2, 4, 5] 

I believe these are common operations in real apps (data-driven tables, dynamic forms, etc..), and are worth optimizing for. The benchmark results are a nice bonus.

This change does not handle multiple consecutive items being removed, or if a non-keyed item is mixed in. The diffing will go back to its current behavior and do a bunch of "insertBefore" DOM operations.

Those cases are where a more complicated diffing algorithm like the one in ivi can make better decisions, but for now I'm happy to write my views in a way that avoids those scenarios when possible.

@jorgebucaran jorgebucaran mentioned this pull request Apr 1, 2018
@SkaterDad
Copy link
Contributor Author

@jorgebucaran Do you see this going anywhere before 2.0? I've got code I'm waiting to PR for thunks/lazy VNodes which touches code in the same region. I would imagine 2.0 is touching some of the patch & resolveNode code also?

I'm fine waiting, just curious what your plans are.

@jorgebucaran
Copy link
Owner

jorgebucaran commented Apr 4, 2018

@SkaterDad I didn't know this was blocking you, I'll get this merged and published tomorrow.

I would imagine 2.0 is touching some of the patch & resolveNode code also?

It isn't. That algo rewrite is also in the pipeline, but after 2.0. 😅

@SkaterDad
Copy link
Contributor Author

No worries! Thanks for getting to it.

src/index.js Outdated
@@ -320,13 +320,22 @@ export function app(state, actions, view, container) {

while (k < children.length) {
var oldKey = getKey(oldChildren[i])
var nextOldKey = getKey(oldChildren[i + 1])
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@SkaterDad Can we inline this call with the if (newKey != null && newKey === nextOldKey) { later below?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You bet. Looks like that shaves ~3 bytes off after gzip.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done 👍

@jorgebucaran
Copy link
Owner

@SkaterDad If the current old child is non-keyed, remove it. This keeps the "mixed keyed/nonkeyed" test happy.

Do we want to keep those tests happy though? 🤔

@SkaterDad
Copy link
Contributor Author

@jorgebucaran Do we want to keep those tests happy though? thinking

If you don't remove the non-keyed child at that point in the code, it stays in the DOM since cleanup doesn't happen until the end and assumes the only children you need to remove are at the end of the oldChildren array.

I think mixed keyed/non-keyed is a nice feature to support. I do find myself only keying specific children sometimes, and it behaves intuitively.

@jorgebucaran jorgebucaran merged commit f16f7fc into jorgebucaran:master Apr 6, 2018
@jorgebucaran
Copy link
Owner

Thank you, @SkaterDad! 🎉🎉🎉

@cjh9
Copy link

cjh9 commented Apr 17, 2018

@SkaterDad @jorgebucaran The thing I'm building is highly dependent on having non-keyed elements together with keyed elements in the same parent. If it is keyed, I don't want the algorithm to touch it, contrary if it's not the algo can figure out the most efficient way to modify the dom in order to map to the vdom.

@jorgebucaran
Copy link
Owner

@cjh9 ...having non-keyed elements together with keyed elements in the same parent...

This hasn't changed.

@infinnie infinnie mentioned this pull request Apr 26, 2018
@jorgebucaran
Copy link
Owner

@SkaterDad I published 1.2.6 by the way. Sorry for the incredible delay. 🙇

@SkaterDad
Copy link
Contributor Author

SkaterDad commented May 31, 2018 via email

@jorgebucaran jorgebucaran removed the bug Something isn't working label Sep 1, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
website hyperapp.dev
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants