I'm using Java 8 Nashorn to render CommonMark to HTML server side. If I compile and cache and reuse a CompiledScript
, a certain page takes 5 minutes to render. However, if I instead use eval
, and cache and reuse the script engine, rendering the same page takes 3 seconds.
Why is CompiledScript
so slow? (sample code follows)
What's a good approach for running Javascript code in Nashorn, over and over again as quickly as possible? And avoiding compiling the Javascript code more than once?
This is the server side Scala code snippet that calls Nashorn in a way that takes 5 minutes: (when run 200 times; I'm compiling many comments from CommonMark to HTML.) (This code is based on this blog article.)
if (engine == null) {
val script = scala.io.Source.fromFile("public/res/remarkable.min.js").mkString
engine = new js.ScriptEngineManager(null).getEngineByName("nashorn")
compiledScript = engine.asInstanceOf[js.Compilable].compile(s"""
var global = this;
$script;
remarkable = new Remarkable({});
remarkable.render(__source__);""");
}
engine.put("__source__", "**bold**")
val htmlText = compiledScript.eval()
Edit Note that the $script
above is reevaluated 200 times. I did test a version that evaluated it only once, but apparently then I wrote some bug, because the only-once version wasn't faster than 5 minutes, although it should have been one of the fastest ones, see Halfbit's answer. Here's the fast version:
...
val newCompiledScript = newEngine.asInstanceOf[js.Compilable].compile(s"""
var global;
var remarkable;
if (!remarkable) {
global = this;
$script;
remarkable = new Remarkable({});
}
remarkable.render(__source__);""")
...
/Edit
Whereas this takes 2.7 seconds: (when run 200 times)
if (engine == null) {
engine = new js.ScriptEngineManager(null).getEngineByName("nashorn")
engine.eval("var global = this;")
engine.eval(new jio.FileReader("public/res/remarkable.min.js"))
engine.eval("remarkable = new Remarkable({});")
}
engine.put("source", "**bold**")
val htmlText = engine.eval("remarkable.render(source)")
I would actually have guessed that the CompiledScript
version (the topmost snippet) would have been faster. Anyway, I suppose I'll have to cache the rendered HTML server side.
(Linux Mint 17 & Java 8 u20)
Update:
I just noticed that using invokeFunction
at the end instead of eval
is almost twice as fast, takes only 1.7 seconds. This is roughly as fast as my Java 7 version that used Javascript code compiled by Rhino to Java bytecode (as a separate and complicated step in the build process). Perhaps this is as fast as it can get?
if (engine == null) {
engine = new js.ScriptEngineManager(null).getEngineByName("nashorn")
engine.eval("var global = this;")
engine.eval(new jio.FileReader("public/res/remarkable.min.js"))
engine.eval("remarkable = new Remarkable({});")
engine.eval(
"function renderCommonMark(source) { return remarkable.render(source); }")
}
val htmlText = engine.asInstanceOf[js.Invocable].invokeFunction(
"renderCommonMark", "**bold1**")
The Nashorn engine has been deprecated in JDK 11 as part of JEP 335 and and has been removed from JDK15 as part of JEP 372. GraalVM can step in as a replacement for JavaScript code previously executed on the Nashorn engine. GraalVM provides all the features for JavaScript previously provided by Nashorn.
With Java 8, Nashorn, a much improved javascript engine is introduced, to replace the existing Rhino. Nashorn provides 2 to 10 times better performance, as it directly compiles the code in memory and passes the bytecode to JVM.
With the release of Java 11, Nashorn was deprecated citing challenges to maintenance, and has been removed from JDK 15 onwards. Nashorn development continues on GitHub as a standalone OpenJDK project and the separate release can be used in Java project from Java 11 and up.
If you wonder what jjs stands for, it stands for Java JavaScript. The command is located in the JDK_HOME\bin directory. The command can be used to run scripts in files or scripts entered on the command-line in interactive mode. It can also be used to execute shell scripts.
The variant of your code which uses CompiledScript
seems to re-evaluate remarkable.min.js
200 times - while your eval
based version does this once. This explains the huge difference in runtimes.
With just the remarkable.render(__source__)
precompiled, the CompiledScript
based variant is slightly faster than the eval
and invokeFunction
based ones (on my machine, Oracle Java 8u25).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With