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

[Perf] Fine-grained reactivity #3624

Open
8 of 10 tasks
nolanlawson opened this issue Jul 14, 2023 · 7 comments
Open
8 of 10 tasks

[Perf] Fine-grained reactivity #3624

nolanlawson opened this issue Jul 14, 2023 · 7 comments

Comments

@nolanlawson
Copy link
Collaborator

nolanlawson commented Jul 14, 2023

Update: tracking progress on this as separate issues/PRs:


In the js-framework-benchmark, our slowest area is currently "select row":

Screenshot 2023-07-31 at 8 20 50 AM

The main thing about this test is that it's rendering 10k rows and then only updating every 10th one. So we are generating 9k VDOM nodes that never change and then uselessly diffing them.

The static content optimization helps a bit (krausest/js-framework-benchmark#1288), but it's not perfect because we still have the key which is different for every row, so not every VDOM can be static-optimized.

I can think of a few ways to optimize this:

  1. Implement an equivalent to Vue's v-memo; this is how they're able to get a high score on this benchmark.
  2. Implement something like fine-grained reactivity or block virtual DOM (Could "block virtual dom" benefit LWC? #3565) to only re-render the parts of the iterator that actual change based on the reactive observers. (This can be thought of as a kind of "auto memo".)
  3. Hoist the key outside of the vdom inside of the loop somehow – this would allow the underlying vdom to be more likely to be statically-optimized. (It would help with this benchmark, but maybe not with more dynamic iterator content.) We could also make this part of building a replacement iterator directive like lwc:each (Allow shorthand for directives like lwc:ref #3303).
@git2gus
Copy link

git2gus bot commented Jul 24, 2023

This issue has been linked to a new work item: W-13814092

@nolanlawson
Copy link
Collaborator Author

nolanlawson commented Jul 31, 2023

If you look at the js-framework-benchmark, the VDOM frameworks that are faster than us in the "select row" test are all relying on a shouldComponentUpdate/memo strategy:

E.g. Vue:

v-memo="[label, id === selected]"

We can do this, but it feels a bit unsatisfying to me, since it requires manual work on the part of the component author. It would be neat if we could do this automatically instead.

I was looking at how Solid does it, and they have something similar to our static fragment approach. They create a fragment like this:

_tmpl$3 = /*#__PURE__*/template(`<tr><td class="col-md-1"></td><td class="col-md-4"><a> </a></td><td class="col-md-1"><a><span class="glyphicon glyphicon-remove" aria-hidden="true"></span></a></td><td class="col-md-6">`);

... then they do surgical reactive updates on the part that changes (the text node content):

        children: row => {
          let rowId = row.id;
          return (() => {
            const _el$11 = _tmpl$3(),
              _el$12 = _el$11.firstChild,
              _el$13 = _el$12.nextSibling,
              _el$14 = _el$13.firstChild,
              _el$15 = _el$14.firstChild, // textnode
              _el$16 = _el$13.nextSibling,
              _el$17 = _el$16.firstChild;
            _el$12.textContent = rowId;
            _el$14.$$click = setSelected;
            _el$14.$$clickData = rowId;
            _el$17.$$click = remove;
            _el$17.$$clickData = rowId;
            createRenderEffect(_p$ => {
              const _v$ = isSelected(rowId) ? "danger" : "",
                _v$2 = row.label();
              _v$ !== _p$._v$ && className(_el$11, _p$._v$ = _v$);
              _v$2 !== _p$._v$2 && (_el$15.data = _p$._v$2 = _v$2); // textnode data updated here
              return _p$;
            }, {
              _v$: undefined,
              _v$2: undefined
            });
            return _el$11;
          })();

(Note they do firstChild/nextSibling because browsers use linked lists under the hood, so this is faster than allocating a children array.)

This is very similar to our static fragment optimization, except that we don't do it for anything that changes dynamically.

So here's what we could do:

  1. Identify blocks that would be static, except for, say, dynamic text node content.
  2. Create a static fragment for that with a text node placeholder. (Solid uses one whitespace character.)
  3. Enclose that in a ReactiveObserver that captures the value of cmp.foo.
  4. When cmp.foo changes, update the text node content.

The magic part is step 3 – if we enclose over the cmp.foo getter (i.e. replace the currentReactiveObserver), then it will not trigger the template's ReactiveObserver, so we won't re-run the tmpl function at all. (Unless cmp.foo is used somewhere else – then we will have to recreate the whole VNode array from scratch.)

We can gradually introduce this optimization. E.g. we can start with text nodes, then move on to classes, then attributes, etc. We might also start with shallow subtrees to avoid the firstChild/nextSibling dance (similar to what we currently do for static vnodes with events/refs).

@nolanlawson
Copy link
Collaborator Author

After internal discussion: another intermediate step we can take is to do the "static-ish fragment" optimization before the fine-grained reactivity optimization. Presumably the cloneNode() optimization alone would get us some perf wins.

@nolanlawson
Copy link
Collaborator Author

A potential game plan:

  1. Make deep-traversal for event listeners and lwc:ref work. Putting the machinery in place to do the firstChild/nextSibling dance should lay the groundwork for the next phase
  2. Make dynamic attributes/classes/textContent to be considered "static-ish" and use the previous machinery to update them after calling cloneNode(true).

@AllanOricil
Copy link
Contributor

😐 => my face because I can't comprehend most of what you commented above.

@nolanlawson
Which docs/books can I read to learn the fundamentals to understand what you said above?

@nolanlawson
Copy link
Collaborator Author

@AllanOricil This is a pretty dense topic, but if you watch some streams from Ryan Carniato or his blog posts you can find some information on this. In particular, the ones where he talks about js-framework-benchmark should be helpful. Sorry I can't provide any more detail than that!

@AllanOricil
Copy link
Contributor

That is a start. Thank you

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants