Use Nashorn Engine To Do Server-Side Rendering with React

When I finished one e-commerce project which web client is written in react and talk to backend rest api which is written in groovy and run on JVM, the performance and user interactivity is great. But there is a very pain point in production.

The overall size of react javascript including libraries and application is over 2MB after minified. When user first time access the react application or react application is updated which means react javascript file name changes, these javascript files are not cached in user browser, So they are downloaded through internet. It takes 8 seconds just to download them, and takes 700ms to evaluate and execute. During downloading period, the browser is just blank for 8 seconds, this hurts user experience very much. The leads to the below journey.

Solution to the first rendering issue obviously is server-side rendering. So I googled around various server-side rendering solutions. Most of articles and demo code is using NodeJS which very natural in React stack. It is fine for me to use NodeJS as server since web stack is around React and mobile is around React Native. But my Server stack is Groovy and Java which runs on JVM, Adding a NodeJS server only hosting React web application complicates server deployment, especially when same application need to be deployed as independent instance for each client. With NodeJS solution, one NodeJS server and JVM server for each client, if there are 10,000 client, then 10,000 NodeJS server and 10,000 JVM server. It does not seems economic for me.

There are few articles about running javascript on Nashorn Script Engine which is shipped with JDK 1.7. There is one excellent article https://blog.codecentric.de/en/2014/06/project-nashorn-javascript-jvm-polyglott/ explain Nashorn Script Engine capability and gap to do server-side rending.

React, same applies to Angular 2 and VueJS, does not directly operate on DOM when running in browser. It operates on a virtual dom (old terminology) which is a representation of actual dom. All of React DOM operation and event is done on this representation, then through periodic reconciliation which diffs previous state and current state of virtual dom and figure out what actual dom operation need to do then perform them on browser DOM. When running on server-side to generate html, React, Angular 2 and VueJs does not need to perform on actual dom, so server-side javascript execution environment does not need to have DOM related capabilities. So the fundamental capabilities Nashorn need to provide is showing in

from https://blog.codecentric.de/files/2014/06/specifications.png

XmlHttpRequest spec and Html 5 Spec (section 6). The others like fetch, promise, ES6, ES7 actually can be polyfilled by pure javascript which a lot people use core-js . There is code in https://github.com/morungos/java-xmlhttprequest/blob/develop/src/main/resources/polyfill.nashorn.js polyfill Nashorn with these two capabilities. So I started from there.

I put the code on JVM server, played around and fixed some issue in XmlHttpRequest, When it runs a simple React application which has counter using Promise to return intial value by adding 5 (https://github.com/shendepu/react-ssr-starter-kit), it works fine. So I upload it to https://www.npmjs.com/nashorn-polyfill. Caution: this polyfill is meant to run on server, and can’t be imported as regular npm module since it imports Java classes. But later I met a issue I did not know why at that moment that occasionally the request just hang forever until timeout forced by server. And when run it with concurrent requests by apache benchmark, e.g. 100 requests with 40 concurrency, around 7% requests hang till timeout. This is annoying and can’t be put into production.

A fews days ago, I happened watching a video https://www.youtube.com/watch?v=8aGhZQkoFbQ that Philip Roberts explained how event loop works in Browser, similarly applies to NodeJS. In early Nashorn Polyfill, it used Timer to run scheduled task that inheried code from https://github.com/morungos/java-xmlhttprequest/blob/develop/src/main/resources/polyfill.nashorn.js. So I started adding a nashornEventLoop which uses a deque that Timer task only add an object of function callback into the deque, and the object shape is { fn, args }, then call nashornEventLoop.process() to run function call back by picking it from beginning of deque one by one. This surprised me that the hang issue is gone. Even I run with 1000 request in 100 concurrency, all requests are responded fast and without error.

I thought it over again and added some log to verify my understanding, and I think I understand it well now. In early version of Polyfill for Html 5 Specification (section 5), it uses Timer to run function callback. Actually it runs in another thread that Timer creates which is different with Nashorn Engine thread where React application runs. There are might be chance two threads operate one same javascript object at same time, There is Phaser to control only one Timer thread run task one at a time, but this Phase does not guard Nashorn thread. The change with adding nashornEventLoop is Timer thread does not run javascript function callback, it adds function callback to end of event loop deque. It is still Nashorn thread that runs function callback by picking one at a time from beginning of event loop deque. The Phaser is used to guard that only one thread of Nashorn thread and Timer thread to access event loop deque, like addLast, removeFirst, check deque size. So there is no possible corruption on javascript call stack.

There are also some other optimizations like using only one Nashorn Engine instance, using cache compiled script to serve request, even only running Nashorn Polyfill and React libraries once and run React app script per request.

The result is very promising. It run at Macbook Pro with 2.3G 4 core, 16G memory, SSD. It can achieve ~40 request/s and JVM usage is 600 MB ~ 1.1 GB. It should be higher throughput on server.

I think this gives another choice to run server-side render on JVM, other than NodeJS.

Related github repos: 
https://github.com/zloirock/core-js
https://github.com/morungos/java-xmlhttprequest/blob/develop/src/main/resources/polyfill.nashorn.js

My repos: 
https://github.com/shendepu/nashorn-polyfill
https://github.com/shendepu/react-ssr-starter-kit
https://github.com/shendepu/moqui-react-ssr: Java code of running Nashorn Engine can be found here.
https://github.com/shendepu/moqui-react-ssr-demo

A production demo:
https://demo.moshian.com: built with react + apollo (graphql client) for app, moqui + moqui-graphql for backend, used nashorn-polyfill and moqui-react-ssr to do server rendering. Check out https://demo.moshian.com/apps/system to see the heap memory is around 400MB.