Show HN: Gradle plugin for faster Java compiles

33 sgammon 31 6/3/2025, 7:59:19 PM github.com ↗
Hey HN,

We've written a pretty cool Gradle plugin I wanted to share.

It turns out if you native-image the Java and Kotlin compilers, you can experience a serious gain, especially for "smaller" projects (under 10,000 classes).

By compiling the compiler with native image, JIT warmup normally experienced by Gradle/Maven et al is skipped. Startup time is extremely fast, since native image seals the heap into the binary itself. The native version of javac produces identical outputs from inputs. It's the same exact code, just AOT-compiled, translated to machine code, and pre-optimized by GraalVM.

Of course, native image isn't optimal in all cases. Warm JIT still outperforms NI, but I think most projects never hit fully warmed JIT through Gradle or Maven, because the VM running the compiler so rarely survives for long enough.

Elide (the tool used by this plugin) also supports fetching Maven dependencies. When active, it prepares a local m2 root where Gradle can find your dependencies already on-disk when it needs them. Preliminary benchmarking shows a 100x+ gain since lockfiles prevent needless re-resolution and native-imaging the resolver results in a similar gain to the compiler.

We (the authors) are very much open to feedback in improving this Gradle plugin or the underlying toolchain. Please, let us know what you think!

Comments (31)

pjmlp · 8h ago
Personally the solution to faster build times in Gradle, is to keep using Maven.

I never touch Gradle unless there is no way around it, like Android.

However, this looks like an interesting idea.

gerardnico · 6h ago
Exactly, gradle is so slow that I ditch it into the ground. Just starting a blank project is so a bad experience. Nothing beat maven and a couple of shell script.
pjmlp · 6h ago
I am convinced if they hadn't got Google's sponsorship, when Android decided to move into Gradle during Eclipse to InteliJ migration, we would be talking about Gradle as much as Grails is still discussed at local JUGs.
kasabali · 1h ago
Same, but also for Kotlin
jart · 20h ago
OK let's say I have an online store I wrote in Spring Framework. What does my before and after development workflow look like after adopting your plugin? It's been a while since I've messed with Java, but how many seconds could it possibly need to fetch a bunch of jar files? I'd love to see an in-the-life-of kind of screencast of what the daily grind looks like for normal java devs these days, so I can understand why it's so slow.

For example, what does resolution mean? Does that mean fetching the pom.xml files from sonatype to figure out the dependency graph? Don't those HTTP requests normally go fast? Is Elide sort of like setting up an HTTP caching proxy between corp and sonatype?

sgammon · 19h ago
> OK let's say I have an online store I wrote in Spring Framework. What does my before and after development workflow look like after adopting your plugin?

Basically, you install this plugin, and it tells the `JavaCompile` tasks normally used by Gradle to `isFork = true` and use `elide javac -- ...` for the compile command.

So, javac invocations travel over the CLI instead of through the Tooling API, which Gradle normally uses, and which is effectively running in Gradle's daemon VM, and thus is subject to JIT warmup.

Through Elide, since it's a native binary, all that JIT warmup is skipped, and you are effectively choosing to balance more toward quick startup and wall-clock time (with many small calls) instead of waiting for JIT warmup to make the Gradle daemon fast with javac.

Otherwise, it's a completely identical javac experience. Running `elide javac -help` produces identical help output. Identical inputs should produce identical outputs, and we build it at JDK 24 so it can support `--source/--target/--release X` and friends for anything older down to JDK 8.

> For example, what does resolution mean? Does that mean fetching the pom.xml files from sonatype to figure out the dependency graph?

It means resolving the graph from declared (direct) dependencies, downloading pom.xml metadata, downloading JARs, unpacking it all to disk, and providing a local `.m2` Maven-compliant root, sort of like Node does for `node_modules/`. Ours lives in `.dev/dependencies/m2` once you run `elide install`.

Elide embeds Maven's resolver, so resolution semantics are identical to Maven's. We do some small optimizations to make fetching fast (i.e. initializing related classes at build time, that sort of thing), but honestly not much. Just building Maven's resolver like this yields a major gain, and not messing with it preserves expected behavior.

Since Elide emits these deps in a Maven Local-style root, Gradle just finds them on disk when it needs them, so it doesn't need to engage its resolver or fetcher at all.

> Don't those HTTP requests normally go fast?

It is worth noting that Gradle seems confined to HTTP/1.1 and poor connection pooling even today, so it's not that hard to beat.

> Is Elide sort of like setting up an HTTP caching proxy between corp and sonatype?

We don't proxy or anything, this fetching still happens through normal Maven Central unless configured otherwise in your `elide.pkl` manifest. For now, deps are placed in the local project, but we want to move to a central cache and link like modern NPM installers do.

sgammon · 19h ago
(Oh, if you are seeing how the plugin installs a Maven repository, that's just to ship our own plugin artifact and Gradle Catalog, for easy dependency use. maven.elide.dev doesn't proxy to central or anything like that.)
alisonatwork · 18h ago
Hypothetically, if you could daemonize javac, would JIT eventually kick in over multiple recompiles of the same code? The obvious use case for this would be IDEs, but I imagine it could work in CI too if you had some kind of persistent "compiler as a service" setup that outlived the runners.

Not to detract from the cool work done here, just curious if this other approach has been tried too.

sgammon · 18h ago
> Hypothetically, if you could daemonize javac, would JIT eventually kick in over multiple recompiles of the same code?

This is actually how Gradle works by default; in fact, even if you pass `--no-daemon`, it will start a daemon, and just kill it after the build (lol). It's daemon-first, daemon-only, because of this exact issue.

As I understand it, Gradle's daemon timeout is typically 10 minutes, but can be extended. We are guessing that this style rarely hits the 10k class threshold where JIT performance converges with native, especially since Gradle also supports incremental compilation, and that further impedes the progress toward a warm JIT. Gradle focuses hard on build caching and incremental compilation, and this is conceptually in contention with reaching a warm JIT, ironically.

Native Image has JIT capabilities (just not as mature as Hotspot yet), and keeping the daemon alive would probably still yield wins. We haven't tried it yet and that's a good idea

alisonatwork · 17h ago
I'm aware of the Gradle daemon, but wasn't sure if it only handled dependency resolution and other build orchestration tasks or if it also ran the compiler in the same JVM instance. Last time I worked somewhere using Gradle I think we forked the compiler anyway to ensure the project was built on exactly the same Java version regardless of what the Gradle environment was running in, so in that case there is definitely room for startup optimization.

I do recall the Gradle daemon living much longer than 10 minutes, though. The docs say 3 hours is default, although if really trying to maximize the JIT advantage it perhaps would make sense to keep it alive as long as possible.

sgammon · 17h ago
> or if it also ran the compiler in the same JVM instance

Honestly, Gradle's toolchain resolution mechanisms have changed so much, it is a little hard to keep track. Good point

> The docs say 3 hours is default

Huh, I'm not sure where I got 10 minutes. I'll research some more about these assumptions, but even so, having used Gradle professionally and personally for many years (like you), I wonder how often I would have hit that threshold. I was mostly working in smaller projects and companies, though, so my experience could be different from the norm. Even assuming engineers hit that threshold, it would be toward the latter end of that window and only until the end of that window. An experience optimized for cold-start and non-reflection balances this approach, and is even build-cacheable with standard tools like sccache. There are still a lot of projects where JIT-based javac would be optimal

> perhaps would make sense to keep it alive as long as possible

It would for sure. I'm not sure why Gradle was never able to execute on Bazel's full vision for remote execution. Probably a hermeticity problem, considering the challenges they already seem to face with e.g. the configuration cache.

In any case, this is great feedback, thank you :)

alisonatwork · 16h ago
I work professionally in Python these days, but I've just spent a bit of time digging into the contemporary situation because the JVM environment is much more interesting to me, and found this spec: https://build-server-protocol.github.io

Seems the Scala guys have doubled down on the "compiler as a service" approach, presumably because their compile time story continues to be painful. But also looks like the same solution is used for the VS Code Java/Gradle integration, so seems like this might be the more conventional way to go for traditional JVM projects.

For processes where the JIT takes a while to kick in, but also you don't want to waste memory keeping JVMs alive while not doing anything (and compilation could be a good example of that), I wondered if there was a way to snapshot and restore the JVM state and turns out some people are experimenting with that too: https://openjdk.org/projects/crac/

It's all neat stuff!

sgammon · 16h ago
> I work professionally in Python these days

Elide can run Python using GraalPy, and we want to do some similar toolchain stuff in that universe. We are working on an integration with uv. Thanks also for linking this Build Server Protocol thing -- this looks very cool, very relevant, and I don't think I've seen this yet.

> Seems the Scala guys have doubled down on the "compiler as a service" approach, presumably because their compile time story continues to be painful

I wonder if we could run the Scala compiler. Reflection is a challenge in native mode and iirc Scala is reflection heavy, but I have no idea, I've never used it myself. I'll look into this

> snapshot and restore the JVM state and turns out some people are experimenting with that too

There are many efforts in this area: CrAC, Leyden, the new JDK 24 AOT stuff, and pre-warmup stuff before that. These all do make a difference but Native Image takes AOT optimization to a whole other level and seems to perform better for quick-startup quick-shutdown cases[1].

Elide also runs JVM code via Truffle/Espresso, and we're going to need a regular JVM integration. On this side CrAC and so on will be useful to cut down on startup time

[1]: https://github.com/simonis/LeydenVsGraalNative

norir · 13h ago
For scala 2 at least, the standard library is very heavy and the compiler of course relies on it. It has a god object, predef, that cascades massive classloading if you touch anything in it. As a result, it is essential for latency to keep a warm jvm up to avoid reloading it.

Incremental rebuilds can actually be not too bad in scala when a warm jvm is used for compilation as it is by sbt and other scala build tools, but sbt also by default uses the warm jvm to run in process tests. This too can be reasonably quick but leads to problems if there are any resource leaks in the user's test suite.

Thus with sbt, one must either exercise discipline and care with tests to not leak anything or you will periodically have to restart sbt, which can be very painful because it is much heavier than the already heavy scala compiler.

It's a disappointing state of affairs because while there is a lot to like about scala, it is impossible to escape the bloat. Even if you opt out of sbt, the lack of modularity in the standard library forces workarounds, such as native-image, which had its own issues (including horrific build times for making native images and weak reflection support). Even if you or your org avoids the direct pitfalls, it is likely at some point you will end up debugging a dependency that doesn't.

I personally abandoned scala due to this bloat, which was a shame because I find it to have the best combo of expressiveness and pragmatism of any established general purpose languages.

sgammon · 19h ago
Volker Simonis over at Amazon did some very cool benchmarks showing the impact this can have (i.e. native-imaging javac):

https://github.com/simonis/LeydenVsGraalNative

lemming · 8h ago
The underlying Elide tool looks amazing, and potentially solves all sorts of pain for me. Does the Kotlin compiler support plugins, e.g. serialisation? I can imagine they might be too reflective.
sgammon · 1h ago
> The underlying Elide tool looks amazing

Thank you!

> Does the Kotlin compiler support plugins, e.g. serialization?

We don't support Kotlin Serialization yet, but KotlinX is fully embedded and ready. You can reach for things like KotlinX Coroutines without installing a single JAR.

Bring-and-use-your-own-plugins might be challenging, yeah, but we plan to at least build in the KotlinX plugins you would want by default (serialization included). So thank you for this note, it's helpful and I'll add a notch next to that feature :)

kristianp · 18h ago
So elide is a tool to run {js|ts|py} [1]. How does it compile javac to native?

[1] https://github.com/elide-dev/elide

sgammon · 17h ago
In addition to being a runtime, Elide embeds `javac` and `kotlinc`. Here it is being used as a build toolchain, but it can also run those languages, through GraalVM/Truffle.
mdaniel · 15h ago
What.is.happening.here. Are they committing node_modules or something?

  $ git clone https://github.com/elide-dev/elide.git
  Receiving objects: 100% (126086/126086), 643.85 MiB | 3.02 MiB/s, done.
sgammon · 14h ago
There are some JARs we must commit to source because they are built with patches from upstream GraalVM components. That's probably the big weight there.

We have an issue in our repo tracking those upstream PRs. Once merged, we won't need to do this anymore. We should probably publish them in our own Maven repo to avoid this kind of experience. Thanks for the push to take care of that cruft :P

You can find them in `third_party/oracle`. You also don't need to build from source -- Elide ships as a binary for Linux amd64/macOS arm64. You are totally welcome to though, and if you encounter issues, hop in our discord and we'd be glad to help: https://elide.dev/discord

sgammon · 16h ago
(In other news, I've added Kotlin to the list there, as {...|kt|kts}, so thank you for reporting this as well! We really need some docs updates and I'll make sure to clarify this stuff)
linksbro · 16h ago
Would this result in the same .class that javac produces? I.e. at runtime is code compiled with this fundamentally different than code compiled with javac?
sgammon · 16h ago
This results in identical .class bytecode that `javac` produces, yes, because it's literally just `javac`, but built as a native binary instead of a Java entrypoint.

We build at JDK24 and support `--source/--target/--release` so you can build against earlier JVM targets.

If you find any output differences between Elide and JDK24 (Oracle GraalVM), then we'd consider that a bug. Similarly, we accept identical compile flags and emit identical warnings/messages, as under the hood we are just using the same compiler APIs but natively.

normie3000 · 12h ago
Is there an equivalent maven plugin?

Also what's the licence for elide?

sgammon · 12h ago
Not yet, but we are working on a Maven plugin :) if you join our Discord or file an issue, I can update you when its out.

Elide is licensed under MIT https://github.com/elide-dev/elide

rjwalters · 12h ago
Do you have benchmarks? How much of a difference does it make?
sgammon · 10h ago
Benchmarking a toy case (hello world) shows a 20x performance difference; this is true for both kotlinc and javac, when benchmarked through hyperfine. YMMV, but benchmarks of different build styles show native converging with JIT at about 10k classes[1]

So, if your project is 10k classes or less, you are looking at "up to" a 20x improvement in compile time vs. stock javac.

On the dependency fetching side, we haven't benchmarked yet. It definitely feels like 100x+ though on a wall-clock basis.

[1]: https://github.com/simonis/LeydenVsGraalNative

re-thc · 15h ago
Interesting idea. I wonder how this would compare to native-imaging Gradle or Maven itself.
sgammon · 14h ago
Elide's resolver is just Maven's, and we use Maven's own components to read poms. Native-imaging Gradle itself is really challenging because Gradle relies so much on reflection, and really is architected to account for the JIT case (i.e. emphases on the daemon).

I haven't found a way to do it yet so this plugin is the best alternative so far.

Zardoz84 · 11h ago
interesting. I wonder how compares against mavend (mvnd , aka maven as a Daemon service) and his aggressive parallelization.