Using @CompileStatic in Groovy for Predictable Performance
Learn how Groovy's @CompileStatic annotation enables static compilation, improves performance, and what trade‑offs to consider when applying it to your code.
15 Jan 2026, 07:07 UTC

Problem
When a Groovy script contains tight loops or frequently called utility methods, the default dynamic dispatch can add overhead that is hard to predict. Developers often wonder whether switching to static compilation will give them Java‑like speed without breaking existing code.
How @CompileStatic Works
The @CompileStatic annotation tells the Groovy compiler to treat the annotated element (class, method, or closure) as statically typed. Inside that scope:
- Method calls are resolved at compile time using the declared types.
- Dynamic features such as
methodMissing,propertyMissing, and runtime metaprogramming are disabled. - The generated bytecode resembles what javac would produce for equivalent Java code (primitive loops,
invokevirtualcalls).
If the compiler cannot prove a piece of code is statically safe, it automatically falls back to dynamic mode for that fragment.
Worked Example
The following script defines two methods that perform the same integer summation. One is annotated with @CompileStatic, the other is left dynamic.
import groovy.transform.CompileStatic
class Summation {
@CompileStatic
static long staticSum(int n) {
long total = 0
for (int i = 0; i < n; i++) {
total += i
}
return total
}
static long dynamicSum(int n) {
long total = 0
for (int i = 0; i < n; i++) {
total += i
}
return total
}
}
int limit = 1_000_000
long start = System.nanoTime()
long staticResult = Summation.staticSum(limit)
long staticTime = System.nanoTime() - start
start = System.nanoTime()
long dynamicResult = Summation.dynamicSum(limit)
long dynamicTime = System.nanoTime() - start
println "Static result: $staticResult, time: $staticTime ns"
println "Dynamic result: $dynamicResult, time: $dynamicTime ns"
Running the script with Groovy 3.0+ will show that the static version typically executes faster because the loop uses primitive int variables and direct iadd instructions, while the dynamic version incurs extra metaprogramming checks.
Verification Steps
- Compile the script to bytecode:
groovyc Summation.groovy- Inspect the static method:
javap -c Summation.classand look forinvokevirtualoriaddinstructions. - Inspect the dynamic method: you will see calls to Groovy's meta‑method dispatch (e.g.,
invokedynamicorInvokeStatictogroovy.lang.MetaClass). - Run the script and compare the printed nanosecond values. No specific speed‑up numbers are guaranteed; the purpose is to observe that the static version is not slower.
- To confirm the scope of the annotation, add a line that relies on a dynamic feature inside the static method, such as:
String s = "foo"; s.unknownMethod()- Re‑compile – the compiler will emit an error because
unknownMethodcannot be resolved statically.
Trade‑offs and Limitations
- Dynamic Groovy idioms (optional returns, untyped variables,
ExpandoMetaClass, categories) must be rewritten with explicit types or moved outside the@CompileStaticscope. - If a method relies on runtime metaprogramming to add behavior, the annotation will prevent that behavior from being applied.
- The fallback mechanism means that only the parts that can be proven static are compiled statically; mixed files may still contain dynamic bytecode.
Actionable Closing
Start by annotating utility classes or service methods that perform algorithmic work and do not rely on Groovy’s dynamic features. Verify the bytecode with javap and run a simple timing harness as shown above. If the static version compiles without errors and the performance is acceptable, you have gained predictable, Java‑like execution while keeping the rest of your codebase dynamic where needed.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.