Skip to content

Commit

Permalink
docs: add better docs for SSR support (#1490)
Browse files Browse the repository at this point in the history
* docs: add better docs for SSR support

* add more content

* refine

* add Nuxt as word to cspell

* Update docs/framework-integration/react.md

Co-authored-by: Tanner Reits <[email protected]>

* Update docs/framework-integration/react.md

Co-authored-by: Tanner Reits <[email protected]>

* Update docs/framework-integration/react.md

Co-authored-by: Tanner Reits <[email protected]>

* Update docs/framework-integration/vue.md

Co-authored-by: Tanner Reits <[email protected]>

* Update docs/framework-integration/vue.md

Co-authored-by: Tanner Reits <[email protected]>

* minor tweaks

---------

Co-authored-by: Tanner Reits <[email protected]>
  • Loading branch information
christian-bromann and tanner-reits authored Oct 24, 2024
1 parent a6eb8b3 commit 3e25c20
Show file tree
Hide file tree
Showing 4 changed files with 272 additions and 3 deletions.
1 change: 1 addition & 0 deletions cspell-wordlist.txt
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ expressjs
lifecycles
microtask
minifier
Nuxt
polyfilling
precache
prehydration
Expand Down
70 changes: 67 additions & 3 deletions docs/framework-integration/react.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ This package includes an output target for code generation that allows developer
- ♻️ Automate the generation of React component wrappers for Stencil components
- 🌐 Generate React functional component wrappers with JSX bindings for custom events and properties
- ⌨️ Typings and auto-completion for React components in your IDE
- 🚀 Support for Server Side Rendering (SSR) when used with frameworks like [Next.js](https://nextjs.org/)

To generate these framework wrappers, Stencil provides an Output Target library called [`@stencil/react-output-target`](https://www.npmjs.com/package/@stencil/react-output-target) that can be added to your `stencil.config.ts` file. This also enables Stencil components to be used within e.g. Next.js or other React based application frameworks.

Expand Down Expand Up @@ -129,10 +130,11 @@ In your `react-library` project, create a project specific `tsconfig.json` that
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./dist",
"lib": ["dom", "es2015"],
"module": "esnext",
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"moduleResolution": "bundler",
"target": "es2015",
"skipLibCheck": true,
"jsx": "react",
"allowSyntheticDefaultImports": true,
Expand Down Expand Up @@ -365,6 +367,68 @@ function App() {
export default App;
```

### Enable Server Side Rendering (SSR)

If your React framework supports server side rendering, e.g. [Next.js](https://nextjs.org/) your Stencil components will get automatically server side rendered, if set up correctly. In order to enable this:

1. Add a `dist-hydrate-script` output target to your `stencil.config.ts` if not already existing, e.g.:
```ts title="stencil.config.ts"
import { Config } from '@stencil/core';

export const config: Config = {
outputTargets: [
{
type: 'dist-hydrate-script',
dir: './hydrate',
},
// ...
]
};
```

2. Create an export for the compiled files within the `/hydrate` directory, e.g.
```json title="package.json"
{
"name": "component-library",
...
"exports": {
...
"./hydrate": {
"types": "./hydrate/index.d.ts",
"import": "./hydrate/index.js",
"require": "./hydrate/index.cjs.js",
"default": "./hydrate/index.js"
},
...
},
...
}
```

3. Set the `hydrateModule` in your React output target configuration, e.g.
```ts title="stencil.config.ts"
import { Config } from '@stencil/core';
import { reactOutputTarget } from '@stencil/react-output-target';
export const config: Config = {
outputTargets: [
reactOutputTarget({
outDir: '../react-library/lib/components/stencil-generated/',
hydrateModule: 'component-library/hydrate'
}),
// ...
]
};
```

That's it! Your Next.js application should now render a Declarative Shadow DOM on the server side which will get automatically hydrated once the React runtime initiates.

:::cautions

A Declarative Shadow DOM not only encapsulates the HTML structure of a component but also includes all associated CSS. When server-side rendering numerous small components with extensive CSS, the overall document size can significantly increase, leading to longer initial page load times. To optimize performance, it's essential to maintain a manageable document size that aligns with your performance objectives. It is advisable to server-side render only the critical components required for rendering the initial viewport, while deferring the loading of additional components until after the initial render.

:::

## API

### esModule
Expand Down
66 changes: 66 additions & 0 deletions docs/framework-integration/vue.md
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,72 @@ In your page or component, you can now import and use your component wrappers:
</template>
```

### Enable Server Side Rendering (SSR)

If your Vue framework supports server side rendering, e.g. when using [Nuxt](https://nuxt.com/) your Stencil components will get automatically server side rendered, if set up correctly. In order to enable this:

1. Add a `dist-hydrate-script` output target to your `stencil.config.ts` if not already existing, e.g.:
```ts title="stencil.config.ts"
import { Config } from '@stencil/core';

export const config: Config = {
outputTargets: [
{
type: 'dist-hydrate-script',
dir: './hydrate',
},
// ...
]
};
```

2. Create an export for the compiled files within the `/hydrate` directory, e.g.
```json title="package.json"
{
"name": "component-library",
...
"exports": {
...
"./hydrate": {
"types": "./hydrate/index.d.ts",
"import": "./hydrate/index.js",
"require": "./hydrate/index.cjs.js",
"default": "./hydrate/index.js"
},
...
},
...
}
```

3. Set the `hydrateModule` in your React output target configuration, e.g.
```ts title="stencil.config.ts"
import { Config } from '@stencil/core';
import { vueOutputTarget } from '@stencil/vue-output-target';
export const config: Config = {
outputTargets: [
vueOutputTarget({
includeImportCustomElements: true,
includePolyfills: false,
includeDefineCustomElements: false,
componentCorePackage: 'component-library',
hydrateModule: 'component-library/hydrate',
proxiesFile: '../component-library-vue/src/index.ts',
}),
// ...
]
};
```

That's it! Your Nuxt application should now render a Declarative Shadow DOM on the server side which will get automatically hydrated once the Vue runtime initiates.

:::cautions

A Declarative Shadow DOM not only encapsulates the HTML structure of a component but also includes all associated CSS. When server-side rendering numerous small components with extensive CSS, the overall document size can significantly increase, leading to longer initial page load times. To optimize performance, it's essential to maintain a manageable document size that aligns with your performance objectives. It is advisable to server-side render only the critical components required for rendering the initial viewport, while deferring the loading of additional components until after the initial render.

:::

## API

### componentCorePackage
Expand Down
138 changes: 138 additions & 0 deletions docs/guides/server-side-rendering.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
---
title: Server Side Rendering
sidebar_label: Server Side Rendering
description: Server Side Rendering
slug: /server-side-rendering
---

# Server-Side Rendering (SSR) with Stencil

Stencil provides server-side rendering (SSR) support for React and Vue output targets. If you're using frameworks like [Next.js](https://nextjs.org/) or [Nuxt](https://nuxt.com/), Stencil automatically enhances these frameworks to render components on the server using a [Declarative Shadow DOM](https://web.dev/articles/declarative-shadow-dom).

For detailed setup instructions, refer to the SSR documentation for [React](/docs/react) and [Vue](/docs/vue). All interfaces needed for rendering Stencil components into a string are exported through the [Stencil Hydrate Module](/docs/hydrate-app).

## Tips & Best Practices

When server-side rendering Stencil components, there are a few potential pitfalls you might encounter. To help you avoid these issues, here are some key tips and best practices.

### Avoid Non-Primitive Parameters

When building components, it's common to pass complex data structures like objects to components as props. For example, a footer menu could be structured as an object rather than as separate components for each menu item:

```tsx
const menu = {
'Overview': ['Introduction', 'Getting Started', 'Component API', 'Guides', 'FAQ'],
'Docs': ['Framework Integrations', 'Static Site Generation', 'Config', 'Output Targets', 'Testing', 'Core Compiler API'],
'Community': ['Blog', 'GitHub', 'X', 'Discord']
}
return (
<nav>
<footer-navigation items={menu} />
</nav>
)
```

While this approach works fine in the browser, it poses challenges for SSR. Stencil **does not support** the serialization of complex objects within parameters, so the footer items may not render on the server.

A better approach is to structure dynamic content as part of the component's light DOM rather than passing it as props. This ensures that the framework can fully render the component during SSR, avoiding hydration issues. Here’s an improved version of the example:

```tsx
const menu = {
'Overview': ['Introduction', 'Getting Started', 'Component API', 'Guides', 'FAQ'],
'Docs': ['Framework Integrations', 'Static Site Generation', 'Config', 'Output Targets', 'Testing', 'Core Compiler API'],
'Community': ['Blog', 'GitHub', 'X', 'Discord']
}
return (
<nav>
<footer-navigation>
{Object.entries(menu).map(([section, links]) => (
<footer-navigation-section>
<h2>{section}</h2>
{links.map(link => (
<footer-navigation-entry href="#/">{link}</footer-navigation-entry>
))}
</footer-navigation-section>
))}
</footer-navigation>
</nav>
)
```

By rendering the menu directly in the light DOM, SSR can produce a complete, ready-to-render markup.

### Cross-Component State Handling

When propagating state between parent and child components, patterns like reducers or context providers (as in React) are often used. However, this can be problematic with SSR in frameworks like Next.js, where each component is rendered independently.

Consider the following structure:

```tsx
<ParentComponent>
<ChildComponent />
</ParentComponent>
```

When `ParentComponent` is rendered on the server, Stencil will attempt to stringify its children (e.g., `ChildComponent`) for the light DOM. The intermediate markup may look like this:

```tsx
<ParentComponent>
<template shadowrootmode="open">
<style>...</style>
...
</template>
<ChildComponent />
</ParentComponent>
```

At this stage, `ParentComponent` can access and manipulate its children. However, when `ChildComponent` is rendered in isolation, it won’t have access to the parent’s state or context, potentially leading to inconsistencies.

To prevent this, ensure that components rendered on the server don’t depend on external state or context. If the component relies on data fetched at runtime, it’s better to display a loading placeholder during SSR.

### Optimizing Performance

When Stencil server-side renders a component, it converts it into [Declarative Shadow DOM](https://web.dev/articles/declarative-shadow-dom), which includes all structural information and styles. While this ensures accurate rendering, it can significantly increase document size if not managed carefully.

For example, consider a button component:

```tsx
import { Component, Fragment, h } from '@stencil/core'
@Component({
tag: 'my-btn',
styleUrl: './button.css'
})
export class MyBtn {
render() {
return (
<>
<button>...</button>
</>
);
}
}
```

And this `button.css` which imports additional common styles:

```css
/* button.css */
@import "../css/base.css";
@import "../css/tokens.css";
@import "../css/animations.css";
@import "../css/utilities.css";

/* component-specific styles */
button {
...
}
```

When SSR is performed, the entire CSS (including imports) is bundled with the component’s declarative shadow DOM. Rendering multiple instances of this button in SSR can lead to repeated inclusion of styles, bloating the document size and delaying [First Contentful Paint (FCP)](https://web.dev/articles/fcp).

Here are some ways to mitigate this:

- **Use CSS Variables**: CSS variables can pierce the Shadow DOM, reducing the need for redundant styles.
- **Use the `::part` pseudo-element**: This allows you to style parts of the Shadow DOM from outside the component, minimizing the internal CSS.
- **Optimize Component-Specific CSS**: Only include the necessary styles for each component.
- **Limit SSR Scope**: In Next.js, apply `use client` to sections that don’t need SSR to reduce unnecessary rendering.

Stencil continues to enhance SSR capabilities and is committed to solving performance and rendering challenges. Your feedback is important — feel free to [file an issue](https://github.com/ionic-team/stencil/issues/new?assignees=&labels=&projects=&template=feature_request.yml&title=feat%3A+) and contribute your ideas!

0 comments on commit 3e25c20

Please sign in to comment.