Open nicklasfrahm opened 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.
@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.
@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.
@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.
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).
@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.
@nicklasfrahm @mschipperheyn @mattlubner this thread is awesome and it's full of experience :) I Love it ❤️
this issue was here from about 1 year ago, but I read it tonight. I don't know how should I start :)
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
Send all CSS and JS needed for the current route from the server.
for fixing this I had an idea :)
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.
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.
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>
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)
https://github.com/jaredpalmer/after.js/issues/281
Render Async (ReactDOM.renderToNodeStream) 🏎
so we use renderToNodeStream() to implement what I said earlier above
I love to know your thoughts about this @nicklasfrahm @mschipperheyn @mattlubner
@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.
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 withexpress
and to create fake certs in development, as browsers only supporth2
(offical protocol abbreviation) via HTTPS. In production, this might not be relevant, as most people deploy behind anh2
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.