15 Best Practices zur Optimierung von Skripten

15.1 Performance-Optimierung durch Configuration Avoidance

Configuration Avoidance ist die effektivste Einzelmaßnahme zur Build-Performance-Verbesserung. Jede unnötige Task-Konfiguration kostet Zeit, selbst wenn der Task nicht ausgeführt wird. In einem Projekt mit 500 Tasks kann die Konfiguration aller Tasks mehrere Sekunden dauern, obwohl typische Builds nur 20-30 Tasks ausführen. Die konsequente Anwendung von Configuration Avoidance reduziert diese Overhead dramatisch.

Die Verwendung von register statt create ist der erste Schritt:

// Suboptimal - Task wird sofort konfiguriert
val jarTask = tasks.create<Jar>("customJar") {
    from(sourceSets.main.get().output)
    archiveClassifier.set("custom")
}

// Optimal - Task wird nur bei Bedarf konfiguriert
val jarTask = tasks.register<Jar>("customJar") {
    from(sourceSets.main.get().output)
    archiveClassifier.set("custom")
}

Die configureEach Methode vermeidet Realisierung aller Tasks einer Type:

// Suboptimal - Realisiert alle Test-Tasks
tasks.withType<Test>().forEach { task ->
    task.maxHeapSize = "2g"
}

// Optimal - Konfiguriert lazy nur ausgeführte Test-Tasks
tasks.withType<Test>().configureEach {
    maxHeapSize = "2g"
}

Task-Referenzen sollten über Provider erfolgen, nicht über direkte Zugriffe:

// Suboptimal - Realisiert compileJava sofort
val compileTask = tasks.getByName("compileJava") as JavaCompile
val outputDir = compileTask.destinationDirectory

// Optimal - compileJava bleibt unrealisiert
val compileTask = tasks.named<JavaCompile>("compileJava")
val outputDir = compileTask.flatMap { it.destinationDirectory }

Die Messung des Configuration Avoidance Erfolgs erfolgt über Build Scans. Der “Configuration on demand” Modus kann zusätzliche Einsparungen bringen, ist aber mit Vorsicht zu verwenden, da er die Semantik von Multi-Projekt-Builds ändert.

15.2 Script-Kompilierung und Build-Cache-Nutzung

Gradle kompiliert Build-Skripte zu JVM-Bytecode. Diese Kompilierung kostet Zeit, besonders für Kotlin-DSL-Skripte. Der Script-Compile-Cache speichert kompilierte Skripte und vermeidet Rekompilierung bei unveränderten Inputs. Die effiziente Nutzung dieses Caches verbessert Build-Startup-Performance erheblich.

Script-Änderungen invalidieren den Cache. Dynamische Versionen oder häufig ändernde Properties in Build-Skripten führen zu ständiger Rekompilierung:

// Suboptimal - Ändert sich täglich, invalidiert Cache
val buildTime = SimpleDateFormat("yyyy-MM-dd").format(Date())

tasks.register("printInfo") {
    doLast {
        println("Built on: $buildTime")
    }
}

// Optimal - Build-Zeit wird zur Execution-Zeit berechnet
tasks.register("printInfo") {
    doLast {
        val buildTime = SimpleDateFormat("yyyy-MM-dd").format(Date())
        println("Built on: $buildTime")
    }
}

Build Cache Konfiguration maximiert Wiederverwendung:

// settings.gradle.kts
buildCache {
    local {
        isEnabled = true
        removeUnusedEntriesAfterDays = 14
    }
    remote<HttpBuildCache> {
        url = uri("https://cache.company.com/cache/")
        credentials {
            username = System.getenv("CACHE_USER")
            password = System.getenv("CACHE_PASSWORD")
        }
        isEnabled = System.getenv("CI") != null
        isPush = System.getenv("CI_PUSH") == "true"
    }
}

Task-Output-Normalization verbessert Cache-Hit-Rate:

tasks.withType<JavaCompile>().configureEach {
    options.compilerArgs.addAll(listOf(
        "-Xlint:all",
        "-Werror"
    ))
    // Reproducible Builds für besseres Caching
    options.isFork = true
    options.forkOptions.jvmArgs = listOf("-Xmx2g", "-XX:+UseG1GC")
}

15.3 BuildSrc und Convention Plugins

Die Organisation von Build-Logik in buildSrc oder Convention Plugins verbessert Wartbarkeit und Performance. Wiederverwendbare Logik in Build-Skripten führt zu Duplikation und inkonsistenter Anwendung. BuildSrc kompiliert einmal und wird von allen Projekt-Skripten verwendet.

BuildSrc-Struktur für geteilte Logik:

// buildSrc/src/main/kotlin/ProjectExtensions.kt
import org.gradle.api.Project
import org.gradle.kotlin.dsl.*

fun Project.configureJavaConventions() {
    apply(plugin = "java")
    
    configure<JavaPluginExtension> {
        toolchain {
            languageVersion.set(JavaLanguageVersion.of(17))
        }
    }
    
    tasks.withType<JavaCompile>().configureEach {
        options.encoding = "UTF-8"
        options.compilerArgs.add("-parameters")
    }
    
    tasks.withType<Test>().configureEach {
        useJUnitPlatform()
        maxHeapSize = "1g"
    }
}

Convention Plugins kapseln Standard-Konfigurationen:

// buildSrc/src/main/kotlin/company-java-conventions.gradle.kts
plugins {
    java
    jacoco
    id("com.github.spotbugs")
}

java {
    toolchain {
        languageVersion.set(JavaLanguageVersion.of(17))
    }
}

testing {
    suites {
        val test by getting(JvmTestSuite::class) {
            useJUnitJupiter("5.10.0")
        }
    }
}

tasks.jacocoTestReport {
    dependsOn(tasks.test)
    reports {
        xml.required.set(true)
        html.required.set(true)
    }
}

spotbugs {
    effort.set(com.github.spotbugs.snom.Effort.MAX)
    reportLevel.set(com.github.spotbugs.snom.Confidence.LOW)
}

Die Verwendung in Projekten wird minimal:

// build.gradle.kts
plugins {
    id("company-java-conventions")
}

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-web")
}

15.4 Parallelisierung und Task-Avoidance

Parallele Task-Ausführung nutzt moderne Multi-Core-Prozessoren optimal. Gradle kann unabhängige Tasks parallel ausführen, wenn dies explizit aktiviert ist. Die korrekte Deklaration von Task-Dependencies ist Voraussetzung für sichere Parallelisierung.

Parallelisierung aktivieren und konfigurieren:

# gradle.properties
org.gradle.parallel=true
org.gradle.workers.max=8
org.gradle.priority=low

Task-Dependencies präzise definieren für optimale Parallelität:

// Suboptimal - Unnötige Serialisierung
tasks.register("process1") {
    dependsOn("compile")
    doLast { processType1() }
}

tasks.register("process2") {
    dependsOn("compile", "process1")  // Unnötige Abhängigkeit zu process1
    doLast { processType2() }
}

// Optimal - Maximale Parallelität
val compileOutput = tasks.named("compile").map { it.outputs.files }

tasks.register("process1") {
    inputs.files(compileOutput)
    doLast { processType1() }
}

tasks.register("process2") {
    inputs.files(compileOutput)
    doLast { processType2() }
}

Task-Outputs als Inputs verwenden statt explizite Dependencies:

val generateTask = tasks.register<GenerateTask>("generate") {
    outputDirectory.set(layout.buildDirectory.dir("generated"))
}

tasks.register<ProcessTask>("process") {
    // Automatische Dependency durch Output-Input-Verbindung
    inputDirectory.set(generateTask.flatMap { it.outputDirectory })
}

Worker API für CPU-intensive Operationen:

abstract class ParallelTask : DefaultTask() {
    @get:Inject
    abstract val workerExecutor: WorkerExecutor
    
    @TaskAction
    fun execute() {
        val workQueue = workerExecutor.classLoaderIsolation {
            classpath.from(configurations.getByName("workerClasspath"))
        }
        
        inputFiles.forEach { file ->
            workQueue.submit(WorkAction::class) {
                inputFile.set(file)
                parameters.processingMode.set("parallel")
            }
        }
        
        // Warten auf Completion aller Worker
        workQueue.await()
    }
}

15.5 Dependency Resolution Optimierung

Dependency Resolution ist oft ein Performance-Bottleneck. Repositories werden sequenziell abgefragt, transitive Dependencies aufgelöst und Versionen konfliktbereinigt. Optimierungen in diesem Bereich wirken sich auf jeden Build aus, der Dependencies resolved.

Repository-Reihenfolge und Content-Filtering:

repositories {
    // Schneller interner Proxy zuerst
    maven {
        url = uri("https://repo.company.com/maven")
        content {
            includeGroupByRegex("com\\.company\\..*")
        }
    }
    
    // Spezifische Repositories für bekannte Gruppen
    maven {
        url = uri("https://repo.spring.io/release")
        content {
            includeGroup("org.springframework")
        }
    }
    
    // Fallback zu Maven Central
    mavenCentral {
        content {
            excludeGroupByRegex("com\\.company\\..*")
        }
    }
}

Resolution Strategy für schnellere Konfliktauflösung:

configurations.all {
    resolutionStrategy {
        // Cache dynamische Versionen länger
        cacheDynamicVersionsFor(24, TimeUnit.HOURS)
        cacheChangingModulesFor(4, TimeUnit.HOURS)
        
        // Fail-fast bei Konflikten statt automatische Resolution
        failOnVersionConflict()
        
        // Force spezifische Versionen für bekannte Probleme
        force("com.fasterxml.jackson.core:jackson-databind:2.15.2")
        
        // Prefer Projekt-Module über externe Dependencies
        preferProjectModules()
    }
}

Dependency Locking für reproduzierbare Builds:

dependencyLocking {
    lockAllConfigurations()
}

tasks.register("updateDependencyLocks") {
    dependsOn(configurations.compileClasspath)
    dependsOn(configurations.runtimeClasspath)
    
    doLast {
        println("Dependency locks updated")
    }
}

15.6 Code-Organisation und Modularisierung

Gut organisierte Build-Skripte sind wartbar und performant. Monolithische Build-Dateien mit hunderten Zeilen sind schwer zu verstehen und langsam zu parsen. Modularisierung in logische Einheiten verbessert beide Aspekte.

Aufteilung nach Concerns:

// build.gradle.kts - Hauptkonfiguration
plugins {
    id("company-conventions")
}

apply(from = "gradle/dependencies.gradle.kts")
apply(from = "gradle/testing.gradle.kts")
apply(from = "gradle/publishing.gradle.kts")

// gradle/dependencies.gradle.kts
dependencies {
    implementation(libs.spring.boot.starter)
    implementation(libs.jackson.databind)
}

// gradle/testing.gradle.kts
testing {
    suites {
        val integrationTest by registering(JvmTestSuite::class) {
            dependencies {
                implementation(libs.testcontainers)
            }
        }
    }
}

Extension Objects für typsichere Konfiguration:

// buildSrc/src/main/kotlin/AppConfiguration.kt
open class AppConfiguration {
    var applicationName: String = "default-app"
    var deploymentTarget: String = "kubernetes"
    var monitoring: MonitoringConfig = MonitoringConfig()
}

class MonitoringConfig {
    var enabled: Boolean = true
    var endpoint: String = "http://metrics.local"
}

// Verwendung im Build-Skript
extensions.create<AppConfiguration>("app")

app {
    applicationName = "user-service"
    monitoring {
        endpoint = "https://metrics.prod"
    }
}

Property-basierte Konfiguration für Flexibilität:

val env = providers.environmentVariable("ENVIRONMENT").orElse("dev")

val config = when (env.get()) {
    "prod" -> ProductionConfig()
    "staging" -> StagingConfig()
    else -> DevelopmentConfig()
}

tasks.withType<JavaExec>().configureEach {
    systemProperties = config.systemProperties
    jvmArgs = config.jvmArgs
}

Diese Optimierungen resultieren in schnelleren, wartbareren Builds. Die initiale Investition in Skript-Optimierung amortisiert sich durch reduzierte Build-Zeiten und vereinfachte Wartung. Regelmäßiges Profiling und Monitoring stellt sicher, dass Performance-Gewinne erhalten bleiben.