Skip to content
Static vs Dynamic and Performance
step 1/4

Reading — step 1 of 4

Learn

~2 min readSpock, Gradle, Modern Groovy

Groovy is dynamic by default. Every method call goes through reflection-like dispatch. Modern features let you opt into static dispatch for performance.

Default: dynamic

def double(x) {
    x * 2
}

double(5)        // works for int
double("hi")     // works for String — "hihi" via String * 2

The runtime checks x.multiply(2) (String) or x * 2 (numeric). Slow but flexible.

@CompileStatic

import groovy.transform.CompileStatic

@CompileStatic
def sum(List<Integer> nums) {
    int total = 0
    for (int n : nums) {
        total += n
    }
    return total
}
  • Method calls resolved at compile time
  • Type errors caught early
  • Closer to Java performance
  • metaClass mods don't apply

Benchmarks: dynamic Groovy is often 2-5x slower than static Groovy. For hot loops, the difference is measurable.

When to use which

Static (@CompileStatic):

  • Performance-critical code
  • Library code where types matter
  • Refactoring safety (compile errors catch breakage)

Dynamic (default):

  • DSLs (Gradle, Spock — they NEED dynamic)
  • Configuration code
  • Glue scripts
  • Where flexibility outweighs speed

@TypeChecked

Checks types at compile time but keeps dynamic dispatch:

import groovy.transform.TypeChecked

@TypeChecked
def greet(String name) {
    return "Hello, $name"
}

// greet(42)  // compile error: Integer != String

Compromise — type safety without losing metaprogramming.

Static type checking caveats

@CompileStatic
def ambiguous(map) {
    map.size()        // ERROR — what is map's type?
}

Without type annotation, the compiler can't infer. Use:

  • def works (becomes Object)
  • Explicit types: Map map, List<Integer> nums
  • Generic params: def <T> processList(List<T> items)

Performance comparison

Dynamic Groovy:    100 ns/op
@TypeChecked:        80 ns/op  (slight win — early type checks)
@CompileStatic:      30 ns/op  (3x faster)
Pure Java:          25 ns/op  (5% faster than @CompileStatic)

Numbers vary; measure your code.

Mixing static and dynamic

You can apply @CompileStatic per-class, per-method, or globally:

// Class-level — applies to all methods
@CompileStatic
class Calculator { ... }

// Method-level
class Calculator {
    @CompileStatic
    int fastAdd(int a, int b) { a + b }

    def slowAdd(a, b) { a + b }    // dynamic
}

// File-level (with @CompileStatic on a top-level class)

Indy (invokedynamic) backend

Groovy 2.1+ has an invokedynamic mode that dramatically speeds up dynamic calls (60-80% of static performance). Enable with the -indy JAR. Most modern Groovy uses indy by default.

When the dynamism matters

  • Spock: relies on methodMissing, AST transformations
  • Gradle: every block's delegate magic
  • HTTP/JSON builders: json.user { ... }user is intercepted via methodMissing
  • Mocking libraries: Mock(Class) generates dynamic stubs

These DON'T work with @CompileStatic. Keep that code dynamic; static-compile your business logic.

The verdict

  • New Groovy code: prefer @CompileStatic unless you NEED dynamic
  • DSLs and config: keep dynamic
  • Performance: profile first; static usually wins
  • Maintenance: static catches more bugs at compile time

Groovy started as fully dynamic — its evolution toward optional static is a recognition that dynamic wasn't always the right answer.

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…