53

Clojure: React Server Side Rendering with GraalVM

 5 years ago
source link: https://www.tuicool.com/articles/mMFfau
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client
["^ ","~:view-data",["^ ","~:article-settings",null,"~:article-access?",false,"~:preview?",false,"~:article",["^ ","~:article/preview","QmZB466JEjaFeMWb7JW3PgoLS6JSSTnD8FaeaTjRVLn8ST","~:article/published-at","~m1562581469816","~:article/title","React Server Side Rendering with GraalVM for Clojure","~:article/change",["^ ","~:nextjournal/id","~u5d2319d9-966e-4f7f-869a-275aecd789f9","~:preview/name","QmZB466JEjaFeMWb7JW3PgoLS6JSSTnD8FaeaTjRVLn8ST","~:change/inserted-at","~m1562581465320"],"~:article/published-change",["^ ","^9","~u5d2319d9-966e-4f7f-869a-275aecd789f9","^:","QmZB466JEjaFeMWb7JW3PgoLS6JSSTnD8FaeaTjRVLn8ST","^;","~m1562581465320"],"^9","~u02b25a80-2123-4f34-9c7b-ce8083b7c1ed","~:article/name","react-server-side-rendering-with-graalvm-for-clojure","~:article/view-token","MjAUVxXrZWifd53UrCx7pe","~:article/visibility","~:article.visibility/public","~:article/profile",["^ ","~:profile/name","Dieter Komendera","~:profile/description","","~:profile/website","http://www.komendera.com/","^9","~u59cd647b-0129-4aff-8c40-e1e3ebe08f9a","~:stripe/customer-id","cus_ERYntSamd4UB6e","~:profile/private-access?",true,"~:profile/billing-email","[email protected]","~:stripe/subscription-id","sub_ERYn2fycKtNthe","~:profile/handle","kommen","~:profile/avatar","054eaa40-73f9-4143-ad88-c61f5c650983"]],"~:article-contents",["^ ","^4",["^ ","~:root","02b25a80-9ae2-4cea-a5c8-6a7e659bea3b","~:nodes",["^ ","0f79e753-ff02-4520-abcc-ba908e93881d",["^ ","~:runtime/inherited-environment-variables",["~#list",[["^ ","~:name","CLOJURE_VERSION","~:value","1.10.1.447"],["^ ","^Q","NVIDIA_VISIBLE_DEVICES","^R","void"],["^ ","^Q","NVIDIA_DRIVER_CAPABILITIES","^R","all"],["^ ","^Q","GRAAL_VERSION","^R","19.1.0"],["^ ","^Q","PATH","^R","/usr/local/graalvm/bin:/usr/local/nvidia/bin:/usr/local/cuda/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"],["^ ","^Q","GRAAL_DIR","^R","/usr/local/graalvm"],["^ ","^Q","NEXTJOURNAL_MOUNT_CUDA","^R","9.2-cudnn7-devel-ubuntu18.04"],["^ ","^Q","BASH_ENV","^R","/.bash_profile"],["^ ","^Q","LC_ALL","^R","en_US.UTF-8"],["^ ","^Q","LANGUAGE","^R","en_US.en"],["^ ","^Q","LANG","^R","en_US.UTF-8"]]],"~:output-log-lines",["^ ","~:stdout",83],"~:type","~:nextjournal","~:runtime/mounts",[["^ ","~:src",["~:node","eeb8f3d6-c97a-4437-b95e-0f5d4f6936ce"],"~:dest","/deps.edn"],["^ ","^X",["^Y","d2b7ffcc-e582-4290-9702-e1337e4b9ccb"],"^Z","/src/my/app.cljs"]],"~:language","clojure","~:id","0f79e753-ff02-4520-abcc-ba908e93881d","~:compute-ref","~ue9e96a90-a168-11e9-a319-f63da8d076b5","~:kind","runtime","~:error",null,"~:environment",["^14",["^ ","~:article/nextjournal.id","~u5b45eb52-bad4-413d-9d7f-b2b573a25322","~:change/nextjournal.id","~u5d1c83b8-6eff-4edf-b75f-3ddd3471f3a4","~:node/id","0ae15688-6f6a-40e2-a4fa-52d81371f733"]]],"43fce69a-f3f5-4e92-812d-cd1dd90895a7",["^ ","^10","43fce69a-f3f5-4e92-812d-cd1dd90895a7","^12","paragraph","~:content","Let's create a simple ClojureScript app using reagent . This will be used for server side rendering."],"28d5e10f-2953-476d-b606-1c8bf396c95b",["^ ","^10","28d5e10f-2953-476d-b606-1c8bf396c95b","^12","paragraph","^19","You can see that for small notebooks GraalVM actually performs better because we don't have to go through a whole http stack. For large notebooks, it's in the same ballpark (~30% slower) and we're hopeful this will further improve in the future. "],"7bab3fe6-61a3-49b1-8d74-0e3bdb0a3ae6",["^ ","^10","7bab3fe6-61a3-49b1-8d74-0e3bdb0a3ae6","^12","paragraph","^19","We use the Context/newBuilder polyglot API to create a Graal execution context and specifically only allow JavaScript execution. The js.java-package-globals is needed to prevent namespace collisions between Java packages and some ClojureScript namespaces in our codebase."],"c453a3e2-3686-4b63-9f7c-d196ea86c057",["^ ","^10","c453a3e2-3686-4b63-9f7c-d196ea86c057","^12","paragraph","^19","We ran into a number of issues and limitations, which we reported and were either resolved or worked around. We'll continue to work with the GraalVM and ClojureScript teams to get those issues resolved."],"c31764b2-c8f3-4e1c-866c-a8457b0570e1",["^ ","^10","c31764b2-c8f3-4e1c-866c-a8457b0570e1","^12","paragraph","^19","GraalJS is currently optimized for long running processes, and for some workloads it is still slower than V8 or JavaScriptCore. Our use case should fit well as we reuse the Context over and over and execution times get quite a bit faster after the first few requests. We also have less overhead for server side rendering now, because we can invoke Javascript functions directly without needing a complete http stack. In many cases this is faster than our old nodejs rendering process."],"0c9dd96f-b1ab-46f8-a31b-07c4f0d00908",["^ ","^10","0c9dd96f-b1ab-46f8-a31b-07c4f0d00908","^12","paragraph","^19","Let me walk you through how this works. "],"02b25a80-9ae2-4cea-a5c8-6a7e659bea3b",["^ ","^10","02b25a80-9ae2-4cea-a5c8-6a7e659bea3b","~:title","React Server Side Rendering with GraalVM for Clojure","^12","section","~:version",2,"^19",["^P",[]],"~:sections",["392d8f48-c268-4aa0-bf3d-560653e4ab62","7803eb9e-91e6-4db2-9e83-702c87ebf098","abb5d553-eedd-4379-896b-68cd95f01900","fa5d0320-0c9b-445a-995a-4e80fd8f3846"]],"a3a9e66c-c364-4ef2-8efd-f7f07374b500",["^ ","^10","a3a9e66c-c364-4ef2-8efd-f7f07374b500","^12","list","^19","Use code sharing for GraalJS so that the multiple render threads can share the AST to reduce the warmup time for optimal performance."],"b1d47273-d561-4f7a-9093-9927254cdede",["^ ","^10","b1d47273-d561-4f7a-9093-9927254cdede","^12","list","^19","Makefigwheel live code reloading work with the GraalJS so ClojureScript code changes are immediately reflected in the server side rendering in our development environment."],"7803eb9e-91e6-4db2-9e83-702c87ebf098",["^ ","^10","7803eb9e-91e6-4db2-9e83-702c87ebf098","^12","section","^1@","Performance","^19",["c31764b2-c8f3-4e1c-866c-a8457b0570e1","ae716fbd-4f1a-4a15-a55e-25235f66faa0","f7a77fa3-58e9-4a31-ad4c-87f6f8f6e917","28d5e10f-2953-476d-b606-1c8bf396c95b"]],"d0a2b6fc-baf2-4ec1-9da2-a0da462d1f91",["^ ","^10","d0a2b6fc-baf2-4ec1-9da2-a0da462d1f91","^12","paragraph","^19","At Nextjournal, we perform server side rendering of notebooks to provide better user experience, especially for published notebooks. A visitor instantly sees the content of a notebook while the client side app loads. However, we do use a few Javascript-only dependencies like CodeMirror and ProseMirror, which prevents us from doing this server side rendering directly in Clojure on the JVM. Until now, we used a separate nodejs version of our browser app to perform this task. This necessitated a whole lot of code to expose ClojureScript app via an HTTP interface to the Clojure app as they were running in different VMs."],"6a4d3f4a-f4d3-4c5d-b30b-f42b608710c7",["^ ","^10","6a4d3f4a-f4d3-4c5d-b30b-f42b608710c7","^12","paragraph","^19","The sha key points to a ClojureScript version with some small adaptations to make work better with GraalJS."],"eeb8f3d6-c97a-4437-b95e-0f5d4f6936ce",["^ ","^10","eeb8f3d6-c97a-4437-b95e-0f5d4f6936ce","^12","code-listing","^19","{:deps\n {org.clojure/clojure {:mvn/version \"1.10.0\"}\n reagent {:mvn/version \"0.8.1\"}\n org.clojure/clojurescript {:git/url \"https://github.com/nextjournal/clojurescript\"\n :sha \"da9166015f6a28b2c18fa7706e457901d02a5d81\"}}\n }","^[","clojure","^Q","deps.edn"],"acbe9b45-a352-468e-a9b0-9894f993fc1f",["^ ","^10","acbe9b45-a352-468e-a9b0-9894f993fc1f","^12","list","^19","Finish up our ClojureScript patches to improve support for GraalJS and get them applied upstream."],"27a98bb4-86b5-4f47-9c1e-ee0e79fffdb2",["^ ","^19","(def app-js (clojure.java.io/file \"out/main.js\"))\n(def app-source (.build (Source/newBuilder \"js\" app-js)))\n(.eval context app-source)","~:refs",["^P",[]],"^S",["^ "],"^[","clojure","^10","27a98bb4-86b5-4f47-9c1e-ee0e79fffdb2","^11","~uc6ba4a63-2172-49ee-878d-1da500dd8f64","~:runtime",["^1L","0f79e753-ff02-4520-abcc-ba908e93881d"],"^12","code","~:outputs",["^ ","~_",["^ ","application/transit+json",["^ ","^R","[\"~$org.graalvm.polyglot.Value\",\"0x25b2672a\",\"null\",{\"~:bean\":{\"~_\":{\"~:get\":{\"~#list\":[\"~$unrepl.repl$hpNiwYgtt8PN_xegDbIo_Axg5Xo/fetch\",\"~:G__728\"]}}}}]"]]],"^13",null,"~:exec-duration",10590],"d2b7ffcc-e582-4290-9702-e1337e4b9ccb",["^ ","^10","d2b7ffcc-e582-4290-9702-e1337e4b9ccb","^12","code-listing","^19","(ns my.app\n (:require\n [reagent.core :as reagent]\n [reagent.dom.server :as dom-server]))\n\n(defn hello-component [name]\n [:div (str \"Hello \" name \"!\")])\n\n(defn html [name]\n (dom-server/render-to-string [hello-component name]))","^[","clojurescript","^Q","app.cljs"],"90c0033b-93c6-4197-bdeb-048950fb924e",["^ ","^19","clj -m cljs.main -t graaljs -c my.app","^1K",["^P",[]],"^S",["^ "],"^[","bash","^10","90c0033b-93c6-4197-bdeb-048950fb924e","^11","~u4e6c4c9b-52f5-457b-bd8a-905e757cc85e","^1L",["^1L","0f79e753-ff02-4520-abcc-ba908e93881d"],"^12","code","^1M",["^ "],"^13",null,"^1O",51645],"b67f0185-8ab4-48de-9e0f-89a29302bfb0",["^ ","^19","(defn execute-fn [context fn & args]\n (let [fn-ref (.eval context \"js\" fn)\n args (into-array Object args)]\n (assert (.canExecute fn-ref) (str \"cannot execute \" fn))\n (.execute fn-ref args)))","^1K",["^P",[]],"^S",["^ "],"^[","clojure","^10","b67f0185-8ab4-48de-9e0f-89a29302bfb0","^11","~u098a426d-f866-40bc-9cac-72912d63d002","^1L",["^1L","0f79e753-ff02-4520-abcc-ba908e93881d"],"^12","code","^1M",["^ ","~_",["^ ","^1N",["^ ","^R","{\"~#'\":{\"~#with-meta\":[\"~$user/execute-fn\",{\"~:arglists\":{\"~#list\":[[\"~$context\",\"~$fn\",\"~$&\",\"~$args\"]]},\"~:line\":5,\"~:column\":1,\"~:file\":\"unrepl-reader-703\",\"~:name\":\"~$execute-fn\",\"~:ns\":\"~$user\"}]}}"]]],"^13",null,"^1O",96],"fc59d257-a3ed-4c14-93c5-6694c007b7d4",["^ ","^10","fc59d257-a3ed-4c14-93c5-6694c007b7d4","^12","list","^19","Optimized :graaljs builds from ClojureScript are missing the bootstrap code: ClojureScript CLJS-3113 "],"393297d7-b4a8-45d1-a0b4-4db9d133b343",["^ ","^19","(import '(org.graalvm.polyglot Context Source))\n\n(def context-builder\n (doto (Context/newBuilder (into-array String [\"js\"]))\n (.option \"js.timer-resolution\" \"1\")\n (.option \"js.java-package-globals\" \"false\")\n (.out System/out)\n (.err System/err)\n (.allowAllAccess true)\n (.allowNativeAccess true)))\n\n(def context (.build context-builder))\n","^1K",["^P",[]],"^S",["^ "],"^[","clojure","^10","393297d7-b4a8-45d1-a0b4-4db9d133b343","^11","~ufdf2f49e-9a64-40aa-92cb-2485e2f6794f","^1L",["^1L","0f79e753-ff02-4520-abcc-ba908e93881d"],"^12","code","^1M",["^ ","~_",["^ ","^1N",["^ ","^R","{\"~#'\":{\"~#with-meta\":[\"~$user/context\",{\"~:line\":4,\"~:column\":1,\"~:file\":\"unrepl-reader-703\",\"~:name\":\"~$context\",\"~:ns\":\"~$user\"}]}}"]]],"^13",null,"^1O",834],"f7a77fa3-58e9-4a31-ad4c-87f6f8f6e917",["^ ","^10","f7a77fa3-58e9-4a31-ad4c-87f6f8f6e917","^12","file","~:mime-type","text/csv","^Q","renderer performance.csv","~:blob-id","QmaYowfz6ujxTXXBLgcYmXaF6dXvUPCB6bbydSNDhhowm8","~:viewer/selected",["^ ","~_","table"]],"a529905b-b64b-415e-aaba-1f0356c80add",["^ ","^10","a529905b-b64b-415e-aaba-1f0356c80add","^12","paragraph","^19","Now everything is coming together and we can perform server side rendering with our React ClojureScript app directly from Clojure."],"fa5d0320-0c9b-445a-995a-4e80fd8f3846",["^ ","^10","fa5d0320-0c9b-445a-995a-4e80fd8f3846","^12","section","^1@","Upstream Issues","^19",["c453a3e2-3686-4b63-9f7c-d196ea86c057","d6ecb47e-c9bc-4fea-bd00-09b52ac1d79d","f372d059-8d15-4017-8768-f870a2509d02","fc59d257-a3ed-4c14-93c5-6694c007b7d4","db23a26f-b0f8-46c0-a2f0-4f37e2b3a927"]],"de5902cd-9ab1-420d-9b19-e5546ffca390",["^ ","^10","de5902cd-9ab1-420d-9b19-e5546ffca390","^12","paragraph","^19","We utilize Graal's Source class to load the JavaScript artifact and evaluate it in the execution context to make the our html function available."],"dd7c8efa-159b-4a95-8dc0-1740c2a0212d",["^ ","^10","dd7c8efa-159b-4a95-8dc0-1740c2a0212d","^12","paragraph","^19","Before we can actually call our Javascript functions, we first need to load our little demo app into the context we created before."],"f372d059-8d15-4017-8768-f870a2509d02",["^ ","^10","f372d059-8d15-4017-8768-f870a2509d02","^12","list","^19","GraalJS namespace collisions of Java package globals with ClojureScript namespaces: GraalJS issue #164 and ClojureScript CLJS-3087 "],"db23a26f-b0f8-46c0-a2f0-4f37e2b3a927",["^ ","^10","db23a26f-b0f8-46c0-a2f0-4f37e2b3a927","^12","list","^19","Make main files produced by ClojureScript for :graaljs targets work without java package globals: ClojureScript CLJS-3089 "],"090d7ded-7160-43bf-963a-dddd9b1de2c7",["^ ","^10","090d7ded-7160-43bf-963a-dddd9b1de2c7","^12","paragraph","^19","Compile it using ClojureScript's command line interface."],"abb5d553-eedd-4379-896b-68cd95f01900",["^ ","^10","abb5d553-eedd-4379-896b-68cd95f01900","^12","section","^1@","Where to go from here","^19",["^P",["28310f37-73fd-491d-8efb-ce554af10e57","b1d47273-d561-4f7a-9093-9927254cdede","acbe9b45-a352-468e-a9b0-9894f993fc1f","a3a9e66c-c364-4ef2-8efd-f7f07374b500"]],"^1B",["^P",[]]],"d6ecb47e-c9bc-4fea-bd00-09b52ac1d79d",["^ ","^10","d6ecb47e-c9bc-4fea-bd00-09b52ac1d79d","^12","list","^19","GraalJS regex engine bug affecting lookarounds: GraalJS issue #162 (the fix shipped already in Graal 19.1.0)"],"5c1b98fb-1c68-4d24-9923-22179671d425",["^ ","^10","5c1b98fb-1c68-4d24-9923-22179671d425","^12","paragraph","^19","We start with Nextjournal's default Clojure environment, which already uses GraalVM."],"c82d8a7e-fa2b-4b23-b955-1682a2b38777",["^ ","^19","js --jvm -f out/main.js -e \"console.log(my.app.html('GraalJS \uD83D\uDC4B'))\"","^1K",["^P",[]],"^S",["^ ","^T",1],"^[","bash","^10","c82d8a7e-fa2b-4b23-b955-1682a2b38777","^11","~u5c2d2c7e-7298-47a2-848a-12f7d049d8dc","^1L",["^1L","0f79e753-ff02-4520-abcc-ba908e93881d"],"^12","code","^1M",["^ "],"^13",null,"^1O",13555],"392d8f48-c268-4aa0-bf3d-560653e4ab62",["^ ","^10","392d8f48-c268-4aa0-bf3d-560653e4ab62","^12","section","^19",["d0a2b6fc-baf2-4ec1-9da2-a0da462d1f91","738d9c33-a84d-4857-9e98-19ba08b8ec53","0c9dd96f-b1ab-46f8-a31b-07c4f0d00908","5c1b98fb-1c68-4d24-9923-22179671d425","79482a88-cb00-4190-b8cf-2c95d58474db","5a1a1502-7754-4c7e-8747-dbe7155355c0","eeb8f3d6-c97a-4437-b95e-0f5d4f6936ce","6a4d3f4a-f4d3-4c5d-b30b-f42b608710c7","43fce69a-f3f5-4e92-812d-cd1dd90895a7","d2b7ffcc-e582-4290-9702-e1337e4b9ccb","090d7ded-7160-43bf-963a-dddd9b1de2c7","90c0033b-93c6-4197-bdeb-048950fb924e","92f3b343-dda2-4db5-a3ba-006830097323","c82d8a7e-fa2b-4b23-b955-1682a2b38777","ff4f3e6e-9df4-4dbe-b03b-07d87c577ac7","393297d7-b4a8-45d1-a0b4-4db9d133b343","7bab3fe6-61a3-49b1-8d74-0e3bdb0a3ae6","b67f0185-8ab4-48de-9e0f-89a29302bfb0","83bf04a0-79bd-45aa-a25f-01488d66e85b","dd7c8efa-159b-4a95-8dc0-1740c2a0212d","27a98bb4-86b5-4f47-9c1e-ee0e79fffdb2","de5902cd-9ab1-420d-9b19-e5546ffca390","a529905b-b64b-415e-aaba-1f0356c80add","8408b1e4-8585-433b-abf1-85fe5ef7052c","e3326cec-c396-4f60-989f-cfac811c66b8"],"^1@","Usecase at Nextjournal"],"ae716fbd-4f1a-4a15-a55e-25235f66faa0",["^ ","^10","ae716fbd-4f1a-4a15-a55e-25235f66faa0","^12","paragraph","^19","These are 5 measurements of server-side rendering calls in milliseconds after a warm-up phase of 1000 render calls."],"e3326cec-c396-4f60-989f-cfac811c66b8",["^ ","^10","e3326cec-c396-4f60-989f-cfac811c66b8","^12","paragraph","^19","In Nextjournal's case, we need to pass data of a notebook to the ClojureScript app. Rather than passing a string like \"Polyglot Graal \uD83C\uDF08\" to the html function for rendering, we actually pass a transit -encoded notebook. This is the same data we pass to the browser for client side rendering."],"92f3b343-dda2-4db5-a3ba-006830097323",["^ ","^10","92f3b343-dda2-4db5-a3ba-006830097323","^12","paragraph","^19","As a first test to see if our ClojureScript app actually works in GraalJS, we can use Graal's standalone js command line operation to load our app and produce a call to our html function."],"5a1a1502-7754-4c7e-8747-dbe7155355c0",["^ ","^10","5a1a1502-7754-4c7e-8747-dbe7155355c0","^12","paragraph","^19","We use this deps.edn

file to bring in some minimal dependencies to build a small ClojureScript app which can be used for server side rendering."],"8408b1e4-8585-433b-abf1-85fe5ef7052c",["^ ","^19","(def result (execute-fn context \"my.app.html\" \"Polyglot Graal \uD83C\uDF08\"))\n(.asString result)","^1K",["^P",[]],"^S",["^ "],"^[","clojure","^10","8408b1e4-8585-433b-abf1-85fe5ef7052c","^11","~uaf1a2cf3-0b1a-4cfb-b949-6f687f700ec2","^1L",["^1L","0f79e753-ff02-4520-abcc-ba908e93881d"],"^12","code","^1M",["^ ","~_",["^ ","^1N",["^ ","^R","{\"~#'\":\"

Hello Polyglot Graal \\uD83C\\uDF08!

\"}"]]],"^13",null,"^1O",943],"ff4f3e6e-9df4-4dbe-b03b-07d87c577ac7",["^ ","^10","ff4f3e6e-9df4-4dbe-b03b-07d87c577ac7","^12","paragraph","^19","However, we want to use this html function from within the JVM which runs our Clojure app. For this we can leverageGraalVM's polyglot abilities to load the JavaScript artifact from the ClojureScript app into the Clojure JVM and directly call the function with almost no overhead."],"79482a88-cb00-4190-b8cf-2c95d58474db",["^ ","^19","java -version","^1K",["^P",[]],"^S",["^ ","^T",3],"^[","bash","^10","79482a88-cb00-4190-b8cf-2c95d58474db","^11","~ud5f5291f-8863-45b4-aa71-5cb3abeb64c5","^1L",["^1L","0f79e753-ff02-4520-abcc-ba908e93881d"],"^12","code","^1M",["^ "],"^13",null,"^1O",931],"83bf04a0-79bd-45aa-a25f-01488d66e85b",["^ ","^10","83bf04a0-79bd-45aa-a25f-01488d66e85b","^12","paragraph","^19","The execute-fn helper makes it convenient to call our Javascript functions and provide them params directly from Clojure."],"738d9c33-a84d-4857-9e98-19ba08b8ec53",["^ ","^10","738d9c33-a84d-4857-9e98-19ba08b8ec53","^12","paragraph","^19","However, recently we switched our production JVM to GraalVM

, which opened a new possibility since it comes with GraalJS, a JavaScript engine which aims to compete with today's faster JavaScript engines like V8 and JavaScriptCore on performance. It lets us embed the ClojureScript app into our Clojure app so it can perform the rendering in-process in the JVM. "],"28310f37-73fd-491d-8efb-ce554af10e57",["^ ","^10","28310f37-73fd-491d-8efb-ce554af10e57","^12","paragraph","^19","While we already happy with our current setup, there a few things are still on our todo list:"]],"~:transclusions",["~#cmap",[["^ ","^15","~u5b45eb52-bad4-413d-9d7f-b2b573a25322","^17","0ae15688-6f6a-40e2-a4fa-52d81371f733","^16","~u5d1c83b8-6eff-4edf-b75f-3ddd3471f3a4"],["^ ","^O",["^P",[["^ ","^Q","GRAAL_VERSION","^R","19.1.0"],["^ ","^Q","PATH","^R","/usr/local/graalvm/bin:/usr/local/nvidia/bin:/usr/local/cuda/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"],["^ ","^Q","GRAAL_DIR","^R","/usr/local/graalvm"],["^ ","^Q","NVIDIA_VISIBLE_DEVICES","^R","void"],["^ ","^Q","NVIDIA_DRIVER_CAPABILITIES","^R","all"],["^ ","^Q","NEXTJOURNAL_MOUNT_CUDA","^R","9.2-cudnn7-devel-ubuntu18.04"],["^ ","^Q","BASH_ENV","^R","/.bash_profile"],["^ ","^Q","LC_ALL","^R","en_US.UTF-8"],["^ ","^Q","LANGUAGE","^R","en_US.en"],["^ ","^Q","LANG","^R","en_US.UTF-8"]]],"^;","~m1562149816073","~:transclusion",["^ ","^15","~u5b45eb52-bad4-413d-9d7f-b2b573a25322","^16","~u5d1c83b8-6eff-4edf-b75f-3ddd3471f3a4","^17","0ae15688-6f6a-40e2-a4fa-52d81371f733"],"^1K",["^P",[]],"^Q","Clojure","~:docker/environment-image","docker.nextjournal.com/environment@sha256:8171ea23e969817f4f299ade8174729db72e57f3c5cfff6d436a2907dcb592fd","^S",["^ ","^T",1],"^U","^V","~:environment?",true,"^[","bash","^10","0ae15688-6f6a-40e2-a4fa-52d81371f733","^11","~uf7c3e6e0-9d7b-11e9-9a45-a4401e29db29","^12","runtime","~:changed?",false,"^13",null,"^14",["^14",["^ ","^15","~u02977b4a-ce26-4025-9a73-cbbae2521a70","^16","~u5d1c6441-5c77-4143-82c0-3ee3a92ac311","^17","4641de4c-67e7-4689-90f8-f22f1092707a"]],"~:runtime/environment-variables",[["^ ","^Q","CLOJURE_VERSION","^R","1.10.1.447"]],"~:diff",""]]]],"^10",17592189791941,"^9","~u02b25a80-2123-4f34-9c7b-ce8083b7c1ed"],"~:show?",true,"~:change-id","CW4Fq7zwdDkYD4c63d4nz4","^1A","published","~:article-collection",["^P",[]],"~:html","

React Server Side Rendering with GraalVM for Clojure

Usecase at Nextjournal

At Nextjournal, we perform server side rendering of notebooks to provide better user experience, especially for published notebooks. A visitor instantly sees the content of a notebook while the client side app loads. However, we do use a few Javascript-only dependencies like CodeMirror and ProseMirror, which prevents us from doing this server side rendering directly in Clojure on the JVM. Until now, we used a separate nodejs version of our browser app to perform this task. This necessitated a whole lot of code to expose ClojureScript app via an HTTP interface to the Clojure app as they were running in different VMs.

However, recently we switched our production JVM toGraalVM, which opened a new possibility since it comes with GraalJS, a JavaScript engine which aims to compete with today's faster JavaScript engines like V8 and JavaScriptCore on performance. It lets us embed the ClojureScript app into our Clojure app so it can perform the rendering in-process in the JVM.

Let me walk you through how this works.

We start with Nextjournal's default Clojure environment, which already uses GraalVM.

java
 -version

0.9s

Bash in Clojure

Clojure

\n\n\n

We use this deps.edn file to bring in some minimal dependencies to build a small ClojureScript app which can be used for server side rendering.

{:deps\n
 {org.clojure/clojure {:mvn/version "1.10.0"}\n
  reagent {:mvn/version "0.8.1"}\n
  org.clojure/clojurescript {:git/url "https://github.com/nextjournal/clojurescript"\n
                             :sha "da9166015f6a28b2c18fa7706e457901d02a5d81"}}\n
  }
deps.edn

Clojure

The sha key points to a ClojureScript version with some small adaptations to make work better with GraalJS.

Let's create a simple ClojureScript app using reagent . This will be used for server side rendering.

(ns my.app\n
  (:require\n
    [reagent.core :as reagent]\n
    [reagent.dom.server :as dom-server]))\n
\n(defn hello-component [name]\n
  [:div (str "Hello " name "!")])\n
\n(defn html [name]\n
  (dom-server/render-to-string [hello-component name]))
app.cljs

ClojureScript

Compile it using ClojureScript's command line interface.

clj
 -m 
cljs
.main
 -t 
graaljs
 -c 
my
.app

51.6s

Bash in Clojure

Clojure

As a first test to see if our ClojureScript app actually works in GraalJS, we can use Graal's standalone js command line operation to load our app and produce a call to our html function.

js
 --jvm -f 
out
/main
.js
 -e "console.log(my.app.html('GraalJS \uD83D\uDC4B'))"

13.6s

Bash in Clojure

Clojure

\n

However, we want to use this html function from within the JVM which runs our Clojure app. For this we can leverageGraalVM's polyglot abilities to load the JavaScript artifact from the ClojureScript app into the Clojure JVM and directly call the function with almost no overhead.

(import '(org.graalvm.polyglot Context Source))\n
\n(def context-builder\n
  (doto (Context/newBuilder (into-array String ["js"]))\n
     (.option "js.timer-resolution" "1")\n
     (.option "js.java-package-globals" "false")\n
     (.out System/out)\n
     (.err System/err)\n
     (.allowAllAccess true)\n
     (.allowNativeAccess true)))\n
\n(def context (.build context-builder))

0.8s

Clojure

Clojure

user/context

We use the Context/newBuilder polyglot API to create a Graal execution context and specifically only allow JavaScript execution. The js.java-package-globals is needed to prevent namespace collisions between Java packages and some ClojureScript namespaces in our codebase.

(defn execute-fn [context fn & args]\n
  (let [fn-ref (.eval context "js" fn)\n
        args (into-array Object args)]\n
    (assert (.canExecute fn-ref) (str "cannot execute " fn))\n
    (.execute fn-ref args)))

0.1s

Clojure

Clojure

user/execute-fn

The execute-fn helper makes it convenient to call our Javascript functions and provide them params directly from Clojure.

Before we can actually call our Javascript functions, we first need to load our little demo app into the context we created before.

(def app-js (clojure.java.io/file "out/main.js"))\n(def app-source (.build (Source/newBuilder "js" app-js)))\n(.eval context app-source)

10.6s

Clojure

Clojure

Vector (4) [ org.graalvm.polyglot.Value , "0x25b2672a" , "null" , Map ]

We utilize Graal's Source class to load the JavaScript artifact and evaluate it in the execution context to make the our html function available.

Now everything is coming together and we can perform server side rendering with our React ClojureScript app directly from Clojure.

(def result (execute-fn context "my.app.html" "Polyglot Graal \uD83C\uDF08"))\n(.asString result)

0.9s

Clojure

Clojure

"<div data-reactroot="">Hello Polyglot Graal \uD83C\uDF08!</div>"

In Nextjournal's case, we need to pass data of a notebook to the ClojureScript app. Rather than passing a string like \"Polyglot Graal \uD83C\uDF08\" to the html function for rendering, we actually pass a transit -encoded notebook. This is the same data we pass to the browser for client side rendering.

Performance

GraalJS is currently optimized for long running processes, and for some workloads it is still slower than V8 or JavaScriptCore. Our use case should fit well as we reuse the Context over and over and execution times get quite a bit faster after the first few requests. We also have less overhead for server side rendering now, because we can invoke Javascript functions directly without needing a complete http stack. In many cases this is faster than our old nodejs rendering process.

These are 5 measurements of server-side rendering calls in milliseconds after a warm-up phase of 1000 render calls.

0 items

You can see that for small notebooks GraalVM actually performs better because we don't have to go through a whole http stack. For large notebooks, it's in the same ballpark (~30% slower) and we're hopeful this will further improve in the future.

Where to go from here

While we already happy with our current setup, there a few things are still on our todo list:

  • Makefigwheel live code reloading work with the GraalJS so ClojureScript code changes are immediately reflected in the server side rendering in our development environment.

  • Finish up our ClojureScript patches to improve support for GraalJS and get them applied upstream.

  • Use code sharing for GraalJS so that the multiple render threads can share the AST to reduce the warmup time for optimal performance.

Upstream Issues

We ran into a number of issues and limitations, which we reported and were either resolved or worked around. We'll continue to work with the GraalVM and ClojureScript teams to get those issues resolved.

"]]


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK