Best Practices for SSRed Components

The following sections describe how to maintain portable components for server-side rendering (SSR).

Block Access to General Browser APIs

Rendering components on the server means you don’t have access to general browser APIs like window, document, and querySelector. If you’re performing these operations during any component lifecycle events that execute during SSR, you need to revise your code so that it becomes portable.

To prevent non-portable code from running during SSR, use the import.meta.env.SSR boolean. For example, this connectedCallback() doesn't run window.addEventListener() if App gets SSRed.

export default class App extends LightningElement {
    connectedCallback() {
        // guard usage of the window object so it does not throw during SSR
        if (!import.meta.env.SSR) {
            window.addEventListener('error', (evt) => {
                console.error(`⚠️ Uncaught error: ${evt.message}`);
            });
        }
    }
}

Use globalThis for Feature Detection

Feature detection ensures that code only runs in supported browsers. Optional chaining causes failed expressions to return undefined instead of an error. This works best when the return value of the browser-only function isn't needed by the component.

To enable feature detection via optional chaining, use the globalThis property.

connectedCallback() {
// "globalThis" exists in all JavaScript environments
    globalThis.addEventListener?.('keydown', (e) => {...});
}

You can't use the window object because it's undefined on the server.

connectedCallback() {
    // the global "window" object is undefined on the server
    window.addEventListener('keydown', (e) => {...});
}

Adjust Slotting Behavior for Built-In Events

Slotted content refers to anything contained in a <slot> element that you pass into a slot. This content renders as expected during SSR. However, native browser built-in events like slotchange can’t fire during SSR because they’re rendered outside of a browser context. As a result, you might observe changes to slotchange behavior in your components.

For example, a slotchange event determines the number of elements to display in this carousel component. The carousel is outlined in blue, and a pagination component is outlined in pink. The pagination component acts as a control for the number of images being displayed, and it shows a count of three in this example.

A basic carousel component with three cards.

The pagination component logic runs during the slotchange event. Since the slotchange event doesn’t happen during an SSR flow, the first initial render only shows a count of 1.

When the component is rendered in the browser, the slotchange event fires and hydration updates the count to three.

When these browser-specific events occur, the webpage can sometimes display a small “flash” effect or layout shift. You can reduce the visibility of these effects by adding placeholders or loading messages to your code. This approach is useful when logic takes a long time to run on the client, like a REST or GraphQL data fetch.

Dynamically Import Non-Portable Modules

To ensure that non-portable modules don't get processed on the server, import them dynamically in your components.

For example, this component imports a portable and a non-portable module.

<!-- prettier-ignore -->
import { portableApi } from 'my/library';
import { nonPortableApi } from 'some/library';
export default class Cmp extends LightningElement {
    connectedCallback() {
        portableApi();
        nonPortableApi();
    }
}

To import these modules dynamically, the component can use an async/await function, like this. Remember to guard non-portable code using the import.meta.env.SSR boolean.

<!-- prettier-ignore -->
import { portableApi } from 'my/library';
export default class Cmp extends LightningElement {
    async connectedCallback() {
        if (import.meta.env.SSR) {
            portableApi(); // executed during SSR
        } else {
            const { nonPortableApi } = await import('some/library');
            nonPortableApi(); // NOT reachable during SSR
        }
    }
}

Remove Host Element Mutations

Mutating the host element in connectedCallback() isn't supported in SSR and CSR.

SSR hydration validates that the virtual DOM exactly matches the HTMLElement. For validation to succeed, each virtual DOM attribute has to have an equivalent HTMLElement counterpart. If a mutation occurs in a component's connectedCallback(), changes that may appear in HTMLElement don't appear in the virtual DOM.

For example, this connectedCallback() uses the classList anti-pattern to try to add class names to the host element. However, directly mutating the HTMLElement means the container class never appears in the virtual DOM.

connectedCallback() {
    this.classList.add('container');
}

To correctly add the container class to the host, use a semantic element in your <template> instead.

<template>
    <form class='container'></form>
</template>

You can pass a class from a parent component to a child component by wrapping the child in a <div> with a dynamic value for the class property. Then, implement the corresponding getter in the parent component's JavaScript.

<template>
    <div class={classForChild}>
      <x-child></x-child>
   </div>
</template>

The parent component's JavaScript uses the fromOutside property to set <c-parent from-outside="parent-class">. The getter ensures that the child component renders the my-child-needs-parent-class value on the class attribute.

import { api, LightningElement } from 'lwc';

export default class Cmp extends LightningElement {
    @api fromOutside;
    get classForChild() {
        return `my-child-needs-${this.fromOutside}`;
    }
}

Use Supported Lightning Base Components

You can speed up app development by using the LWC versions of base components, which are out-of-the-box building blocks for user interfaces like Lightning Web Runtime (LWR) sites.

Only the following base components are supported for SSR. For more information about the Lightning base components, see the Component Library.

lightning-accordion lightning-dynamic-icon lightning-layout-item lightning-spinner
lightning-accordion-section lightning-file-upload lightning-lookup-address lightning-tab
lightning-alert lightning-formatted-address lightning-menu-divider lightning-tabset
lightning-avatar lightning-formatted-date-time lightning-menu-item lightning-textarea
lightning-badge lightning-formatted-email lightning-menu-subheader lightning-tile
lightning-breadcrumb lightning-formatted-location lightning-modal lightning-toast
lightning-breadcrumbs lightning-formatted-name lightning-modal-body lightning-toast-container
lightning-button lightning-formatted-number lightning-modal-footer lightning-tree
lightning-button-group lightning-formatted-phone lightning-modal-header lightning-tree-item
lightning-button-icon lightning-formatted-rich-text lightning-pill lightning-vertical-navigation
lightning-button-icon-stateful lightning-formatted-text lightning-pill-container lightning-vertical-navigation-item
lightning-button-menu lightning-formatted-time lightning-progress-bar lightning-vertical-navigation-item-badge
lightning-button-stateful lightning-formatted-url lightning-progress-indicator lightning-vertical-navigation-item-icon
lightning-card lightning-helptext lightning-progress-ring lightning-vertical-navigation-overflow
lightning-checkbox-group lightning-icon lightning-progress-step lightning-vertical-navigation-section
lightning-click-to-dial lightning-input lightning-prompt
lightning-combobox lightning-input-address lightning-radio-group
lightning-confirm lightning-input-location lightning-rich-text-toolbar-button
lightning-datatable lightning-input-rich-text lightning-rich-text-toolbar-button-group
lightning-dual-listbox lightning-layout lightning-select

SSR can impact your existing style rules for base components on your site. To learn how to adapt your styles for SSR, see Update Lightning Base Components Styling.

Next Steps

Now that your components can opt-into SSR, we recommend testing them with the SSR playground and test runner to catch and correct unexpected behavior. To learn more, follow the instructions in Test Your Components.