jaredpalmer / razzle

✨ Create server-rendered universal JavaScript applications with no configuration
https://razzlejs.org
MIT License
11.09k stars 873 forks source link

Native HTTP2 support #870

Open nicklasfrahm opened 5 years ago

nicklasfrahm commented 5 years ago

Hello,

this is a feature request / proposal.

I discovered HTTP2 the other day. So, when I used razzle to create an after.js app, I thought that this could be a great improvement for this framework. In a nutshell HTTP2 supports a server push "command" that allows the webserver to push static assets, such as stylesheets and bundles along with the index.html without an additional request cycle. This can significantly improve site performance and could be a great value.

As for the implementation, this would require us to use the spdy module in conjunction with express and to create fake certs in development, as browsers only support h2 (offical protocol abbreviation) via HTTPS. In production, this might not be relevant, as most people deploy behind an h2 compliant reverse proxy anyway. The implementation should be rather straight forward, as the asset paths are exposed as environment variables, if I recall that correctly.

It is to note also that h2 will fall back to HTTP 1.1 if the browser does not support it, so it would not impose any breaking changes.

mschipperheyn commented 5 years ago

I would recommend putting an nginx server in front of a razzle server. You get http2 out of the box and I'm guessing you will enjoy some security benefits.

nicklasfrahm commented 5 years ago

@mschipperheyn I only partially agree with you. Yes, it is the more secure setup and in most of the cases you'd have that anyway, if you use Kubernetes with nginx-ingress or something like nginx-proxy on a single Docker host.

As for the HTTP/2 out of the box I have to disagree with you, correct me, if I am wrong. Yes you get binary data transfer and some of the other nice features, but the feature I am talking about is the HTTP/2 Server Push and as far as I understood, this requires an implementation and support up until the application server, which would be razzle in this case. So I think we should not yet close this, until we've fully evaluated its usefulness.

mschipperheyn commented 5 years ago

@nicklasfrahm You're right. I reviewed https://webapplog.com/http2-server-push-node-express/ and frankly, that is a sucky way to have to implement this, because it makes it hard to supply the service out of the box without manual implementation each and every time.

Instead, it probably makes sense to implement some kind of manifest (e.g. https://github.com/GoogleChromeLabs/http2-push-manifest), that a front end webserver (nginx) can pick up. The static assets part of this manifest already exists, the assets.json that razzle already generates. It would have to be rewritten to manifest format. On top of that, you might need to be able to parse react-router routes to also push pages.

Hmm, Nginx doesn't seem to be able to work with push manifests.

So, alternative 3, is to use Link headers, which is preload instead of push. That seems like the kind of thing that should be more in line with out of the box processing and wouldn't require SSL setup on the razzle side. You could implement something like https://github.com/cloudflare/netjet to get the fastest response possible.

nicklasfrahm commented 5 years ago

@mschipperheyn I think we could potentially go with method one, because we could implement a middleware, that accepts the asset manifest that we have at the moment and then pushes based on that. This middleware could then be set up in the server.js of the examples and it would ship by default if you use create-razzle-app.

But I am not sure if this could also work with after.js, because it does code splitting based on your routes. The current manifest does not include any information about which routes an asset belongs to, so that might need to be adjusted or it might completely rule out this approach. If we modify the form of the asset manifest, we should probably stick to the reference from Google that you mentioned.

It might make sense to consider this change before releasing version 3, because it might require potentially breaking changes to the format of the asset manifest, if it should include route information for the assets.

mattlubner commented 5 years ago

Perhaps I've not understood, but does HTTP2 need to be baked into Razzle if it can be implemented as an express middleware?

Many folks (myself included) have been waiting for Razzle v3 to hit the "stable" milestone for some time. HTTP2 support makes more sense as a potential v4 feature, rather than pushing v3 further down the road (and delaying production-use feedback/validation on the current v3 alpha changes).

nicklasfrahm commented 5 years ago

@mattlubner Currently, we are still trying to evaluate a good approach for this, so I cannot say if the express middleware alone would do it.

As for version 3. I have only recently discovered this gem (razzle) and my intention is definitely not to block anybody. Thus, I would totally agree with you to move it to version 4. I have just never done open source before, so I was not too aware of the relevance of the production-use feedback and validation. But in hindsight, I agree that it is vital.

nimaa77 commented 4 years ago

@nicklasfrahm @mschipperheyn @mattlubner this thread is awesome and it's full of experience :) I Love it ❤️

nimaa77 commented 4 years ago

this issue was here from about 1 year ago, but I read it tonight. I don't know how should I start :)

What Is The Problem

After.js only send the main CSS and js files from the server response then on browser at ensureReady method it tries to fetch javascript and CSS files for the current code spilled page and it's too slow

Desired Behavior

Send all CSS and JS needed for the current route from the server.

for fixing this I had an idea :)

fast-forward:

this is the result #1178

{
  "client": {
    "css": [
      "/static/css/bundle.36d04d42.css"
    ],
    "js": [
      "/static/js/bundle.d3433edb.js",
      "/static/js/bundle.d3433edb.js.map"
    ]
  },
  "home": {
    "css": [
      "/static/css/home.2f97ec2d.chunk.css"
    ],
    "js": [
      "/static/js/home.82beecb0.chunk.js",
      "/static/js/home.82beecb0.chunk.js.map"
    ]
  },
  "about": {
    "css": [],
    "js": [
      "/static/js/about.1e639ac9.chunk.js",
      "/static/js/about.1e639ac9.chunk.js.map"
    ]
  }
}

Which is exactly what @nicklasfrahm has mentioned

The current manifest does not include any information about which routes an asset belongs to, so that might need to be adjusted or it might completely rule out this approach. If we modify the form of the asset manifest, we should probably stick to the reference from Google that you mentioned.

How it's working?

1) we need to add webpackChunkName to our async components, we should also store chunkName in a variable so we can find the chunk in the next steps.

// routes.js

import Home from './Home';

export default [
  {
    path: '/',
    exact: true,
    component: Home,
  },
  {
    path: '/about',
    exact: true,
    component: () => import(/* webpackChunkName: "whatever" */ './About'),
    chunkName: 'whatever',
  },
  {
    path: '/contact-us',
    exact: true,
    component: () => import(/* webpackChunkName: "ContactUs" */ './Contact'),
    chunkName: 'ContactUs',
  },
];

you can develop a babel plugin that do this for you too, like this plugin.

2) find out the current request is for which component. I used react-router so I will call the matchPath method.

const matched = matchPath(req.url, routes)

so matched variable will contain an object like this:

{
  path: '/contact-us',
  exact: true,
  component: () => import(/* webpackChunkName: "ContactUs" */ './Contact'),
  chunkName: 'ContactUs',
}

3) getAssets() this is simple a function that returns a property from an array

const { scripts, styles } = getAssets(matched.chunkName, chunks)

function getAssets(chunkName, chunks) {
  const scripts = chunks[chunkName].js || [];
  const styles = chunks[chunkName].css || [];
  return { scripts, styles };
}

it's that much EZ :)

you can find the implemented version of the above code at https://github.com/jaredpalmer/after.js/pull/237.

http2 push!

https://github.com/jaredpalmer/after.js/pull/237#issuecomment-536197799

we can do more awesome things too :D

both react-loadable and loadable-components find scrips and css files for current request after ReactDOM.renderToString finishes it's job.

so our TTFB (Time to first byte) going to be time of getInitialProps for current route (about 400ms) + ReactDOM.renderToString (about 60ms)

but with this PR, the server knows CSS and JS files for the current request before call getInitialProps and renderToString. internally after.js calls renderToString two times, first time to render request for current route and the second time to render Document.js...

if we use renderToNodeStream to render Document.js we can send <head> of HTML to the client ASAP with preloaded CSS and JS files so our TTFB decrease to 0 (actually not 0 but it's too low) and while server handling getInitialProps and renderToString for the current route, the browser can fetch CSS and JS files (there is also another thing called h2 push but many people say it's not a good idea to totally use h2 push).

<html>
  <head>
    <meta charset="utf-8" />
    <link
      rel="preload"
      href="/static/media/bundle.css"
    />
    <link
      rel="preload"
      href="/static/ccs/homepage.chunk.css"
    />
    <link
      rel="preload"
      href="/static/media/homepage.chunk.js"
    />  
    <link
      rel="preload"
      href="/static/media/bundle.js"
    />  

when rendering of current request finished we can grab meta tags from react-helmet and send them to browser

    <title>Hii</title>
    <meta name="description" content="A page's description, usually one or two sentences."/>

and we send rest of the page to the end

  </head>
  <body>
    <div id="root">
      WHAT EVER REACT RENDER TO STRING RETURNED
    </div>
    <div>window.__SERVER_APP_STATE__ = { ... }</div>
  </body>
</html>

that was too much complicated!

we can simply use HTTP Link Header to achieve the same functionality.

// server.js
import { getAssets } from "@jaredpalmer/after"

const server = express()
server.get("/*", async (req, res) => {
  // before call render
  const { scripts, styles } = getAssets({ url: req.url, routes })
  res.links({
    preload: scripts,
    preload: styles,
  })
  const html = await render()

with above code TTFB not change (still about 500ms) but browser fetches CSS and JS files faster. (not sure if this works as i explained)

What's next?

https://github.com/jaredpalmer/after.js/issues/281

Render Async (ReactDOM.renderToNodeStream) 🏎

so we use renderToNodeStream() to implement what I said earlier above

nimaa77 commented 4 years ago

I love to know your thoughts about this @nicklasfrahm @mschipperheyn @mattlubner

nicklasfrahm commented 4 years ago

@nimaa77 This sounds amazing. So if I understand it correctly, you are not using any HTTP/2 features, but improve the loading time by using features that are already available via HTTP/1.1. I love it, because it does not require additional scaffolding from the users.