Replies: 3 comments 5 replies
-
Some observations to check my understanding. I don't have much experience with these frameworks, so please grade my answers and correct me!
|
Beta Was this translation helpful? Give feedback.
-
I have some implementation thoughts on this, from an ECS perspective:
There are a number of ways we could handle this, if we wanted to :) Ultimately, this is a question of "we need to be able to reason about components, based on the properties of that component type". Some options:
In most cases, we would want some custom mechanism for "how should this be diffed", to allow for flexibility in terms of which fields we care about for example. The easiest way to do that would be to use the same strategy as Unsurprisingly, the "component metadata" and "trait query" approaches lead to the same solution here. Trait queries are effectively a convenient conceptual framework to allow for the querying and usage of metadata about each component type. I think that trait queries are the best approach here (they're more principled than just "more metadata"), and think that using Reflection to do this is likely to be both slow and leak implementation details down into the stack: bevy_reflect should not have to care about these problems, and frankly is the wrong tool for this entirely. Relying on |
Beta Was this translation helpful? Give feedback.
-
Dioxus also does this. It's a key reason for using a macro. The macro inspects the syntax tree and marks anything constant (e.g. string literals - which are pretty common for things like style properties) as not needing to be diffed. |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
In my previous post #20821, I tried to narrow the discussion to just reactions, avoiding any discussion of incrementalization. However, since @JMS55 brought up incrementalization, I figured I would write a post that explores this topic.
What is incrementalization?
I first heard the phrase, "Every modern UI framework contains an incrementalization strategy" in one of Raph Levien's talks, although he didn't originate the saying.
In a nutshell, incrementalization refers to a way of dynamically updating a UI hierarchy (or any display hierarchy for that matter) via patching:
In effect, incrementalization is a way to implement the following formula:
old_tree
+new_parameters
=>new_tree
, such thatnew_tree
re-uses the parts ofold_tree
that didn't change.A key requirement is that incremental updates should produce results that are indistinguishable from rebuilding everything from scratch: that is, given the same input parameter values, it shouldn't matter whether this is an incremental update or an initial construction. It should look like a "pure function" even when it isn't.
As pointed out in the comment, you can have incrementalization without reactivity: it just means that you have to change the input parameters manually.
Various Strategies
The display tree - what the user actually sees - is the output of an algorithm, however we need more than just the output in order to construct that tree. We need some kind of template or blueprint: a data structure that describes the output we want to generate.
We also need to store some kind of parameter state: the value of the parameters from the previous update, along with any intermediate values derived from them. There might also be local state.
Different UI frameworks approach this problem in different ways. Some of the ones I have seen are:
Long-lived scaffolding that exist separately from the output tree but are connected to it (React, Dioxus). In this approach, the user constructs some alternate tree (the "component hierarchy") that is not the output itself, but rather a scaffolding for building the output. This tree has roughly the same shape as the output.
The user usually doesn't reference the output directly - that is, the user doesn't keep around a reference to the DOM node or entity, instead they hold on to a reference to the scaffolding (an instantiated template), which in turn holds a reference to the output roots. Updating the parameters of this template causes a regeneration of the output.
In this approach, the parameter state is typically stored as properties within the scaffolding nodes.
Temporary scaffolding is a tree that is newly constructed each time (Xylem). In this approach, we regenerate the scaffolding every time from code, throwing it away as soon as we are done. In order for this approach to work, we need three for things to be true:
Scaffolding embedded within the output as hidden metadata. In the case of Bevy, this can often be done by hidden components (hidden in the sense that they have no effect on the entity's appearance). However, we frequently run into cases where there's no unambiguous single entity that should "own" the scaffolding: examples are templates with multiple roots, or conditional blocks with multiple children. In these cases, we need to insert additional entities, such as ghost nodes, into the tree to hang on to this data.
Differencing
All of the solutions mentioned use some form of "diffing", in the sense that there's some code that compares the old state to the new. However, this may or may not involve actually walking the entire output tree and comparing every property.
In fact, all diffing solutions are selective to some extent: just blindly iterating over all properties (via reflection) doesn't work, because there are some properties that need to be able to update on a different schedule. This is true in React as well: popular animation libraries such as
react-spring
andframer-motion
directly update animatable properties in the DOM, bypassing the diffing mechanism, as this is the only way they can achieve high frame rates cheaply.This is one of the reasons why React uses a VDOM (Virtual DOM). Not only does it avoid slow DOM operations like appending a node, but the VDOM only contains properties that are meant to be diffed, and doesn't include properties that we want to mutate directly.
For a VDOM, it's pretty simple: if the user mentions a property in the template, then it's meant to be diffed; otherwise leave it alone. This requires a data structure that supports a sparse representation of properties, which entities and components certainly do not.
This has consequences for Bevy: it means that a naive approach which attempts to diff components using reflection won't work, since the diff algorithm would have no way to know which properties are suitable for diffing and which are not (particularly because this decision is highly context-sensitive). Instead, you'd have to build some separate AST-like structure which could then be transformed into entities. This is effectively how Dioxus works, if I understand things correctly.
Some frameworks (like Svelte and Solid) know at compile time which parts of the output hierarchy are static and which are dynamic (they can look for dynamic control-flow elements in the template), and avoid diffing the static parts.
You can take this even further and apply diffing at the parameter state level instead of diffing the output tree, at which point your incremental strategy becomes a kind of memoization of sub-trees rather than a classic "diff". This also avoids wasting time rebuilding parts of the tree that don't change, and which would be discarded after discovering that the diff detected no changes.
Is BSN incremental?
BSN, as currently prototyped, does involve something called a "patch". However, the patch mechanism in BSN is designed to facilitate composition, not dynamic updates. It allows you to bring together multiple sources during construction. It's not designed to modify entities once construction is completed.
Working with BSN macros right now is a little bit like the "temporary scaffolding" approach: you produce an
impl Scene
, this is used to construct an entity tree, and then theScene
is disposed. It doesn't have any long-lived parameter state, which would require a much different API.There are a couple of ways that BSN could be extended to support incrementalization:
The second approach is the one I am currently experimenting with. The basic idea is that you have components whose job it is to perform dynamic updates on the tree. As far as BSN knows, these are just components like any other, there's no special support for them. The downside is that, without any kind of special syntactic sugar for dynamism, the syntax for defining these components is a bit boilerplatey. My approach also continues to rely on the ghost nodes feature.
Beta Was this translation helpful? Give feedback.
All reactions