facebook / lexical

Lexical is an extensible text editor framework that provides excellent reliability, accessibility and performance.
https://lexical.dev
MIT License
17.5k stars 1.45k forks source link

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

Open agarwalamn opened 8 months ago

agarwalamn commented 8 months ago

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.

acywatson commented 8 months ago

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 commented 8 months ago

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?

agarwalamn commented 8 months ago

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:

(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:

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.

acywatson commented 8 months ago

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.

agarwalamn commented 8 months ago

@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.

morteymike commented 8 months ago

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.

akichim21 commented 5 months ago

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

sashapetrovska commented 4 months ago

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.

2wheeh commented 3 months ago

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.

acywatson commented 3 months ago

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

GermanJablo commented 3 months ago

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.

acywatson commented 3 months ago

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).

2wheeh commented 3 months ago

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 !

jmargatan commented 3 months ago

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

GermanJablo commented 3 months ago

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

jmargatan commented 3 months ago

Essentially there are 2 separate experiences:

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.

sakhmedbayev commented 2 months ago

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