Performance
Prerequisites: Static compilation
Groovy performance is usually a non-issue for scripts, builds, and tests. When it does matter, the rules are simple: measure first, then apply static compilation and JVM-level fixes where the time actually goes.
Where time goes
- Start-up and compilation — running a
.groovyfile compiles it first. For short scripts this dominates. Precompile withgroovycfor scripts that run often. - Dynamic dispatch — each dynamic call goes through the MOP. Modern
Groovy uses
invokedynamic, which the JIT optimizes well, but hot loops still benefit from@CompileStatic. - Boxing and BigDecimal —
defnumbers are objects and decimal literals areBigDecimal. Exact, but slower thandouble. - Everything else — I/O, database queries, and algorithms, exactly as in Java.
BigDecimal vs. double
import groovy.transform.CompileStatic
@CompileStatic
double sumDoubles(int n) {
double total = 0
for (int i = 0; i < n; i++) total += 0.1d
total
}
BigDecimal sumDecimals(int n) {
BigDecimal total = 0
for (int i = 0; i < n; i++) total += 0.1
total
}
println sumDoubles(1_000_000)
println sumDecimals(1_000_000)
100000.00000133288
100000.0
The double version is much faster but accumulates rounding error; the
BigDecimal version is exact. Use BigDecimal for money and
double (with static compilation) for heavy numeric work.
Collection idioms
import groovy.transform.CompileStatic
@CompileStatic
int countMatches(List<Integer> xs) {
int n = 0
for (int x : xs) if (x % 3 == 0) n++
n
}
def data = (1..100_000).toList()
println countMatches(data)
println data.count { it % 3 == 0 }
def names = (1..5).collect { "user$it" }
def sb = new StringBuilder()
names.each { sb << it << ',' }
println sb
33333
33333
user1,user2,user3,user4,user5,
- Idiomatic
count { }and a static loop give the same answer; profile before rewriting readable code. - Build large strings with a
StringBuilder(<<appends) rather than repeated+or GString concatenation in a loop. GStrings are evaluated lazily and converted each time they're used as aString— calltoString()once when reusing one.
Measuring properly
- Use a profiler (IntelliJ's, async-profiler, or Java Flight Recorder
with
-XX:StartFlightRecording) to find hot spots. - Benchmark with JMH (there's a Gradle plugin); naive timing loops are distorted by JIT warm-up.
- Watch memory: closures and metaclass changes allocate; heavy metaclass churn used to deoptimize call sites globally (Groovy 6 scopes that invalidation to the changed class).
Checklist
- Measure — find the 5% of code that matters.
- Add
@CompileStaticto hot classes and give them real types. - Use primitives and
doublein numeric hot paths. - Fix algorithms and I/O (batching, caching, fewer queries) — usually the biggest win.
- For start-up-sensitive tools, precompile or consider GraalVM native images (Groovy 6 improves support, including for dynamic code).