Server-Side Rendering (SSR) is a technique that allows you to serve an initial payload with HTML, JavaScript, CSS, and even application state. You serve a fully rendered HTML page that would make sense even without JavaScript enabled. In addition to providing potential performance benefits, this can help with Search Engine Optimization (SEO).
Even though the idea does not sound that unique, there is a technical cost. The approach was popularized by React. Since then frameworks encapsulating the tricky bits, such as Next.js and razzle, have appeared.
To demonstrate SSR, you can use webpack to compile a client-side build that then gets picked up by a server that renders it using React following the principle. Doing this is enough to understand how it works and also where the problems begin.
To use React, we require specific configuration. Given most of React projects rely on JSX format, you have to enable it through Babel:
npm add babel-loader @babel/core @babel/preset-react --develop
Connect the preset with Babel configuration as follows:
.babelrc
{
"presets": [
["@babel/preset-env", { "modules": false }],
"@babel/preset-react"
]
}
To make sure the project has the dependencies in place, install React and react-dom to render the application to the DOM.
npm add react react-dom
Next, the React code needs a small entry point. For browser, we'll render a div
and show an alert on click. For server, we return JSX to render.
As ES2015 style imports and CommonJS exports cannot be mixed, the entry point has to be written in CommonJS style. Adjust as follows:
src/ssr.js
const React = require("react");
const ReactDOM = require("react-dom");
const SSR = <div onClick={() => alert("hello")}>Hello world</div>;
// Render only in the browser, export otherwise
if (typeof document === "undefined") {
module.exports = SSR;
} else {
ReactDOM.hydrate(SSR, document.getElementById("app"));
}
To keep things nice, we will define a separate configuration file. A lot of the work has been done already. Given you have to consume the same output from multiple environments, using UMD as the library target makes sense:
webpack.ssr.js
const path = require("path");
const APP_SOURCE = path.join(__dirname, "src");
module.exports = {
mode: "production",
entry: { index: path.join(APP_SOURCE, "ssr.js") },
output: {
path: path.join(__dirname, "static"),
filename: "[name].js",
libraryTarget: "umd",
globalObject: "this",
},
module: {
rules: [
{
test: /\.js$/,
include: APP_SOURCE,
use: "babel-loader",
},
],
},
};
To make it convenient to generate a build, add a helper script:
package.json
{
"scripts": {
"build:ssr": "wp --config webpack.ssr.js"
}
}
If you build the SSR demo (npm run build:ssr
), you should see a new file at ./static/index.js
. The next step is to set up a server to render it.
To keep things clear to understand, you can set up a standalone Express server that picks up the generated bundle and renders it following the SSR principle. Install Express first:
npm add express --develop
Then, to get something running, implement a server:
server.js
const express = require("express");
const { renderToString } = require("react-dom/server");
const SSR = require("./static");
const app = express();
app.use(express.static("static"));
app.get("/", (req, res) =>
res.status(200).send(renderMarkup(renderToString(SSR)))
);
app.listen(process.env.PORT || 8080);
function renderMarkup(html) {
return `<!DOCTYPE html>
<html>
<head><title>SSR Demo</title><meta charset="utf-8" /></head>
<body>
<div id="app">${html}</div>
<script src="./index.js"></script>
</body>
</html>`;
}
Run the server now (node ./server.js
) and go below http://localhost:8080
, you should see a "Hello World". Clicking the text should show an alert and you should see pre-rendered HTML in the source.
Even though there is a React application running now, it's difficult to develop. If you try to modify the code, nothing happens. The problem can be solved for example by using webpack-dev-middleware.
If you want to debug output from the server, set export DEBUG=express:application
.
Even though the demo illustrates the basic idea of SSR, it still leaves open questions:
Questions like these are the reason why solutions such as Next.js or razzle exist. They have been designed to solve SSR-specific problems like these.
Webpack provides require.resolveWeak for implementing SSR. It's a specific feature used by solutions such as react-universal-component underneath.
__non_webpack_require__(path)
allows you to separate imports that should be evaluated outside of webpack. See the issue #4175 for more information.
SSR isn't the only solution to the SEO problem. Prerendering is an alternate technique that is easier to implement. The point is to use a headless browser to render the initial HTML markup of the page and then serve that to the crawlers. The caveat is that the approach won't work well with highly dynamic data.
The following solutions exist for webpack:
SSR comes with a technical challenge, and for this reason, specific solutions have appeared around it. Webpack is a good fit for SSR setups.
To recap:
In the next chapter, we'll learn about micro frontends and module federation.
This book is available through Leanpub (digital), Amazon (paperback), and Kindle (digital). By purchasing the book you support the development of further content. A part of profit (~30%) goes to Tobias Koppers, the author of webpack.