Skip to content
HN On Hacker News ↗

JavaFX 27 as a GraalVM Native Image on a Raspberry Pi 5

▲ 55 points • 13 comments • by 0x54MUR41 • 3w ago • HN discussion ↗

Pangram verdict · v3.3

We believe that this entire text is human-written.

0 %

AI likelihood · overall

Human
100% human-written 0% AI-generated
SEGMENTS · HUMAN 1 of 1
SEGMENTS · AI 0 of 1
WORD COUNT 1,194
PEAK AI % 0% · §1
Analyzed
Sep 22
backend: pangram/v3.3
Segments scanned
1 windows
avg 1194 words each
Distribution
100 / 0%
human / AI fraction
Verdict
Human
Pangram v3.3

Article text · 1,194 words · 1 segments analyzed

Human AI-generated
§1 Human · 0%

We initially used GraalVM Native Image as a way to deploy JavaFX to mobile targets. However, after seeing our mobile apps feel snappier than our desktop apps, we gradually migrated all of our GUI and CLI applications over to ahead-of-time (AOT) compilation. GUIs mostly run in interpreted mode (users would have to click thousands of times to reach the JIT threshold), and with AOT we see up to 90% reductions in startup and first visit times, which results in a noticeably better user experience.Table 1. AtlantaFX sampler comparison on a Raspberry Pi 5AtlantaFX Sampler (RPi5)jlink (JIT)jlink + CDS (JIT)Native Image (AOT)Distribution size139 MB155 MB (+12%)124 MB (-11%)Time to first window3.6 s2.7 s (-26%)0.5 s (-86%)First visit: HTMLEditor (WebView)2.39 s2.31 s (-3%)0.22 s (-91%)First visit: Overview (FXML)1.90 s1.50 s (-21%)0.32 s (-83%)Private memory after startup224 MB244 MB (+9%)164 MB (-27%)Private memory after all pages1424 MB1396 MB (-2%)634 MB (-55%)In fact, starting the AtlantaFX sampler on a high-end Ryzen 9 9950X desktop with jlink + CDS takes more than 2.5 times as long (1.3 s) as the same application running AOT-compiled on a tiny Raspberry Pi 5 (0.5 s, see benchmarks). The whole run is captured in a short video walkthrough of all pages, including FXML, WebView, MediaPlayer, and AWT integration.JavaFX actually works very well in Native Image once everything is configured, but getting a complex application running for the first time is typically not a straightforward experience. This steep initial hurdle has unfortunately earned JavaFX, and desktop Java in general, a reputation of being incompatible or working poorly with Native Image. I think a big part of this stems from complex, specialized tooling that performs a lot of "magic" behind the scenes to hide the native layers. Whenever anything breaks, it produces errors that many Java developers don’t know how to debug.In this post I want to demystify the internals of these tools, and show why we built our own tooling to run the latest JavaFX 27 release with the latest enterprise Oracle GraalVM on desktop targets.Three ApproachesUnlike frameworks such as Quarkus or Micronaut that were designed for ahead-of-time compilation and controlled server environments, JavaFX is a large and highly dynamic GUI framework that long predates native-image and needs to run on arbitrary client machines with different operating systems. Making that whole stack work requires a dedicated solution with linker fixes, substitutions, and nearly a thousand metadata entries. Gluon’s Substrate and BellSoft’s Liberica NIK are the two established options, and we just released StaticFX as a third.Gluon SubstrateGluon’s Substrate is a complex toolchain that can port JavaFX to just about anything, including Windows, macOS, Linux, and the mobile targets iOS and Android. It can create executables with and without JavaFX, shared and static libraries, deal with resources, add custom C extensions, build OS-specific bundles and installers, and even do remote deployments to small targets via ssh.Getting all of that to work requires modified builds and custom tools for practically every layer, starting at the GraalVM distribution to mobile tooling and dedicated Maven and Gradle plugins. The result is quite an impressive stack, and Gluon deserves credit for contributing a lot of the upstream changes that let JavaFX run everywhere.However, the number of moving targets makes for a brittle combination that can easily break. According to Gluon: "every new release of iOS, Android, the AOT compiler, the JVM components or the JDK required patches and adjustments", so they have been pivoting towards OpenJDK Mobile. That is a more standard JDK + Leyden approach with an open-world model that removes the need for metadata and should significantly simplify the user experience.That being said, the existing tooling still gets maintenance updates (see docs) and is comparably user-friendly considering the enormous underlying complexity. However, the latest versions Gluon currently supports are GraalVM 23 Community Edition (CE) and JavaFX 21, and there is currently no supported way to use a newer JavaFX release.We’ve used Gluon’s tools internally for 1-2 years and still refer to it for mobile targets. JavaFX on mobile actually works far better than we originally expected, and it’s really nice to be able to deploy to all five targets with full 2D and 3D rendering as well as hot-reload of styling and layouts (see Mobile Scope).A common point of confusion is the licensing model. Only their rich component libraries require a license, but the build tools are free to use for all platforms.BellSoft’s Liberica NIKBellSoft took a much more focused approach and created a GraalVM CE distribution dedicated to desktop apps: Liberica NIK (full). If you need to get something running quickly, NIK is the easiest way to get started.Their distribution bundles JavaFX and Swing/AWT modules, as well as corresponding metadata for both. Users only need to set GRAALVM_HOME and can use it with GraalVM’s stock native-maven-plugin. Even a stock Oracle GraalVM ships the static AWT archives, but users have to generate the required metadata themselves.At the time of writing, NIK (full) supports the LTS versions GraalVM 25 CE with JavaFX 25 and the GPU-accelerated pipelines on the four main desktop architectures. They currently do not support the software pipeline or linux-aarch64 targets. Neither Gluon nor BellSoft supports the media and web modules in a Native Image.HEBI’s StaticFXWe have been using Liberica NIK for our desktop applications for about two years, and it has admittedly worked quite well. However, some of our applications measure latency at the sub-millisecond level, and we ran into issues with incorrect measurements because the community edition’s escape analysis eliminated far fewer allocations than HotSpot, on top of differences in GC behavior. Our worst allocation source turned out to be a simple new double[]{a,b} in Math::pow, which would never allocate on HotSpot.Since the Oracle GraalVM (formerly Enterprise Edition) became free to use under the GFTC license in 2023, we wanted to switch to get better escape analysis, G1 GC on all platforms, and other enterprise-only features like profile-guided optimization (PGO).We also needed to upgrade to a newer JavaFX version for hebi-charts, which exposes high-performance JavaFX visualizations through a native shared library to other languages like Python and C++. Our goal was to be able to generate graphics and images through SSH on a linux-aarch64 device using the Headless mode introduced in JavaFX 26 with the software pipeline.Neither Substrate nor Liberica NIK supports that specific combination, so we ended up creating StaticFX. In a way, the decoupling of JavaFX from the JDK turned into a nice benefit because it can be treated like a normal library that we can update at will without needing vendor support.Rather than taking over the build system, StaticFX runs as a regular GraalVM Feature on the stock toolchain, and can pass all the required configurations to native-image without modifying any of the jfx sources. It also supports dynamic linking where static linking is not available, so it can support all JavaFX modules, including Media, Web, and Swing interop.It consists of two artifacts: jfx-static-libs for version-specific static archives and metadata, and jfx-static-feature for the relatively version-agnostic glue. GraalVM automatically picks up configuration files from artifacts on the classpath, so users only need to add two dependencies next to the org.openjfx jars:<dependency> <groupId>us.hebi.graalvm</groupId> <artifactId>jfx-static-feature</artifactId> <version>1.0</version> </dependency> <dependency> <groupId>us.hebi.graalvm</groupId> <artifactId>jfx-static-libs</artifactId> <version>${javafx.version}</version> <scope>runtime</scope>