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

feat(ssr): shadow dom components can render as declarative-shadow-dom or as 'scoped' #6147

Merged
merged 16 commits into from
Feb 11, 2025

Conversation

johnjenkins
Copy link
Contributor

@johnjenkins johnjenkins commented Feb 5, 2025

What is the current behavior?

When generating a shadow: true component during SSR, Stencil renders a Declarative-Shadow-Dom (DSD).
Generally, this is preferable as it's natively supported in most browsers.

There are some scenarios at present, where DSD is not ideal:

  • The requirement to support older browsers
  • Rendering 10s to 100s of the same component*

(* e.g. given my-list-item component which is rendered 20 times. DSD will render 20 <style> tags which can negatively impact the initial weight of the page - especially without compression.)

GitHub Issue Number: N/A

What is the new behavior?

The ability to use Stencil's scoped behaviour has been extended for use during SSR.

During SSR a shadow: true scoped component will be rendered without a template - just plain, lightDOM html and a single <style /> tag added to the <head />.

On the client, these lightDOM nodes get added to a new shadow-root and the <style /> tag is added as a `constructible stylesheet'.

To this end, the serializeShadowRoot options (passed into renderToString()) now accepts a number of new options:

  • 'declarative-shadow-dom' - will render all shadow: true components as DSD
  • 'scoped' - will render all shadow: true components with Stencil's scoped behaviour
  • { 'declarative-shadow-dom': ['tag-1'], default: 'scoped'; } will render component tags tag-1 with DSD, but all others with Stencil's scoped behaviour
  • { 'scoped': ['tag-1'], default: 'declarative-shadow-dom'; } will render component tags tag-1 with Stencil's scoped behaviour, but all others with DSD
  • false - will disable all SSR render behaviour for shadow: true components
  • true - DEPRECATED will render all shadow: true components as DSD

Documentation

stenciljs/site#1507

Does this introduce a breaking change?

  • Yes
  • No

Testing

  • New unit test
  • New wdio tests

Other information

Copy link
Member

@christian-bromann christian-bromann left a comment

Choose a reason for hiding this comment

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

Some initial thoughts

@johnjenkins johnjenkins changed the title feat(ssr): shadowDOM can now render as dsd or 'scoped' feat(ssr): shadow dom components can render as declarative-shadow-dom or as 'scoped' Feb 5, 2025
@johnjenkins johnjenkins marked this pull request as ready for review February 7, 2025 00:32
@johnjenkins johnjenkins requested a review from a team as a code owner February 7, 2025 00:32
Copy link
Member

@christian-bromann christian-bromann left a comment

Choose a reason for hiding this comment

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

Awesome work 👏 some minor nit picks, once pipeline passes I am happy to merge.

@johnjenkins
Copy link
Contributor Author

johnjenkins commented Feb 7, 2025

@christian-bromann I had to un-deprecate false for now as I didn't realise it's required for the custom jest matchers

Copy link
Member

@christian-bromann christian-bromann left a comment

Choose a reason for hiding this comment

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

I've been running some tests and discovered that the order of elements within the light DOM differs between DSD and scoped mode, e.g.:

import { renderToString } from './test/wdio/hydrate/index.mjs';

const { html } = await renderToString(
  `
  <ssr-shadow-cmp>
    <p>Default slot content</p>
    <p slot="client-only">Client-only slot content</p>
  </ssr-shadow-cmp>
`,
  {
    fullDocument: false,
    serializeShadowRoot: true,
    constrainTimeouts: false,
    prettyHTML: true,
  },
);

console.log(1, html);

would return:

<ssr-shadow-cmp class="hydrated" s-id="1">
  <template shadowrootmode="open">
    <style>
      :host {
        display: block;
        padding: 10px;
        border: 2px solid #000;
        background: yellow;
        color: red;
      }
    </style>
    <div c-id="1.0.0.0">
      <slot name="top" c-id="1.1.1.0"></slot>
      <slot c-id="1.2.1.1"></slot>
    </div>
  </template>
  <!--r.1-->
  <p>Default slot content</p>
  <p slot="client-only">Client-only slot content</p>
</ssr-shadow-cmp>

But when setting serializeShadowRoot: 'scoped', I get:

<ssr-shadow-cmp class="sc-ssr-shadow-cmp-h hydrated" s-id="1">
  <!--r.1-->
  <!--o.0.2.-->
  <!--o.0.4.-->
  <p slot="client-only" hidden="" c-id="0.4">Client-only slot content</p>
  <div class="sc-ssr-shadow-cmp sc-ssr-shadow-cmp-s" c-id="1.0.0.0">
    <!--s.1.1.1.0.top-->
    <!--s.1.2.1.1.-->
    <p c-id="0.2" s-sn="">Default slot content</p> 
  </div>
</ssr-shadow-cmp>

Note: the order of the slots changing.

@johnjenkins
Copy link
Contributor Author

johnjenkins commented Feb 10, 2025

I think that makes sense @christian-bromann

in 1, the shadow dom is separate from the light dom and the browser takes care of how it’s rendered. The <p slot="client-only"> doesn't have a <slot /> home but will be hidden because that's the native behaviour.

in 2, there is no shadow dom so the dom gets resolved and rendered as it appears as a light dom tree.
In the example, the default slotted p element has a <slot /> 'home' - so get nested in the tree appropriately.

The <p slot="client-only"> has no <slot /> ‘home’ yet, so it gets hidden and added to the component root. When it goes through the scoped > shadow process during cliend-side hydration, it will remain in the light dom, get it's hidden removed and be in the same state as example 1.

Copy link
Member

@christian-bromann christian-bromann left a comment

Choose a reason for hiding this comment

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

Thanks for clarifying, that makes total sense to me 👍

Before we release this, can we raise a PR for the docs with an update?

@johnjenkins
Copy link
Contributor Author

Yep - will do tomorrow

@christian-bromann christian-bromann removed this pull request from the merge queue due to a manual request Feb 11, 2025
@christian-bromann christian-bromann merged commit 26e4aa3 into stenciljs:main Feb 11, 2025
71 checks passed
@johnjenkins johnjenkins deleted the feat-ssr-scoped-shadow branch February 11, 2025 19:07
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.

2 participants