React SSR on JVM using GraalVM
Do you want to do Server-Side Rendering(SSR) of a React app on the JVM?
1. Why?
But why would somebody want to do this in the first place? There are some cases where I think it might be justified:
- You have a team of Java(Scala/Kotlin) developers and you had to render a React frontend on the server and don’t want to use node.js for this.
- You are gradually migrating from legacy SSR on JVM(Play, Spring, whatever) to Javascript SSR. You want to reuse some functionality from your JVM codebase and be able to render both ways from a single app.
In any case, you have some options for how to do that on the JVM and I’ll show you how exactly the things work.
2. The history
First, let’s recall what is server-side javascript and what was the history behind node.js. Javascript was created in 1995 by Netscape as a scripting tool to manipulate web pages inside the browser. Node.js was created by Ryan Dahl and released in 2009. It combined Google Chrome V8 javascript engine, event loop, and IO API. This made it possible to run javascript on the server and share code between browser and server applications.
3. GraalVM is created
In 2019 the GraalVM was released - an alternative JVM and JDK distribution from Oracle which supports running Java with support for JavaScript, Ruby and Python. GraalVM’s polyglot capabilities made it possible to mix multiple programming languages in a single application.
To run Javascript you have several options:
- Use drop-in replacement for “node” binary. All should work, hopefully. But test everything twice 🤞
- Use the GraalJS engine as a library on stock JDK.
- Use GraalJS bundled with the runtime of the GraalVM.
4. The solution
If you want to skip the process and just check the resulting repo, go here.
For those who want to go step by step - let’s start with a simple React application
//App.jsx
import * as React from "react";
export default function App() {
const [times, setTimes] = React.useState(0);
return (
<div>
<h1>Hello {times}</h1>
<button onClick={() => setTimes((times) => times + 1)}>ADD</button>
</div>
);
}
//index.jsx
import * as React from "react";
import ReactDOM from "react-dom";
import App from "./App";
ReactDOM.hydrate(<App />, document.getElementById("root"));
Also, we will need a node server to do SSR:
//server.jsx
app.get("/", (req, res) => {
fs.readFile(path.resolve("./public/index.html"), "utf8", (err, data) => {
if (err) {
console.error(err);
return res.status(500).send("An error occurred");
}
return res.send(
data.replace(
'<div id="root"></div>',
`<div id="root">${ReactDOMServer.renderToString(<App />)}</div>`
)
);
});
});
And after npm run build && npm run start
we got a page rendered on the server:
Okay, at this point you can just switch node installation with that which comes with GraalVM installation and have performance gains(hopefully).
$GRAALVM/bin/gu install nodejs
$GRAALVM_HOME/bin/node [options] [filename] [args]
Could we benefit from GraalVM at this stage? The answer is “it depends”. Test with your workload and your application.
5. Embedding JS app into JVM process
So far, so good. How to do exactly that with GraalVM as embedded in a Java app? The algorithm is:
- Bundle app as for browser, prefer a small number of chunks because otherwise it would make your life more difficult for no reason
- You can’t use node or browser API’s like xhr, fetch, fs… because they are not implemented in GraalJS runtime.
You should keep in mind one important thing. When you use stock JDK(not GraalVM) you still can use GraalJS, just add a few jars to project dependencies and start hacking your JS app. But GraalVM is used you will have better performance because of Graal’s JIT engine. To use this advantage you will have to switch to another JVM implementation, which has its own pros and cons. Generally, GraalVM should have superior performance over stock JDK, but always test your use cases with benchmarks. If you are writing an app where performance of JS code is important - you should go with the GraalVM, not the other JDK implementations.
One more important thing. GraalJS on JVM lacks several important things that we have in browser. Like IO, HTTP client lib, fs etc.How do we call other services over HTTP from the SSR JS in this case? The answer from GraalVM creators is to inject required functionality from the host language(Java). So you write a “proxy” or a “client” for missing API’s, inject them using .putMember()
and call it from the JS code.
Please read this and this before trying to run your JS app on the JVM.
The way I do bundling for this particular demo is:
"build:server:graaljs": "esbuild server/server-graaljs.jsx --bundle --outfile=build/server-graaljs.js --platform=browser"
Okay, we have bundled an app to a single file, and now we are ready to run it in the JVM process.
val context = Context
.newBuilder("js")
.allowAllAccess(true)
.allowHostAccess(HostAccess.ALL)
.allowIO(true)
.build()
context.eval("js", polyfillContents)
val jsCodeReader = Source.fromFile("../build/server-graaljs.js").reader()
context.eval(GraalSource.newBuilder("js", jsCodeReader, "").build())
val renderResult = context
.getBindings("js")
.getMember("render")
.execute()
.asString()
The first thing we do is create a Context to parse/compile/run JS code. After the Context is ready we should eval our app code with context.eval(). After the eval we could call a render method from our React app, which looks like this:
const render = () => {
return ReactDOMServer.renderToString(<App />)
}
global = window
global.render = render
We are rendering our React app to a string and could use it as a response to a user request.
Let’s try it out.
It worked! You could embed logic like this in your controllers and benefit from rendering a ReactJS app on the server!
In case you run the code using the stock JVM you would get a warning.
6. Useful options for SSR rendering on the JVM
You should read about the options on the official website here and here. The options that I use often in my day-to-day work are:
.option("cpusampler", "true")
.option("cpusampler.OutputFile", "./cpu-samples-graal.txt")
This option enabled CPU sampling of the JS code. The output file will have samples about the most “hot” functions. Like this
.option("engine.TraceCompilation", "false")
Enables trace of the compilation.
.option("inspect.Secure", "false")
.option("inspect", "0.0.0.0:9229")
This one enables debugger in chrome. Could be handy if you want to debug what’s happening during the SSR.
7. Takeaways
Rendering of React on the JVM is doable but it has a very specific and narrow use case. You could do just SSR or go fully isomorphic. The performance of the solution could be a surprise for you, so check everything with benchmarks. In some cases I had a better performance compared to node.js, in others - much worse.
You can check the code here: https://github.com/dkovalenko/react-ssr-graaljs
Have questions? Join the discussion.
Want to render #React on the server using the #JVM? It's not that simple but doable. Wrote a blog post about React SSR using the @GraalVM. https://t.co/PD9Sk1LdDk
— Dmytro Kovalenko (@dkovalenko_fun) February 5, 2023