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

[labs/ssr-react] Add package for integrating Lit SSR to React #3605

Merged
merged 15 commits into from Feb 8, 2023
Merged

Conversation

augustjk
Copy link
Member

Resolves #3588

This package contains tools for integrating Lit SSR with React for deeply server rendering Lit components.

A separate package for integrating these with Next.js is forthcoming(#3589)

@changeset-bot
Copy link

changeset-bot bot commented Jan 26, 2023

🦋 Changeset detected

Latest commit: 8d97073

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

This PR includes changesets to release 1 package
Name Type
@lit-labs/ssr-react 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

@github-actions
Copy link
Contributor

github-actions bot commented Jan 26, 2023

📊 Tachometer Benchmark Results

Summary

nop-update

  • lit-html-kitchen-sink: unsure 🔍 -2% - +16% (-0.42ms - +3.68ms)
    this-change vs tip-of-tree

render

  • lit-element-list: 76.45ms - 81.87ms
  • lit-html-kitchen-sink: unsure 🔍 -4% - +5% (-1.19ms - +1.48ms)
    this-change vs tip-of-tree
  • lit-html-repeat: unsure 🔍 -7% - +4% (-0.81ms - +0.42ms)
    this-change vs tip-of-tree
  • lit-html-template-heavy: unsure 🔍 -2% - +2% (-0.88ms - +1.35ms)
    this-change vs tip-of-tree
  • reactive-element-list: unsure 🔍 -1% - +3% (-0.57ms - +1.77ms)
    this-change vs tip-of-tree

update

  • lit-element-list: 783.98ms - 798.33ms
  • lit-html-kitchen-sink: unsure 🔍 -4% - +6% (-3.36ms - +4.63ms)
    this-change vs tip-of-tree
  • lit-html-repeat: unsure 🔍 -3% - +1% (-8.02ms - +4.52ms)
    this-change vs tip-of-tree
  • lit-html-template-heavy: unsure 🔍 -0% - +2% (-0.33ms - +2.57ms)
    this-change vs tip-of-tree
  • reactive-element-list: unsure 🔍 -1% - +0% (-8.63ms - +4.01ms)
    this-change vs tip-of-tree

update-reflect

  • lit-element-list: 786.81ms - 795.72ms
  • reactive-element-list: unsure 🔍 -1% - +1% (-5.64ms - +6.43ms)
    this-change vs tip-of-tree

Results

lit-element-list

render

VersionAvg timevs
76.45ms - 81.87ms-

update

VersionAvg timevs
783.98ms - 798.33ms-

update-reflect

VersionAvg timevs
786.81ms - 795.72ms-
lit-html-kitchen-sink

render

VersionAvg timevs this-change
vs tip-of-tree
tip-of-tree
vs previous-release
previous-release
this-change
30.57ms - 32.73ms-unsure 🔍
-4% - +5%
-1.19ms - +1.48ms
unsure 🔍
-4% - +4%
-1.19ms - +1.32ms
tip-of-tree
tip-of-tree
30.72ms - 32.30msunsure 🔍
-5% - +4%
-1.48ms - +1.19ms
-unsure 🔍
-3% - +3%
-1.09ms - +0.93ms
previous-release
previous-release
30.96ms - 32.22msunsure 🔍
-4% - +4%
-1.32ms - +1.19ms
unsure 🔍
-3% - +3%
-0.93ms - +1.09ms
-

update

VersionAvg timevs this-change
vs tip-of-tree
tip-of-tree
vs previous-release
previous-release
this-change
80.59ms - 84.64ms-unsure 🔍
-4% - +6%
-3.36ms - +4.63ms
unsure 🔍
-0% - +6%
-0.32ms - +5.13ms
tip-of-tree
tip-of-tree
78.54ms - 85.42msunsure 🔍
-6% - +4%
-4.63ms - +3.36ms
-unsure 🔍
-3% - +7%
-2.12ms - +5.67ms
previous-release
previous-release
78.39ms - 82.03msunsure 🔍
-6% - +0%
-5.13ms - +0.32ms
unsure 🔍
-7% - +3%
-5.67ms - +2.12ms
-

nop-update

VersionAvg timevs this-change
vs tip-of-tree
tip-of-tree
vs previous-release
previous-release
this-change
23.31ms - 26.54ms-unsure 🔍
-2% - +16%
-0.42ms - +3.68ms
unsure 🔍
-8% - +9%
-1.97ms - +2.25ms
tip-of-tree
tip-of-tree
22.03ms - 24.57msunsure 🔍
-14% - +1%
-3.68ms - +0.42ms
-unsure 🔍
-13% - +1%
-3.35ms - +0.38ms
previous-release
previous-release
23.42ms - 26.16msunsure 🔍
-9% - +8%
-2.25ms - +1.97ms
unsure 🔍
-2% - +15%
-0.38ms - +3.35ms
-
lit-html-repeat

render

VersionAvg timevs this-change
vs tip-of-tree
tip-of-tree
vs previous-release
previous-release
this-change
11.02ms - 11.98ms-unsure 🔍
-7% - +4%
-0.81ms - +0.42ms
unsure 🔍
-7% - +3%
-0.82ms - +0.39ms
tip-of-tree
tip-of-tree
11.31ms - 12.08msunsure 🔍
-4% - +7%
-0.42ms - +0.81ms
-unsure 🔍
-5% - +4%
-0.55ms - +0.51ms
previous-release
previous-release
11.35ms - 12.08msunsure 🔍
-3% - +7%
-0.39ms - +0.82ms
unsure 🔍
-4% - +5%
-0.51ms - +0.55ms
-

update

VersionAvg timevs this-change
vs tip-of-tree
tip-of-tree
vs previous-release
previous-release
this-change
304.10ms - 312.67ms-unsure 🔍
-3% - +1%
-8.02ms - +4.52ms
unsure 🔍
-4% - +0%
-12.18ms - +0.55ms
tip-of-tree
tip-of-tree
305.55ms - 314.72msunsure 🔍
-1% - +3%
-4.52ms - +8.02ms
-unsure 🔍
-3% - +1%
-10.63ms - +2.51ms
previous-release
previous-release
309.49ms - 318.91msunsure 🔍
-0% - +4%
-0.55ms - +12.18ms
unsure 🔍
-1% - +3%
-2.51ms - +10.63ms
-
lit-html-template-heavy

render

VersionAvg timevs this-change
vs tip-of-tree
tip-of-tree
vs previous-release
previous-release
this-change
53.64ms - 55.05ms-unsure 🔍
-2% - +2%
-0.88ms - +1.35ms
unsure 🔍
-2% - +2%
-0.85ms - +1.18ms
tip-of-tree
tip-of-tree
53.25ms - 54.98msunsure 🔍
-2% - +2%
-1.35ms - +0.88ms
-unsure 🔍
-2% - +2%
-1.20ms - +1.06ms
previous-release
previous-release
53.45ms - 54.92msunsure 🔍
-2% - +2%
-1.18ms - +0.85ms
unsure 🔍
-2% - +2%
-1.06ms - +1.20ms
-

update

VersionAvg timevs this-change
vs tip-of-tree
tip-of-tree
vs previous-release
previous-release
this-change
117.83ms - 120.28ms-unsure 🔍
-0% - +2%
-0.33ms - +2.57ms
unsure 🔍
-1% - +1%
-1.63ms - +1.71ms
tip-of-tree
tip-of-tree
117.15ms - 118.71msunsure 🔍
-2% - +0%
-2.57ms - +0.33ms
-unsure 🔍
-2% - +0%
-2.46ms - +0.29ms
previous-release
previous-release
117.88ms - 120.15msunsure 🔍
-1% - +1%
-1.71ms - +1.63ms
unsure 🔍
-0% - +2%
-0.29ms - +2.46ms
-
reactive-element-list

render

VersionAvg timevs this-change
vs tip-of-tree
tip-of-tree
vs previous-release
previous-release
this-change
51.97ms - 53.89ms-unsure 🔍
-1% - +3%
-0.57ms - +1.77ms
unsure 🔍
-3% - +2%
-1.70ms - +0.86ms
tip-of-tree
tip-of-tree
51.66ms - 53.00msunsure 🔍
-3% - +1%
-1.77ms - +0.57ms
-unsure 🔍
-4% - +0%
-2.11ms - +0.06ms
previous-release
previous-release
52.50ms - 54.20msunsure 🔍
-2% - +3%
-0.86ms - +1.70ms
unsure 🔍
-0% - +4%
-0.06ms - +2.11ms
-

update

VersionAvg timevs this-change
vs tip-of-tree
tip-of-tree
vs previous-release
previous-release
this-change
811.65ms - 821.13ms-unsure 🔍
-1% - +0%
-8.63ms - +4.01ms
unsure 🔍
-1% - +0%
-10.69ms - +2.40ms
tip-of-tree
tip-of-tree
814.53ms - 822.88msunsure 🔍
-0% - +1%
-4.01ms - +8.63ms
-unsure 🔍
-1% - +1%
-7.98ms - +4.31ms
previous-release
previous-release
816.02ms - 825.06msunsure 🔍
-0% - +1%
-2.40ms - +10.69ms
unsure 🔍
-1% - +1%
-4.31ms - +7.98ms
-

update-reflect

VersionAvg timevs this-change
vs tip-of-tree
tip-of-tree
vs previous-release
previous-release
this-change
831.77ms - 839.80ms-unsure 🔍
-1% - +1%
-5.64ms - +6.43ms
unsure 🔍
-1% - +1%
-6.22ms - +5.29ms
tip-of-tree
tip-of-tree
830.89ms - 839.90msunsure 🔍
-1% - +1%
-6.43ms - +5.64ms
-unsure 🔍
-1% - +1%
-6.97ms - +5.25ms
previous-release
previous-release
832.12ms - 840.38msunsure 🔍
-1% - +1%
-5.29ms - +6.22ms
unsure 🔍
-1% - +1%
-5.25ms - +6.97ms
-

tachometer-reporter-action v2 for Benchmarks

Copy link
Member

@aomarks aomarks left a comment

Choose a reason for hiding this comment

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

Just some preliminary thoughts/question, only looked at the README/package.json so far!

packages/labs/ssr-react/README.md Outdated Show resolved Hide resolved
packages/labs/ssr-react/README.md Outdated Show resolved Hide resolved
packages/labs/ssr-react/README.md Show resolved Hide resolved
packages/labs/ssr-react/README.md Outdated Show resolved Hide resolved
packages/labs/ssr-react/package.json Outdated Show resolved Hide resolved
packages/labs/ssr-react/README.md Outdated Show resolved Hide resolved
Copy link
Collaborator

@justinfagnani justinfagnani left a comment

Choose a reason for hiding this comment

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

Just through the readme mostly.

packages/labs/ssr-react/.gitignore Outdated Show resolved Hide resolved
packages/labs/ssr-react/.gitignore Outdated Show resolved Hide resolved
packages/labs/ssr-react/README.md Outdated Show resolved Hide resolved
packages/labs/ssr-react/README.md Outdated Show resolved Hide resolved
packages/labs/ssr-react/README.md Outdated Show resolved Hide resolved
packages/labs/ssr-react/README.md Outdated Show resolved Hide resolved
packages/labs/ssr-react/README.md Outdated Show resolved Hide resolved
packages/labs/ssr-react/README.md Outdated Show resolved Hide resolved
packages/labs/ssr-react/README.md Outdated Show resolved Hide resolved
packages/labs/ssr-react/README.md Outdated Show resolved Hide resolved
@augustjk augustjk marked this pull request as draft January 27, 2023 22:14
@augustjk
Copy link
Member Author

Thanks for the initial feedback! Marking as draft to redo some parts of this before putting it up for review again.

@augustjk augustjk force-pushed the ssr-react branch 2 times, most recently from fd6b0ff to aab935d Compare February 1, 2023 00:49
Copy link
Collaborator

@justinfagnani justinfagnani left a comment

Choose a reason for hiding this comment

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

🎉

"node": "./node/enable-lit-ssr.js",
"default": "./enable-lit-ssr.js"
},
"./jsx-dev-runtime": {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Add .js extension

Copy link
Member Author

Choose a reason for hiding this comment

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

"LitElement",
"ssr",
"lit-ssr",
"react"
Copy link
Collaborator

Choose a reason for hiding this comment

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

add "web components" and "next". If there's a limit, I don't know if we need lit-element and variations.

Copy link
Member Author

Choose a reason for hiding this comment

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

Done

/**
* @fileoverview Side-effect import meant to be loaded in the **browser** before
* any user code is loaded. Installs hydration support for `LitElement`.
*/
Copy link
Collaborator

Choose a reason for hiding this comment

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

why do we need this file in addition to lit/experimental-hydrate-support.js?

Copy link
Member Author

Choose a reason for hiding this comment

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

With projects like Next.js that use the same source code for both server and client, I figured it's a convenience for users to just import this once at the top, and it'll handle the monkey patching on the server, and hydration import on the client.

import 'lit/experimental-hydrate-support.js';

// eslint-disable-next-line import/extensions
export {jsxDEV} from 'react/jsx-dev-runtime';
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why doesn't this have Fragment and jsxs? Should it be an export *?

Is there a link to docs on the module exports contract for this module?

Copy link
Member Author

Choose a reason for hiding this comment

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

Good catch for the Fragment. Added. I think it's better to be explicit rather than export * so we know it exports everything that's needed. It doesn't have a separate jsxs because in dev mode the jsxDEV serves both purpose and gets a isStaticChildren boolean as a fourth argument instead instead.

There was no canonical doc that specifies this contract. Things I referenced were:

Copy link
Collaborator

Choose a reason for hiding this comment

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

I ask about export * because if this module needs to implement a certain contract that we know that react/jsx-dev-runtime does, then the easiest way to guarantee that we meet the contract is to export * that module. It fits the spirit of monkey-patching that module, which is kind of what we're emulating.


import 'lit/experimental-hydrate-support.js';
// eslint-disable-next-line import/extensions
export {Fragment, jsx, jsxs} from 'react/jsx-runtime';
Copy link
Collaborator

Choose a reason for hiding this comment

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

Should this be export *?

Copy link
Member Author

Choose a reason for hiding this comment

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

I like the explicit better and the exports here really shouldn't change.

* the server. It is simply a passthrough of `React.createElement` in the
* browser.
*/
export const createElement = wrapCreateElement(React.createElement);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can we not do this with an if (isServer) check and have one module?

Copy link
Member Author

Choose a reason for hiding this comment

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

We could but everything else in this package already uses the conditional export. What's the reason for wanting to consolidate to a single module using isServer?


const {mode = 'open', delegatesFocus} = renderer.shadowRootOptions;
const templateAttributes = {
shadowroot: mode,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Change to shadowrootmode: mode for compatibility with Safari and streaming SSR. We need to update our polyfill too.

Copy link
Member Author

Choose a reason for hiding this comment

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

Should we keep the existing shadowroot: mode as well for compatibility with older Chrome versions? Per Elliott's testing, it seems fine to have both present and will have the streaming behavior for supported browsers. The streaming DSD feature that also renames the attribute is estimated to ship in Chrome 111 https://chromestatus.com/feature/5161240576393216

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think we can launch with just the new attribute considering that we have no existing users. We'd have to deprecate the old attribute right away anyway.

Copy link
Member Author

Choose a reason for hiding this comment

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

I'm hesitant on launching with an attribute that doesn't work on any currently shipping stable browsers. Our core labs/ssr package also does both which means nested custom elements will get both attributes. We should match that behavior and deprecate the old attr at the same time as when we do so on in labs/ssr.

Copy link
Collaborator

Choose a reason for hiding this comment

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

"launch" is a loose term here. Does this PR put the package in releasable state? Chrome 111 goes beta next well and stable in 4 weeks. Safari TP will be promoted relatively soon.

Either way is ultimately fine, though I do want to remove the old attribute as soon as possib.e.

Copy link
Member Author

Choose a reason for hiding this comment

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

This PR does put the package in a releasable state. I think removing the old attribute in sync with core labs/ssr makes sense.


// elementAttributes will be provided to React as props for the host element
// for properly rendering reflected attributes
const elementAttributes =
Copy link
Collaborator

Choose a reason for hiding this comment

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

We should update the ElementRenderer interface to have a getAttributes() API.

Copy link
Member Author

Choose a reason for hiding this comment

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

Filed #3630

packages/labs/ssr-react/README.md Outdated Show resolved Hide resolved
packages/labs/ssr-react/README.md Outdated Show resolved Hide resolved

### Using the Automatic Runtime JSX Transform

If your project is using the [runtime JSX transform](https://reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html), this package can serve as the JSX import source.
Copy link
Member

Choose a reason for hiding this comment

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

Do we have a solution at all for projects that use dependencies compiled with the runtime JSX transform? Do we need to note this case?

Copy link
Member Author

Choose a reason for hiding this comment

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

It should be a very unlikely use case that affects external React components pre-compiled with auto runtime transform that directly make use of Lit components without the labs/react wrapper. But added a couple sentences to the bottom of this section regarding that use case.

* Renders the shadow contents of the provided custom element type with props.
* Should only be called in server environments.
*/
export const renderShadowContents = (type: string, props: {} | null) => {
Copy link
Member

Choose a reason for hiding this comment

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

There's almost nothing React-specific about this, right? This is generally useful because it wraps up the minimum steps needed to transform a tagName, attrs, and props from a call site into the reflected attrs and SR contents using ElementRenderer (something others have stumbled over as well). e.g. the Astro integration could use the same logic.

Can we move this to labs/ssr, with an additional attrs argument, and then this file would only add specific logic for separating React props into CE attrs & props?

Copy link
Member

Choose a reason for hiding this comment

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

Naming nit: since this is more than just rendering "shadow contents" (technically it handles props -> reflected attributes as well), renderCustomElement might be better? Also type -> tagName would be clearer.

Copy link
Member Author

Choose a reason for hiding this comment

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

Renamed. I agree that this could very well be part of labs/ssr but want to punt it to make sure we can separately discuss the proper API for making it more general use.

Copy link
Member Author

Choose a reason for hiding this comment

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

Filed #3656

},
});

return originalCreateElement(
Copy link
Member

Choose a reason for hiding this comment

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

Should be outside the if (shadowContents !== undefined) test? We should still render a CE that doesn't render a SR for whatever reason.

Copy link
Member Author

Choose a reason for hiding this comment

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

It would fall back to the return originalCreateElement(type, props, ...children) outside the if (isCustomElement(type)) block in that case.

Comment on lines 56 to 67
if (isCustomElement(type)) {
const {shadowContents, elementAttributes, templateAttributes} =
renderShadowContents(type, props);

if (shadowContents) {
const templateShadowRoot = createElement('template', {
...templateAttributes,
dangerouslySetInnerHTML: {
__html: [...shadowContents].join(''),
},
});

Copy link
Member

Choose a reason for hiding this comment

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

This code is duplicated 4x, is there a way to factor it better? This can't all be just wrapCreateElement I guess because there are slight differences in how children, keys, etc. are handled between the different fns, but maybe this inner part could be factored?

Copy link
Member Author

Choose a reason for hiding this comment

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

The renderShadowContents() (or maybe now-to-be-called renderCustomElement()) was as much commonality I could pull out at the time. Though I think there might be a better way to deduplicate all of this. I'm thinking of actually creating a custom React element to handle the adding of template shadowroot so it would not need to have bespoke handling of the different transforms. I'll see if this idea pans out and push up a commit with it.

Copy link
Collaborator

Choose a reason for hiding this comment

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

@augustjk I would be tempted to TODO the deduplication and try a component in a follow-on PR.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, it would just be implementation details and not affect the outward API so seems okay to punt that. @aomarks had the same thought while discussing offline.

Co-authored-by: Kevin Schaaf <kschaaf@google.com>
Copy link
Member

@aomarks aomarks left a comment

Choose a reason for hiding this comment

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

Awesome!

@augustjk augustjk merged commit b731bb5 into main Feb 8, 2023
@augustjk augustjk deleted the ssr-react branch February 8, 2023 00:39
@lit-robot lit-robot mentioned this pull request Mar 10, 2023
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.

[labs/ssr-react] Lit SSR React integration
4 participants