Clojure: React Server Side Rendering with GraalVM
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.
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 thishtml
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 }
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]))
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.
-
GraalJS regex engine bug affecting lookarounds: GraalJS issue #162 (the fix shipped already in Graal 19.1.0)
-
GraalJS namespace collisions of Java package globals with ClojureScript namespaces: GraalJS issue #164 and ClojureScript CLJS-3087
-
Optimized
:graaljs
builds from ClojureScript are missing the bootstrap code: ClojureScript CLJS-3113 -
Make main files produced by ClojureScript for
:graaljs
targets work without java package globals: ClojureScript CLJS-3089
"]]
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK