lexical icon indicating copy to clipboard operation
lexical copied to clipboard

Feature: Server-Side Rendering (SSR) Support for Lexical Editor

Open agarwalamn opened this issue 2 years ago • 19 comments

Use Case:

We are currently working on a project similar to Reddit, where our users utilize the Lexical Editor to create posts and comments. These posts are then displayed on their timelines using a read-only editor. This is a fundamental use case where we believe Server-Side Rendering (SSR) support is essential. SSR would greatly benefit us by allowing us to render these posts on the server, leading to improvements in user experience and SEO.

Current Issue:

Our observation is that the Lexical Editor's rendering is currently limited exclusively to the client-side due to its reliance on client-side rendering mechanisms. Consequently, posts are only rendered on the client side, which can have some drawbacks.

Challenges:

One of the challenges we've encountered is the time it takes for the editor to process data on the client side. This processing delay leads to noticeable UI jumps after content is loaded, which is not an ideal user experience.

Proposed Solutions:

We propose exploring two potential solutions to address these challenges:

Server-Side Rendering with JSDOM:

We suggest investigating the feasibility of enabling SSR for the Lexical Editor by utilizing libraries like JSDOM. This approach would empower us to render the editor within a Node.js/server environment, thus improving initial load times and overall performance.

Render-Only Editor Mode:

Another avenue to explore is the creation of a "render-only" mode for the Lexical Editor. This mode would be optimized for server-side rendering, offering a simplified and resource-efficient way to render the lexical tree.

How This Benefits the Project:

Implementing these solutions would not only enhance the user experience but also make our platform more SEO-friendly. It would address the limitations of client-side rendering, reduce processing time, and eliminate UI jumps, resulting in a smoother and more efficient application.

agarwalamn avatar Sep 02 '23 09:09 agarwalamn

Our observation is that the Lexical Editor's rendering is currently limited exclusively to the client-side due to its reliance on client-side rendering mechanisms

Can you be more specific about what you've observed here? What tools are you using, what outcome are you expecting and what is actually happening?

Another option is just to traverse the parsed JSON editor state and render whatever HTML you want from it on the server.

acywatson avatar Sep 02 '23 16:09 acywatson

One of the challenges we've encountered is the time it takes for the editor to process data on the client side.

Can you give an example here? What exactly are you trying to do that leads to the undesirable behavior?

acywatson avatar Sep 02 '23 16:09 acywatson

Hi @acywatson, Adding details below

Our tech stack includes Next.js. We leverage Next.js getServerSideProps function to make backend API calls for retrieving JSON data and hydrate on server

Observations:

  • Due to the fact that the editor doesn't render on the server due to lexical being heavily reliant on DOM API's.
  • The user sees a blank screen when a post is delivered first on UI.
  • After a brief delay, the post is rendered, resulting in a noticeable UI jerk that affects the overall user experience.
  • We've included screenshots to illustrate this issue below.

(before loading) MoreForYouCleveland__Sports_Betting_Picks_Predictions_and_Promos_2023-07-27_16-10-50 (1) (after loading) MoreForYouCleveland__Sports_Betting_Picks_Predictions_and_Promos_2023-07-27_16-11-45 (source: Thread on lexical discord)

What We've Tried:

  • We've attempted to replicate the internal workings of the lexical editor to generate HTML strings and render them on the server.
  • You can find a sample implementation in this CodeSandbox link. [This codesandbox does not render SSR but you can get the idea of core logic]
  • However, this approach has numerous shortcomings, making it unsuitable for production use.
  • We have also tried polyfilling the DOM APIs but it breaks the NextJS.

Possible solutions

It would be immensely beneficial if we could have an integrated render-only editor that is compatible with server-side rendering. Maybe by using JSDOM to render the editor when DOM API's are not available.

agarwalamn avatar Sep 06 '23 06:09 agarwalamn

Thanks! I think this makes sense.

However, this approach has numerous shortcomings, making it unsuitable for production use.

I'm curious about the shortcomings here? You don't need to really know about the internal workings of the Lexical editor. It's completely normal to traverse a serialized JSON tree and render it to another format (HTML in this case). This effectively exactly what the "render-only editor" you're asking for would do, AFAICT. We'd just be making it easier (for you) by wrapping it all up into a Lexical package.

acywatson avatar Sep 06 '23 16:09 acywatson

@acywatson We appreciate the suggestion to parse the JSON and generate HTML, but it does introduce complexities, as it requires a good understanding of Lexical's internal logic, potentially involving reverse engineering. For instance, deciphering how JSON is generated for formatted text (e.g., bold, italic) or how list items are handled can be intricate tasks. Moreover, this approach could become obsolete if Lexical decides to change its node generation logic in the future.

Having an inbuilt server-side HTML generation capability within Lexical would be advantageous, ensuring that the logic remains up-to-date with any upcoming releases.

We also believe that this feature would benefit not only us but also the broader community, especially those using SSR (Server-Side Rendering) frameworks like React. The ability to render content on the server is a fundamental requirement for various applications and aligns with modern development practices and the promotion of SSR by popular frameworks. It would enhance the versatility and usability of Lexical for a wide range of use cases.

agarwalamn avatar Sep 09 '23 08:09 agarwalamn

I'll be doing something similar with SSR soon and am currently researching how I might approach this with Next JS.

One thought that comes to mind as a workaround: When saving the editor state to the backend, we could export to HTML using this, then save that HTML to the backend along with the raw editor state.

This (in theory) would allow us to render the HTML string on server, and render the full editor on client.

There are some potential implications of this with setting inner HTML via string, you'd need to ensure the exported HTML is safe and secure, and you'd need to add in relevant css for the exported HTML, but it seems like a workable alternative to me.

morteymike avatar Sep 10 '23 13:09 morteymike

If possible, I would like you to parse the JSON and create a React component in a separate project. Read Only of course. Then, we can plug in customizations for some of them.

SSR as well, but when reading an article, I may want to fire React's LOGIC on the client.

Most Nodes (such as TextNode) are fine with exportDOM content, so I want a common React Component

akichim21 avatar Nov 28 '23 07:11 akichim21

I'll be doing something similar with SSR soon and am currently researching how I might approach this with Next JS.

One thought that comes to mind as a workaround: When saving the editor state to the backend, we could export to HTML using this, then save that HTML to the backend along with the raw editor state.

This (in theory) would allow us to render the HTML string on server, and render the full editor on client.

There are some potential implications of this with setting inner HTML via string, you'd need to ensure the exported HTML is safe and secure, and you'd need to add in relevant css for the exported HTML, but it seems like a workable alternative to me.

it's the same issue than Quilljs, just came form there for this, can't believe a project from Fracebook has the same dummy issue. most of us care about this because the SEO.

sashapetrovska avatar Jan 07 '24 23:01 sashapetrovska

Hi guys! please check this out: https://lexical-nextjs-ssr.vercel.app/ Now it's possible with the latest lexical. Check the html preview loaded on your network tab.

2wheeh avatar Jan 29 '24 17:01 2wheeh

What do we need to close this issue? Perhaps some documentation?

acywatson avatar Jan 30 '24 19:01 acywatson

What do we need to close this issue? Perhaps some documentation?

Well, the @2wheeh example is not that complex. But I have a feeling it's complex enough that the Lexical team will have to answer several questions about bugs or implementations.

Just an idea, I think the DX could be taken a little further. An SSR property could be added to LexicalComposer or initialConfig that receives a DOM implementation (for example an instance of jsdom or happy-dom).

If window is undefined, then lexical runs that little utility with the dom instance passed to it. So that Lexical users only have to worry about one line of code.

GermanJablo avatar Jan 30 '24 22:01 GermanJablo

What do we need to close this issue? Perhaps some documentation?

Well, the @2wheeh example is not that complex. But I have a feeling it's complex enough that the Lexical team will have to answer several questions about bugs or implementations.

Just an idea, I think the DX could be taken a little further. An SSR property could be added to LexicalComposer or initialConfig that receives a DOM implementation (for example an instance of jsdom or happy-dom).

If window is undefined, then lexical runs that little utility with the dom instance passed to it. So that Lexical users only have to worry about one line of code.

Let's discuss. I agree that documenting it officially is a burden on the team that requires some up front consideration and potentially mitigation (via DX, as you suggest).

acywatson avatar Jan 30 '24 22:01 acywatson

Well, the @2wheeh example is not that complex. But I have a feeling it's complex enough that the Lexical team will have to answer several questions about bugs or implementations.

Yes, I agree that my example is not enough as is. For example, adding more plugins which return JSX element will make codes ugly easily while achieving zero layout shift. This is bad for DX.

Just an idea, I think the DX could be taken a little further. An SSR property could be added to LexicalComposer or initialConfig that receives a DOM implementation (for example an instance of jsdom or happy-dom).

If window is undefined, then lexical runs that little utility with the dom instance passed to it. So that Lexical users only have to worry about one line of code.

I love this !

2wheeh avatar Jan 31 '24 03:01 2wheeh

Slightly tangential to this approach. We also had a use case to render Lexical content in NextJS with SSR. We ended up doing the more hacky approach. We created a native React Server Component for each standard Lexical components. Not only it renders fast, it also comes with any NextJS' goodies (hydration, etc.), so natively supports existing client/server components. The one-time effort to create the RSC copy was 1 day ish. The only maintenance item is to sync our RSC copy with Lexical's, but we don't see a lot of changes on the presentation-side standard HTML component. The custom components are already an RSC so no additional cost.

Sadly, our RSC codes are tangled with our internal tooling but let me know if any of you are interested in some pointers. 🙏

Here is a demo page: https://cmty.space/vantient-brand/c/straw-hat-scavenger-hunt-mRpUIbd6ihr5ezHJmhR5KUbyB4eTACLs

jmargatan avatar Jan 31 '24 05:01 jmargatan

@jmargatan I can't see how that link fits into all this. Maybe if you could show some code to explain your approach?

GermanJablo avatar Jan 31 '24 14:01 GermanJablo

Essentially there are 2 separate experiences:

  • editable, this is the Lexical editor, and
  • non-editable, this is the SSR version of the content. Note that interaction is still supported.

After any modification in editable state, we persist the SerializedEditorState JSON in the DB.

During non-editable serving / SSR, we (built a parser that) walk through the stored SerializedEditorState JSON and use the corresponding RSC-equivalent for each LexicalNode as identified by the type attributes.

Sorry, our current code is tangled with internal implementation, but here is some hacky pseudocode:

const parseNode = <T extends SerializedLexicalNode>(node: T, key: string): ReactNode => {
  if (node.type == 'text') {
    // For a node that has children, we recursively run this `parseNode` on each children.
    // For a node that refers to our custom RSC components, we just render the component.
    return <NonEditableText .../>
  } else if (...) {
    ...
  }
}

For actual implementation you can mimic LexicalEditor's "registerXYZPlugin" pattern instead of if-else.

The result is a server-rendered RSC streamed to the browser. React Suspense, if used, within any of the node will work as expected.

Jumping between the 2 experience (editable and non-editable) means replacing the RSC with Lexical editor.

jmargatan avatar Feb 01 '24 00:02 jmargatan

Can we simply persist the HTML version for the editor state and render it server-side?

sakhmedbayev avatar Feb 19 '24 19:02 sakhmedbayev

Hi guys! please check this out: https://lexical-nextjs-ssr.vercel.app/ Now it's possible with the latest lexical. Check the html preview loaded on your network tab.

what if I don't use next.js, but only react, and there I send the component and its content directly to ReactDomServer.renderToStaticMarkup.

  const html = ReactDomServer.renderToStaticMarkup(
      <HtmlPreview value={jsonData} />,
  )

The code entered in this way, where there is a promise, will not work for me. Is it not possible to get html from lexical json using a simple parsing function that is native in lexical?

mbohovic avatar Jul 05 '24 21:07 mbohovic