Gianluca, I agree, this is of true concern for the Groovy language and ecosystem.
However, in the meantime, if there are severe bottlenecks, you should be able to refactor out any dynamic code in Grails Controllers and Gorm queries. Here is an example Grails project where this has been done: https://github.com/matrei/pingcrm-grails /Mattias Den ons 14 jan. 2026 kl 12:27 skrev Gianluca Sartori <[email protected]>: > Hi folks, > > I am personally worried about performance going forward since we've seen > perceivable slowdowns porting our applications to Grails 7/Groovy 4 and we > use @CompileStatic almost everywhere except on Controllers. On Services we > use @CompileDynamic only on methods implementing GORM queries. We > use @GrailsCompileStatic on Domain classes. > > PS: No, we are not going INDY OFF, I don't want this issue to fade away > from me (and it's not future proof). > > How can we move forward thinking about performance optimizations? I would > expect improved or stable performance on future releases rather than lower > performance. > > I've asked ChatGPT, how much of this is true? How can we move forward > thinking about performance as something important to consider for the > language? > > *Q: *Why is Groovy 5 slower than Groovy 3? > > *A:* > > The reason isn’t a single bug, but rather a combination of architectural > and implementation changes. Let me break it down carefully. > ------------------------------ > 1. *AST Transformations and Compilation Changes* > > Groovy 5 introduced a *new compilation infrastructure* that relies more > heavily on *AST (Abstract Syntax Tree) transformations*. While this makes > the language more flexible and enables new features (like better type > checking and improved static compilation), it also *adds overhead at > runtime*, especially for dynamic code. > > - > > In Groovy 3, some dynamic method calls were optimized in a simpler way. > - > > Groovy 5 performs *additional type inference and checking*, even at > runtime for some constructs, which can slow execution. > > ------------------------------ > 2. *JVM Dynamic Call Optimizations* > > Groovy 5 changed its *call site caching mechanism*: > > - > > Groovy 3 used *classic call site caching* that was very fast for > repeated method calls. > - > > Groovy 5 introduced a *more sophisticated but heavier mechanism* to > improve correctness with new language features (like traits and records). > - > > This improves *compatibility and correctness*, but can be slower for > tight loops with many dynamic calls. > > ------------------------------ > 3. *Library and API Changes* > > Groovy 5 upgraded many core libraries and dependencies, which sometimes > means: > > - > > Using newer, safer APIs that are a bit slower. > - > > Removing some low-level optimizations present in Groovy 3 for backward > compatibility reasons. > > ------------------------------ > 4. *Static Compilation Differences* > > If you used @CompileStatic in Groovy 3: > > - > > The bytecode generated in Groovy 5 can sometimes be *larger and less > optimized*, because Groovy 5 focuses on *full feature support*, > sometimes at the expense of raw performance. > - > > Groovy 5’s static compilation is closer to Java semantics, which adds > minor overhead in certain dynamic-heavy scenarios. > > ------------------------------ > 5. *Benchmarks and Real-World Performance* > > - > > For purely *dynamic Groovy scripts*, Groovy 5 tends to be slower than > 3. > - > > For *statically compiled code*, the difference is smaller, but > sometimes Groovy 5 can even be slightly slower due to the AST overhead. > - > > Microbenchmarks in loops and repeated method calls show *Groovy 5 can > be 20–50% slower* in some cases. > > ------------------------------ > > ✅ *In short:* > Groovy 5 trades raw runtime speed for *better correctness, new language > features, and more powerful static compilation*. For heavy dynamic Groovy > code, this can feel noticeably slower than Groovy 3. > 1️⃣ JVM Flags to Monitor invokedynamic > > To count and monitor invokedynamic usage: > > -XX:+PrintCallSiteCounters > -XX:+UnlockDiagnosticVMOptions > -XX:+LogCompilation > -XX:+PrintInlining > > > - > > -XX:+PrintCallSiteCounters → shows how many times each call site was > created or invoked > - > > -XX:+LogCompilation + -XX:+PrintInlining → lets you see which calls > the JIT inlines > > When running Groovy, pass them to the JVM with -J: > > groovy -J-XX:+PrintCallSiteCounters myscript.groovy > > ------------------------------ > 2️⃣ Test Script > > Here’s a small script that tends to slow down in Groovy 4: > > // test.groovy > 10_000.times { i -> > def a = "string" > def b = a + i > println b > } > > > - > > Uses *dynamic operators* (+) > - > > Uses *variable types* > - > > Calls println dynamically > > This simulates a *hot loop* with many dynamic call sites. > ------------------------------ > 3️⃣ Run with Groovy 3 vs Groovy 4 > > # Groovy 3 > groovy -J-XX:+PrintCallSiteCounters test.groovy > # Groovy 4 > groovy -J-XX:+PrintCallSiteCounters test.groovy > > ------------------------------ > 4️⃣ Sample Output > > CallSite counters:invokedynamic site #1: 10000 invocations, 0 > invalidationsinvokedynamic site #2: 10000 invocations, 50 > invalidationsinvokedynamic site #3: 10000 invocations, 200 invalidations > > > - > > *invocations* → how many times the call site was called > - > > *invalidations* → how many times the call site had to be recreated > > *Analysis*: > > - > > Groovy 3: few call sites, few invalidations → loop runs fast > - > > Groovy 4: more dynamic call sites + more invalidations → overhead > increases → slower performance > > ------------------------------ > 5️⃣ Profiling with @CompileStatic > > You can test the difference with *static compilation*: > > import groovy.transform.CompileStatic > > @CompileStatic > void testStatic() { > 10_000.times { i -> > def a = "string" > println a + i > } > } > > testStatic() > > > - > > Compare *dynamic vs static* > - > > You’ll see that static compilation avoids most call site invalidations > → loop is much faster > > ------------------------------ > ✅ Key Takeaways > > > 1. > > invokedynamic itself is *not slow* > 2. > > Groovy 4 is slower than Groovy 3 because: > - > > it uses *more invokedynamic call sites* > - > > more *call site invalidations* > - > > the *MOP* is more dynamic and general > 3. > > Practical ways to improve speed: > - > > Use @CompileStatic for hot loops > - > > Avoid runtime modifications to metaClass > - > > Reduce dynamic operator usage on heterogeneous objects > - > > Profile call sites to find bottlenecks > > > Gianluca Sartori > -- > https://dueuno.com >
