Coder Social home page Coder Social logo

Comments (17)

Ephem avatar Ephem commented on July 26, 2024

I've been thinking a lot about this recently and just found this issue so I thought I'd jot some thoughts down.

This is going to be hugely important when SSR-support for Suspense hits (at least if that will include the ability to render pure html). Streaming will probably be the first class API for a new server renderer and being able to stream content early is perhaps the biggest promise for Suspense on the server side compared to today.

One huge problem that is still left to fix, especially for SEO, is precisely what this issue describes. I can see two valid solutions (that could also be combined) to streaming pure html depending on what type of requirements you have:

  1. Only use something like Helmet on the client side for all regular users, but try to identify bots via User Agent on the server side and wait for entire render to finish before sending the response in those cases so they get a valid head. (You might want to serve different things anyway now that Google supports dynamic rendering, such as only server rendering above the fold for users, but server rendering everything for bots.)

  2. Somehow identify when head is done and start streaming at that point.

To accomplish nr 2 I can see two different solutions, the first is something like what you posted above @wmertens and this is probably a good way to do it for a lot of cases, but if you have Helmet spread out over your application it could be tricky to know when it's done?

A second way, that has slightly different tradeoffs, is declaring up front what you expect to see declared before starting to stream, something roughly like:

  const { headPromise, HelmetProvider } = createHelmetProvider({
    required: {
      meta: [{ name: "description" }, { name: "keywords" }]
    }
  });

  const stream = ReactDOMServer.renderToStream(
    <HelmetProvider>
      <App />
    </HelmetProvider>
  );

  helmetPromise.then(({ helmet }) => {
    // Construct html+head and start streaming
  });

When something has been added for a meta description and keywords, promise would resolve.

I haven't thought hard enough about the tradeoffs for the different solutions, but I don't think they exclude each other and different apps could have different usecases for this.

from react-helmet-async.

Ephem avatar Ephem commented on July 26, 2024

Actually, now that I think about it, since the required things are kept on the context, they could also be updated from within the rendering-process. This is nice because:

  • If you have a centralised routing-config, you could co-locate the required head-stuff with your routes there and declare them all up front based on the url
  • If you don't have a centralised routing config but instead declare them all dynamically within your render-tree, you could still co-locate required head-stuff and routes (as long as requirements are declared before headPromise has already resolved)

from react-helmet-async.

oyeanuj avatar oyeanuj commented on July 26, 2024

I'm coming here with the same problem as I've been facing race conditions with streaming and data-fetching in the components. I think a callback from the Helmet context to notify Helmet (that does as @wmertens solution suggests) would be super useful.

@wmertens @Ephem How are you solving for this problem today?

from react-helmet-async.

Ephem avatar Ephem commented on July 26, 2024

I'm not unfortunately. I've played around with it a little personally and seen a very early POC of what I described work, but nothing nearly ready to share and I'm not currently working on it. We might be tackling streaming at work Q1/Q2 so I'm still very interested in solutions to this! :)

from react-helmet-async.

wmertens avatar wmertens commented on July 26, 2024

from react-helmet-async.

droganov avatar droganov commented on July 26, 2024

Hi. I'm facing this problem too, let's think together.

Goal: Reduce TTFB
Problem: We don't know what head content would be until we render a closing body tag

I'm not sure if promise makes sense, if we just renderToString it would take same or less time.

The solution would be to patch output stream somehow. I've made a quick solution today, but it looks that streamReplace actually buffers the stream, and doesn't help to reduce TTFB.

I didn't test it in live production, I'm only setting up the boilerplate for a new project.

import React from 'react';
import { renderToNodeStream, renderToString } from 'react-dom/server';
import { getDataFromTree } from 'react-apollo';
import multistream from 'multistream';
import stringStream from 'string-to-stream';
import streamReplace from 'stream-replace';

import Head from './Head';
import Html from './Html';
import App from './App';

module.exports = async (ctx) => {
  ctx.set('Content-Type', 'text/html');
  const app = <App context={ctx} />;
  await getDataFromTree(App);
  ctx.body = multistream([
    stringStream('<!doctype html>'),
    renderToNodeStream(<Html app={app} context={ctx} />),
  ]).pipe(streamReplace(
    /<head.*head>/,
    () => renderToString(<Head context={ctx} />),
  ));
};

from react-helmet-async.

wmertens avatar wmertens commented on July 26, 2024

@droganov ok so you are proposing emitting a string like <!--$$HELMET$$--> in the <head>, buffering it through a custom transform stream, and giving that custom stream the correct data once it's complete?

That sounds doable… a little wasteful due to the text scanning, but it can be implemented without changes to react nor react-async-helmet – a good start for seeing if this is worth doing.

from react-helmet-async.

wmertens avatar wmertens commented on July 26, 2024

...Uhm I just realized that I'm using React to render my entire HTML page including doctype and head, but that's actually not really necessary. So instead, the server could immediately send the html preamble, including link tags etc, and then render the react stream and wait for either the "done helmet" callback or the end of the stream, and pipe that to the client. No transform stream or text parsing needed.

(I do cache the SSR results in memory so I'll need to find a different way to do that, now I'm just rendering to string and caching that)

from react-helmet-async.

droganov avatar droganov commented on July 26, 2024

@wmertens if you pipe head after it could look surprising in the browser, you have to buffer

from react-helmet-async.

wmertens avatar wmertens commented on July 26, 2024

@droganov how do you mean? The browser would get

  1. send to client <doctype...><head><link ....><etc...>
  2. start react render stream, pass callback via context
  3. wait for react to finish rendering/early callback
  4. send Helmet strings
  5. pipe react stream to client; it will have buffered the results

This could deadlock if the react stream buffer is smaller than the moment the callback is called, but other than that this is fine, no?

from react-helmet-async.

droganov avatar droganov commented on July 26, 2024

@wmertens
4 send Helmet strings — it may contain styles, title description and what ever

so how you gonna put it the to header after you sent it?

from react-helmet-async.

wmertens avatar wmertens commented on July 26, 2024

I didn't send the </head> yet, only the parts of the head that are fixed (e.g. telling the browser to preload scripts). After the helmet strings are sent, send </head> and the react stream.

from react-helmet-async.

droganov avatar droganov commented on July 26, 2024

then yes, looks realistic, start stream from some string, then buffer and continue

from react-helmet-async.

wmertens avatar wmertens commented on July 26, 2024

The one thing you can't do with this setup is add attributes to the html tag, obviously.

from react-helmet-async.

droganov avatar droganov commented on July 26, 2024

yes

from react-helmet-async.

wmertens avatar wmertens commented on July 26, 2024

I just realized that I do redirects based on react-router, and that changes the very first line of the response (HTTP/1.1 302), so until I'm sure there's no redirects, there's nothing to send at all, and those can happen even after the Helmet calls.

So I'm thinking the complete notification shouldn't even be part of Helmet, just a signal between the app and the SSR setup. 🤔

After the signal is received, the SSR can output the headers with some random etag, the doctype, head, and the body up until the React div, then stream the react render, and finally the post-body with the scripts

from react-helmet-async.

wmertens avatar wmertens commented on July 26, 2024

Closing because on reflection I agree with myself at #3 (comment)

from react-helmet-async.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.