Hi,

I think it would be nice if we supported caching for Groovy scripts to speed up execution time for subsequent runs. Below is a GEP style proposal for that. What do you think?


 GEP: Persistent Script Compilation Cache for the Groovy Command-Line
 Launcher


   Metadata

*Type:* Feature
*Status:* Draft
*Target Groovy Version:* TBD
*Author:* Per Nyfelt
*Discussion:* TBD
*Created:* 2026-06-01


   Abstract

This proposal introduces an optional persistent compilation cache for Groovy scripts executed through the |groovy| command-line launcher.

When a script is executed, the launcher may store the generated class files in a local cache. On later executions, if the script source and relevant compilation inputs are unchanged, the launcher may load the cached class files instead of parsing, transforming, and compiling the script again.

The goal is to improve startup time for short-lived Groovy scripts and command-line tools while preserving current semantics by default.


   Motivation

Groovy is frequently used for scripting, automation, build tooling, data processing, and command-line utilities. In these use cases, process lifetime is often short, and startup overhead matters.

For simple scripts, actual execution time may be negligible compared to:

 *

   JVM startup

 *

   loading the Groovy runtime

 *

   parsing the script

 *

   applying AST transformations

 *

   bytecode generation

 *

   class loading

Recent and ongoing JVM work, such as CDS and Project Leyden, can reduce JVM startup and class-loading overhead. However, raw Groovy script execution still pays the cost of compiling the script source on every invocation.

Groovy already caches compiled script classes during the lifetime of a |GroovyClassLoader|, but this cache is lost when the process exits. A persistent launcher-level cache would allow repeated invocations of unchanged scripts to skip most compilation work.

This would make Groovy more attractive for command-line scripting and developer tooling, especially compared with languages and runtimes that already persist bytecode or compilation artifacts between runs.


   Goals

The goals of this proposal are:

1.

   Reduce repeated startup overhead for unchanged Groovy scripts.

2.

   Avoid changing Groovy language semantics.

3.

   Make the feature safe and conservative by default.

4.

   Provide explicit ways to disable, clear, and inspect the cache.

5.

   Ensure cache invalidation accounts for relevant compilation inputs.

6.

   Allow future integration with JVM startup technologies such as CDS
   or Leyden-style caches.


   Non-Goals

This proposal does not aim to:

1.

   Replace |groovyc|.

2.

   Change Groovy compilation semantics.

3.

   Cache arbitrary runtime results.

4.

   Guarantee improved performance for all scripts.

5.

   Provide a distributed or shared build cache.

6.

   Cache scripts run inside long-lived embedded Groovy runtimes unless
   those runtimes explicitly opt in.

7.

   Solve dependency resolution caching for |@Grab|, although it must
   interact safely with it.


   Proposed Behavior

When running:

|groovy myscript.groovy |

the launcher may:

1.

   Compute a cache key from the script and compilation environment.

2.

   Look for previously generated class files matching that key.

3.

   If found and valid, load the cached classes.

4.

   Otherwise compile the script normally and store the generated
   classes in the cache.

The user-visible behavior of the script must remain the same as if the script had been compiled from source during that invocation.


   Cache Location

The default cache location should follow platform conventions.

Suggested defaults:

|Linux/Unix: $XDG_CACHE_HOME/groovy/script-cache or ~/.cache/groovy/script-cache macOS: ~/Library/Caches/Groovy/script-cache Windows: %LOCALAPPDATA%\Groovy\script-cache |

A system property or environment variable should allow overriding the location:

|groovy -Dgroovy.script.cache.dir=/path/to/cache myscript.groovy |

Possible environment variable:

|GROOVY_SCRIPT_CACHE_DIR=/path/to/cache |


   Enabling and Disabling

The cache should initially be opt-in unless the Groovy project decides the invalidation model is sufficiently conservative for default use.

Possible command-line options:

|groovy --script-cache myscript.groovy groovy --no-script-cache myscript.groovy groovy --clear-script-cache |

Possible system properties:

|-Dgroovy.script.cache=true -Dgroovy.script.cache=false -Dgroovy.script.cache.dir=/path/to/cache |

If the feature later proves safe and reliable, it could become enabled by default for normal file-based scripts.


   Cache Key

The cache key must include enough information to avoid reusing stale or incompatible bytecode.

At minimum, the key should include:

 *

   absolute or canonical script path

 *

   script source hash

 *

   Groovy version

 *

   Java version or class file target version

 *

   effective classpath

 *

   compiler configuration

 *

   invokedynamic setting

 *

   preview/incubating compiler flags where relevant

 *

   script base class

 *

   active AST transformations

 *

   relevant system properties that affect compilation

For scripts using |@Grab|, the resolved dependency coordinates and artifact versions should be included after dependency resolution.

A conservative implementation may choose to skip caching when the compilation environment cannot be reliably fingerprinted.


   Cache Contents

The cache should store:

 *

   generated |.class| files

 *

   metadata describing the compilation environment

 *

   cache format version

 *

   source hash

 *

   Groovy version

 *

   Java/classfile target

 *

   classpath fingerprint

 *

   timestamp of creation

 *

   optional diagnostic information

The cache format should be treated as internal and may change between Groovy versions.


   Invalidation

A cached script must be invalidated when any relevant compilation input changes.

Examples:

 *

   script source changed

 *

   Groovy version changed

 *

   Java target changed

 *

   classpath changed

 *

   AST transform implementation changed

 *

   compiler configuration changed

 *

   |@Grab| dependencies changed

 *

   cache format changed

If validation fails or metadata is unreadable, the launcher should silently fall back to normal compilation unless diagnostics are enabled.


   Diagnostics

The launcher should provide optional diagnostics.

Examples:

|groovy --script-cache-info myscript.groovy groovy --script-cache-verbose myscript.groovy |

Possible output:

|Groovy script cache: miss Reason: source hash changed |

or:

|Groovy script cache: hit Cache entry: ~/.cache/groovy/script-cache/... |

Diagnostics should be disabled by default to preserve normal script output.


   Security Considerations

The cache stores executable bytecode. Therefore:

1.

   Cache entries should be private to the current user by default.

2.

   The cache directory should not be world-writable.

3.

   The launcher should avoid loading cache entries with unsafe permissions.

4.

   Cache keys should prevent cross-user or cross-project collisions.

5.

   The cache should not weaken existing script security assumptions.

On systems where permissions cannot be verified reliably, the launcher may disable caching or use a more conservative mode.


   Concurrency

Multiple processes may execute the same script concurrently.

The implementation should use atomic writes, temporary files, and safe renames to avoid corrupted cache entries.

If a cache entry is being written by another process, the launcher may either wait briefly, ignore the incomplete entry, or compile normally.


   Interaction with Existing Groovy Facilities


     GroovyClassLoader

The existing in-memory class cache remains useful within a single JVM process. The proposed persistent cache complements it by surviving across process invocations.


     groovyc

This proposal does not replace |groovyc|. Users who want explicit ahead-of-time compilation can continue using |groovyc|.

The script cache is intended for the common case where users execute source scripts directly with the |groovy| command.


     @Grab

Scripts using |@Grab| may be cached only if the resolved dependency set can be included in the cache key.

A first implementation may conservatively disable persistent script caching for scripts using |@Grab|.


     AST Transformations

AST transformations affect generated bytecode and must be part of the compilation fingerprint. If this cannot be done reliably, caching should be disabled for affected scripts.


   Possible Implementation Approach

One possible implementation is:

1.

   Extend the |groovy| launcher with a cache-aware script runner.

2.

   Before compilation, compute a |ScriptCacheKey|.

3.

   Look for a matching cache entry.

4.

   If present, load generated classes using an appropriate class loader.

5.

   If absent, compile the script as today.

6.

   Capture generated bytecode.

7.

   Persist generated bytecode and metadata atomically.

8.

   On future runs, validate metadata before loading.

The implementation should be internal and not expose cache internals as stable public API in the first version.


   Testing

Tests should cover:

 *

   cache miss on first execution

 *

   cache hit on second execution

 *

   invalidation when script source changes

 *

   invalidation when classpath changes

 *

   invalidation when Groovy version or cache format changes

 *

   disabling the cache

 *

   clearing the cache

 *

   concurrent execution

 *

   scripts with imports

 *

   scripts with local classes

 *

   scripts using AST transformations

 *

   scripts using different compiler configurations

 *

   failure fallback to normal compilation

 *

   cache directory permission checks where supported

Performance tests should measure:

 *

   trivial script

 *

   script with imports

 *

   script with AST transforms

 *

   script with larger source file

 *

   script with dependency-heavy classpath

 *

   script using |@CompileStatic|

 *

   script using dynamic Groovy features


   Backward Compatibility

This proposal should be backward compatible.

If disabled, behavior is unchanged.

If enabled, the observable behavior of a script should be equivalent to normal source compilation. If the cache cannot guarantee this, it should not be used.


   Risks

The main risks are:

1.

   Incorrect cache invalidation.

2.

   Security issues from loading cached bytecode.

3.

   Increased launcher complexity.

4.

   Hard-to-debug behavior if cached bytecode differs from source
   compilation.

5.

   Limited benefit for scripts where runtime dominates startup.

These risks can be mitigated by making the feature initially opt-in, using conservative invalidation, providing diagnostics, and falling back to normal compilation whenever uncertainty exists.


   Alternatives Considered


     Use groovyc

Users can already precompile scripts with |groovyc|. However, this changes the workflow and removes the convenience of directly running |.groovy| source files.


     Rely Only on JVM Startup Improvements

JVM-level startup improvements help Groovy, but they do not remove Groovy-specific parsing, AST transformation, and bytecode generation costs.


     Keep Only In-Memory Caching

Groovy already benefits from in-memory class caching in long-lived processes. This does not help repeated short-lived invocations of the |groovy| command.


     External Wrapper Tool

An external script runner could implement persistent caching, but launcher-level support would be more discoverable, portable, and consistent.


   Future Work

Future extensions could include:

 *

   enabling the cache by default

 *

   cache statistics

 *

   integration with CDS or Leyden-style JVM caches

 *

   shared cache support for trusted environments

 *

   Gradle/Maven integration

 *

   reusable public APIs for embedders

 *

   support for caching generated stubs where applicable

 *

   smarter dependency fingerprinting for |@Grab|


   Conclusion

A persistent script compilation cache would address a long-standing pain point for Groovy as a scripting language: repeated startup cost for short-lived scripts.

By caching generated class files across invocations, the |groovy| launcher could avoid unnecessary repeated parsing, AST transformation, and bytecode generation when scripts are unchanged.

Implemented conservatively, this feature would preserve Groovy semantics while making Groovy scripts feel significantly faster in day-to-day command-line use.

Reply via email to