Node.js can be used as the backend for server-side rendering instead of execJS. Before you try this, consider the tradeoff of extra complexity with your deployments versus potential performance gains. We've found that using ExecJS with mini_racer to be "fast enough" so far. That being said, we've heard of other large websites using Node.js for better server rendering performance.
If you're serious about this comparing Node.js versus execJS/mini_racer, then get in touch! We can definitely collaborate with you on refining this solution. However, please try out these instructions first.
Warning: this is an experimental feature.
- Every time the webpack bundle changes, you have to restart the server yourself.
To do this you need to add a few files and then configure react_on_rails to use NodeJS. Here are the relevant files to add.
Node server rendering allows you to use separate NodeJS process as a renderer. The process loads your configured server_bundle_js file and then executes javascript to render the component inside its environment. The communication between rails and node occurs
via a socket (client/node/node.sock
)
To use node process just set server_render_method = "NodeJS"
in config/initializers/react_on_rails.rb
. To change back
to ExecJS set server_render_method = "ExecJS"
# app/config/initializers/react_on_rails.rb
config.server_render_method = "NodeJS"
You need to configure the name of the server bundle in two places:
- JavaScript: Change the name of server bundle and adjust yarn start script in
client/node/package.json
- Ruby: The configured server bundle file is defined in
config/react_on_rails.rb
, and you'll have a webpack file that creates this. You maybe using the same file for client rendering.
# This is the file used for server rendering of React when using `(prerender: true)`
# If you are never using server rendering, you may set this to "".
# If you are using the same file for client and server rendering, having this set probably does
# not affect performance.
config.server_bundle_js_file = "webpack-bundle.js"
And in client/node/package.json
// client/node/package.json
{
"name": "react_on_rails_node",
"version": "0.0.0",
"private": true,
"scripts": {
"start": "node ./server.js -s webpack-bundle.js"
},
"dependencies": {
}
}
And you'll need this file: client/node/server.js
// client/node/server.js
const net = require('net');
const fs = require('fs');
const bundlePath = '../../app/assets/webpack/';
let bundleFileName = 'server-bundle.js';
let currentArg;
class Handler {
constructor() {
this.queue = [];
this.initialized = false;
}
initialize() {
console.log(`Processing ${this.queue.length} pending requests`);
let callback;
// eslint-disable-next-line no-cond-assign
while (callback = this.queue.pop()) {
callback();
}
this.initialized = true;
}
handle(connection) {
const callback = () => {
const terminator = '\r\n\0';
let request = '';
connection.setEncoding('utf8');
connection.on('data', (data) => {
console.log(`Processing chunk: ${data}`);
request += data;
if (data.slice(-terminator.length) === terminator) {
request = request.slice(0, -terminator.length);
// eslint-disable-next-line no-eval
const response = eval(request);
connection.write(`${response}${terminator}`);
request = '';
}
});
};
if (this.initialized) {
callback();
} else {
this.queue.push(callback);
}
}
}
const handler = new Handler();
process.argv.forEach((val) => {
if (val[0] === '-') {
currentArg = val.slice(1);
return;
}
if (currentArg === 's') {
bundleFileName = val;
}
});
function loadBundle() {
if (handler.initialized) {
console.log('Reloading server bundle must be implemented by restarting the node process!');
return;
}
/* eslint-disable */
require(bundlePath + bundleFileName);
/* eslint-enable */
console.log(`Loaded server bundle: ${bundlePath}${bundleFileName}`);
handler.initialize();
}
try {
fs.mkdirSync(bundlePath);
} catch (e) {
if (e.code !== 'EEXIST') {
throw e;
} else {
loadBundle();
}
}
fs.watchFile(bundlePath + bundleFileName, (curr) => {
if (curr && curr.blocks && curr.blocks > 0) {
loadBundle();
}
});
const unixServer = net.createServer((connection) => {
handler.handle(connection);
});
unixServer.listen('node.sock');
process.on('SIGINT', () => {
unixServer.close();
process.exit();
});