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

Encoded DOM traversal sequences #15478

Open
Ocean-OS opened this issue Mar 9, 2025 · 2 comments
Open

Encoded DOM traversal sequences #15478

Ocean-OS opened this issue Mar 9, 2025 · 2 comments

Comments

@Ocean-OS
Copy link
Contributor

Ocean-OS commented Mar 9, 2025

Describe the problem

Svelte's toolbox for DOM traversal is sweet and simple, primarily composed of $.next and $.child calls to access siblings and children respectively. This method works well for small components, but can become rather unwieldy for larger components. In larger components, reactive elements are often nested, which means that to access one element, your compiled code could have 3+ lines of:

let element = $.child(fragment);
let element_1 = $.child(element);
let element_2 = $.child(element_1);

This is not only repetitive, but hard to minify, as the best minification is suboptimal:

let c = a.child(a.child(a.child(b)));

Describe the proposed solution

I think that an encoded DOM traversal sequence could be useful here. Essentially, the compiler would take the sequence of nodes accessed in the component fragment and serialize it as a string. Marko does something similar to this, and I think it'd fit well for Svelte. Here's a quick look at how this could work:

let sequence = $.sequence(`|>|^|`);
let root = $.template(`<h1> </h1><button> </button>`, 1);
let on_click = (_, count) => $.update(count);
export default function Component($$anchor) {
    let fragment = root();
    let next = sequence(fragment);
    let name = 'world';
    let count = $.state(0);
    let h1 = next();
    h1.textContent = `Hello, ${name ?? ''}`;
    let button = next();
    button.__click = [on_click, count];
    let text = next();
    $.reset(button);
    $.template_effect(() => $.set_text(text, `Count is ${$.get(count) ?? ''}`));
    $.append($$anchor, fragment);
}

In the above example, $.sequence takes an encoded sequence for DOM traversal. The encoding uses characters to represent different operations:

  • >: nextSibling
  • ^: firstChild
  • |: stop and return current node

So, for the example above, the sequence could be interpreted as:

  1. Get the first child (<h1>)
  2. Get the sibling of the h1 (<button>)
  3. Get the child of the button (the text)

$.sequence's definition could look something like this:

function sequence(code) {
    const [...chars] = code;
    const len = chars.length;
    return function(fragment) {
        let index = 0;
        let curr = fragment?.firstChild;

        function next() {
            curr = curr?.nextSibling;
            // hydration stuff here...
        }

        function child() {
            curr = curr?.firstChild;
            // hydration stuff here...
        }
        return function() {
            while (index < len) {
                let char = chars[index++];
                switch (char) {
                    case '>':
                        next();
                        break;
                    case '^':
                        child();
                        break;
                    case '|':
                        return curr;
                }
            }
        }
    }
}

The encoding could also be shortened for repeated operations (for example, >3 could mean going to the third sibling). However, I think that the encoding should be relatively simple, because I, like probably a lot of other contributors, learned how Svelte's compiler and internals work by reading and analyzing the compiled output, and I wouldn't want this feature to make the compiled output too difficult for possible contributors to understand.

Additional Implementation Details

This would rewrite a bit of these files, but the changes shouldn't be too complex; instead of generating $.next, and $.child (and variables for each interim node), it would just push characters to something like context.state.sequence and generate calls to next. Additionally, since each sequence is unique to its template, I think it'd be best if the sequence were to be included as arguments to $.template, and fragment and next could be retrieved via destructuring:

let root = $.template(`<h1></h1><p></p>`, `|>|^|`, 1);
//...
let [fragment, next] = root();

Importance

nice to have

@paoloricciuti
Copy link
Member

This is generally a non issue with gzip tho since all those instructions gets compressed pretty well. We also have $.next(3); when we need to skip multiple...I wonder if we could ro the same for $.child

@trueadm
Copy link
Contributor

trueadm commented Mar 10, 2025

I tried the encoding approach – it didn't really make the total size much better when it comes to gzip and made the code and logic harder to follow. Furthermore, it had an impact on runtime performance too – as we have other operations other than child and and next, especially when it comes to hydration as we'd now have to ship all the code for each of the functions because of the switch statement vs dead code eliminating them as we do now if they're not used.

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

No branches or pull requests

3 participants