Skip to content

chore: Reduce hydration comment for {:else if} #15250

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

Merged
merged 7 commits into from
Mar 3, 2025

Conversation

adiguba
Copy link
Contributor

@adiguba adiguba commented Feb 9, 2025

Using {#if} {:else if} {/if} in template can produce lots of hydration code, since each {#if} or {:else if} will use a new block (with 2 possible hydration comments).

Example in this issue : #15200

I modified if_block() in order to handle that differently, using a single block for all if/else if.

Take example of this dummy code :

{#if prop === 1}
	<div>one</div>
{:else if prop === 2}
	<div>two</div>
{:else if prop === 3}
	<div>three</div>
{:else if prop === 4}
	<div>four</div>
{:else if prop === 5}
	<div>five</div>
{:else}
	<div>...</div>
{/if}

Currently, this can produces :

  • if prop === 1: <!--[--><div>one</div><!--]-->
  • if prop === 2: <!--[!--><!--[--><div>two</div><!--]--><!--]-->
  • if prop === 3: <!--[!--><!--[!--><!--[--><div>three</div><!--]--><!--]--><!--]-->
  • if prop === 4: <!--[!--><!--[!--><!--[!--><!--[--><div>four</div><!--]--><!--]--><!--]--><!--]-->
  • if prop === 5: <!--[!--><!--[!--><!--[!--><!--[!--><!--[--><div>five</div><!--]--><!--]--><!--]--><!--]--><!--]-->
  • else : <!--[!--><!--[!--><!--[!--><!--[!--><!--[!--><!--[!--><div>...</div><!--]--><!--]--><!--]--><!--]--><!--]--><!--]-->

With this change, it will produce only one hydration block in all case :

  • if prop === 1: <!--[--><div>one</div><!--]-->
  • if prop === 2: <!--[1--><div>two</div><!--]-->
  • if prop === 3: <!--[2--><div>three</div><!--]-->
  • if prop === 4: <!--[3--><div>four</div><!--]-->
  • if prop === 5: <!--[4--><div>five</div><!--]-->
  • if else : <!--[!--><div>...</div><!--]-->

The generated code is also reduced and simplified.
On client side the generated code is :

{
    var consequent = ($$anchor) => {
      var div = root_1();

      $.append($$anchor, div);
    };

    var alternate_4 = ($$anchor) => {
      var fragment_1 = $.comment();
      var node_1 = $.first_child(fragment_1);

      {
        var consequent_1 = ($$anchor) => {
          var div_1 = root_3();

          $.append($$anchor, div_1);
        };

        var alternate_3 = ($$anchor) => {
          var fragment_2 = $.comment();
          var node_2 = $.first_child(fragment_2);

          {
            var consequent_2 = ($$anchor) => {
              var div_2 = root_5();

              $.append($$anchor, div_2);
            };

            var alternate_2 = ($$anchor) => {
              var fragment_3 = $.comment();
              var node_3 = $.first_child(fragment_3);

              {
                var consequent_3 = ($$anchor) => {
                  var div_3 = root_7();

                  $.append($$anchor, div_3);
                };

                var alternate_1 = ($$anchor) => {
                  var fragment_4 = $.comment();
                  var node_4 = $.first_child(fragment_4);

                  {
                    var consequent_4 = ($$anchor) => {
                      var div_4 = root_9();

                      $.append($$anchor, div_4);
                    };

                    var alternate = ($$anchor) => {
                      var div_5 = root_10();

                      $.append($$anchor, div_5);
                    };

                    $.if(
                      node_4,
                      ($$render) => {
                        if ($$props.prop === 5) $$render(consequent_4); else $$render(alternate, false);
                      },
                      true
                    );
                  }

                  $.append($$anchor, fragment_4);
                };

                $.if(
                  node_3,
                  ($$render) => {
                    if ($$props.prop === 4) $$render(consequent_3); else $$render(alternate_1, false);
                  },
                  true
                );
              }

              $.append($$anchor, fragment_3);
            };

            $.if(
              node_2,
              ($$render) => {
                if ($$props.prop === 3) $$render(consequent_2); else $$render(alternate_2, false);
              },
              true
            );
          }

          $.append($$anchor, fragment_2);
        };

        $.if(
          node_1,
          ($$render) => {
            if ($$props.prop === 2) $$render(consequent_1); else $$render(alternate_3, false);
          },
          true
        );
      }

      $.append($$anchor, fragment_1);
    };

    $.if(node, ($$render) => {
      if ($$props.prop === 1) $$render(consequent); else $$render(alternate_4, false);
    });
  }

This will become :

  var consequent = ($$anchor) => {
    var div = root_1();

    $.append($$anchor, div);
  };

  var alternate = ($$anchor, $$elseif) => {
    var consequent_1 = ($$anchor) => {
      var div_1 = root_3();

      $.append($$anchor, div_1);
    };

    var alternate_1 = ($$anchor, $$elseif) => {
      var consequent_2 = ($$anchor) => {
        var div_2 = root_5();

        $.append($$anchor, div_2);
      };

      var alternate_2 = ($$anchor, $$elseif) => {
        var consequent_3 = ($$anchor) => {
          var div_3 = root_7();

          $.append($$anchor, div_3);
        };

        var alternate_3 = ($$anchor, $$elseif) => {
          var consequent_4 = ($$anchor) => {
            var div_4 = root_9();

            $.append($$anchor, div_4);
          };

          var alternate_4 = ($$anchor) => {
            var div_5 = root_10();

            $.append($$anchor, div_5);
          };

          $.if(
            $$anchor,
            ($$render) => {
              if (prop() === 5) $$render(consequent_4); else $$render(alternate_4, false);
            },
            $$elseif
          );
        };

        $.if(
          $$anchor,
          ($$render) => {
            if (prop() === 4) $$render(consequent_3); else $$render(alternate_3, false);
          },
          $$elseif
        );
      };

      $.if(
        $$anchor,
        ($$render) => {
          if (prop() === 3) $$render(consequent_2); else $$render(alternate_2, false);
        },
        $$elseif
      );
    };

    $.if(
      $$anchor,
      ($$render) => {
        if (prop() === 2) $$render(consequent_1); else $$render(alternate_1, false);
      },
      $$elseif
    );
  };

  $.if(node, ($$render) => {
    if (prop() === 1) $$render(consequent); else $$render(alternate, false);
  });

Note : I also removed the JS scope block { ... }. I think this is useless...

Same thing on server side, where this code :

if (prop === 1) {
    $$payload.out += '<!--[-->';
    $$payload.out += `<div>one</div>`;
  } else {
    $$payload.out += '<!--[!-->';

    if (prop === 2) {
      $$payload.out += '<!--[-->';
      $$payload.out += `<div>two</div>`;
    } else {
      $$payload.out += '<!--[!-->';

      if (prop === 3) {
        $$payload.out += '<!--[-->';
        $$payload.out += `<div>three</div>`;
      } else {
        $$payload.out += '<!--[!-->';

        if (prop === 4) {
          $$payload.out += '<!--[-->';
          $$payload.out += `<div>four</div>`;
        } else {
          $$payload.out += '<!--[!-->';

          if (prop === 5) {
            $$payload.out += '<!--[-->';
            $$payload.out += `<div>five</div>`;
          } else {
            $$payload.out += '<!--[!-->';
            $$payload.out += `<div>...</div>`;
          }

          $$payload.out += `<!--]-->`;
        }

        $$payload.out += `<!--]-->`;
      }

      $$payload.out += `<!--]-->`;
    }

    $$payload.out += `<!--]-->`;
  }

  $$payload.out += `<!--]-->`;

will become :

  if (prop === 1) {
    $$payload.out += "<!--[-->";
    $$payload.out += `<div>one</div>`;
  } else if (prop === 2) {
    $$payload.out += "<!--[1-->";
    $$payload.out += `<div>two</div>`;
  } else if (prop === 3) {
    $$payload.out += "<!--[2-->";
    $$payload.out += `<div>three</div>`;
  } else if (prop === 4) {
    $$payload.out += "<!--[3-->";
    $$payload.out += `<div>four</div>`;
  } else if (prop === 5) {
    $$payload.out += "<!--[4-->";
    $$payload.out += `<div>five</div>`;
  } else {
    $$payload.out += "<!--[!-->";
    $$payload.out += `<div>...</div>`;
  }
  $$payload.out += `<!--]-->`;

Before submitting the PR, please make sure you do the following

  • It's really useful if your PR references an issue where it is discussed ahead of time. In many cases, features are absent for a reason. For large changes, please create an RFC: https://github.com/sveltejs/rfcs
  • Prefix your PR title with feat:, fix:, chore:, or docs:.
  • This message body should clearly illustrate what problems it solves.
  • Ideally, include a test that fails without this PR but passes with it.
  • If this PR changes code within packages/svelte/src, add a changeset (npx changeset).

Tests and linting

  • Run the tests with pnpm test and lint the project with pnpm lint

Copy link

changeset-bot bot commented Feb 9, 2025

🦋 Changeset detected

Latest commit: 1d5bedb

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
svelte Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Copy link
Contributor

github-actions bot commented Feb 9, 2025

Playground

pnpm add https://pkg.pr.new/svelte@15250

@adiguba
Copy link
Contributor Author

adiguba commented Feb 9, 2025

Some little change so I edited the first message.

And a question : since if_block()'s signature has changed it's incompatible with previous version.

-if_block(..., elseif: boolean)
+if_block(..., elseif: number[])

I'm not sure if this is OK, or if we have to deal with a compatibility layer.

@paoloricciuti
Copy link
Member

I'm not sure if this is OK, or if we have to deal with a compatibility layer.

Compiler output is not subject to semver so it's fine


if (!!condition === is_else) {
if (!!condition === is_else || isNaN(hydrate_index)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

We definitely don't want to be using isNaN anywhere, it's a major codesmell

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I didn't known that.
What the alternative to detect bad hydration comment ?

Copy link
Contributor

Choose a reason for hiding this comment

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

Validate it before doing parseInt?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

What about hydrate_index !== hydrate_index, which will be true only for NaN ?

Copy link
Contributor

Choose a reason for hiding this comment

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

I mean you also have to facilitate mismatches, where the SSR content is intentionally different from the client side logic too, right? So in that case wouldn't the indexes also not match up?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes in this case the indexes will not match.

Here it is rather the case where the comment is not from the Svelte hydration process...

Copy link
Member

Choose a reason for hiding this comment

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

Also don't fully understand what the problem here is - isn't this more straightforward/less code to just go "this isn't a number, so something else went really wrong"?

Copy link
Contributor

Choose a reason for hiding this comment

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

isNaN has been condemned to hell all over, you can research its history. I'd just rather avoid it and it's caveats where possible.

@trueadm
Copy link
Contributor

trueadm commented Feb 11, 2025

Note : I also removed the JS scope block { ... }. I think this is useless...

It wasn't useless, it was done intentionally to clearly show what blocks belong to the if block.

@adiguba
Copy link
Contributor Author

adiguba commented Feb 11, 2025

I didn't think the generated code had to be so explicit. I will restore it...

Copy link
Contributor

@trueadm trueadm left a comment

Choose a reason for hiding this comment

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

This is awesome, thank you.

@trueadm
Copy link
Contributor

trueadm commented Feb 27, 2025

It needs a changeset though, and is probably a minor given it's quite a big change.

@svelte-docs-bot
Copy link

@adiguba
Copy link
Contributor Author

adiguba commented Feb 27, 2025

Nice,

I added a chanset and merged in order to launch the test with the last version...

@trueadm trueadm merged commit 2032049 into sveltejs:main Mar 3, 2025
9 checks passed
@github-actions github-actions bot mentioned this pull request Mar 3, 2025
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

Successfully merging this pull request may close these issues.

4 participants