| name | performance-smell-detection |
| description | Detect potential code-level performance smells in Java - streams, collections, boxing, regex, object creation. Provides awareness, not absolutes - always measure before optimizing. For JPA/database performance, use jpa-patterns instead. |
Performance Smell Detection Skill
Identify potential code-level performance issues in Java code.
Philosophy
"Premature optimization is the root of all evil" - Donald Knuth
This skill helps you notice potential performance smells, not blindly "fix" them. Modern JVMs (Java 21/25) are highly optimized. Always:
- Measure first - Use JMH, profilers, or production metrics
- Focus on hot paths - 90% of time spent in 10% of code
- Consider readability - Clear code often matters more than micro-optimizations
When to Use
- Reviewing performance-critical code paths
- Investigating measured performance issues
- Learning about Java performance patterns
- Code review with performance awareness
Scope
This skill: Code-level performance (streams, collections, objects)
For database: Use jpa-patterns skill (N+1, lazy loading, pagination)
For architecture: Use architecture-review skill
Quick Reference: Potential Smells
| Smell | Severity | Context |
|---|
| Regex compile in loop | 🔴 High | Always worth fixing |
| String concat in loop | 🟡 Medium | Still valid in Java 21/25 |
| Stream in tight loop | 🟡 Medium | Depends on collection size |
| Boxing in hot path | 🟡 Medium | Measure first |
| Unbounded collection | 🔴 High | Memory risk |
| Missing collection capacity | 🟢 Low | Minor, measure if critical |
String Operations (Java 9+ / 21 / 25)
What Changed
Since Java 9 (JEP 280), string concatenation with + uses invokedynamic, not StringBuilder. The JVM optimizes simple concatenation well.
Java 25 adds String::hashCode constant folding for additional optimization in Map lookups with String keys.
Still Valid: StringBuilder in Loops
String result = "";
for (String s : items) {
result += s;
}
StringBuilder sb = new StringBuilder();
for (String s : items) {
sb.append(s);
}
String result = sb.toString();
String result = String.join("", items);
Now Fine: Simple Concatenation
String message = "User " + name + " logged in at " + timestamp;
return "Error: " + code + " - " + description;
Avoid in Hot Paths: String.format
log.debug(String.format("Processing %s with id %d", name, id));
log.debug("Processing {} with id {}", name, id);
Stream API (Nuanced View)
The Reality
Streams have overhead, but it's often acceptable:
- < 100 items: Streams can be 2-5x slower (but still microseconds)
- 1K-10K items: Difference narrows significantly
- > 10K items: Often within 50% of loops
- GraalVM: Can optimize streams to match loops
Recommendation: Prefer streams for readability. Optimize to loops only when profiling shows a bottleneck.
When Streams Are Problematic
for (int i = 0; i < 1_000_000; i++) {
boolean found = items.stream()
.anyMatch(item -> item.getId() == i);
}
Set<Integer> itemIds = items.stream()
.map(Item::getId)
.collect(Collectors.toSet());
for (int i = 0; i < 1_000_000; i++) {
boolean found = itemIds.contains(i);
}
When Streams Are Fine
List<String> names = users.stream()
.filter(User::isActive)
.map(User::getName)
.sorted()
.collect(Collectors.toList());
int sum = numbers.stream()
.mapToInt(Integer::intValue)
.sum();
Parallel Streams: Use Carefully
smallList.parallelStream().map(...);
List<String> results = new ArrayList<>();
items.parallelStream()
.forEach(results::add);
List<Result> results = largeDataset.parallelStream()
.map(this::expensiveCpuComputation)
.collect(Collectors.toList());
Boxing/Unboxing
Still a Real Issue
Boxing creates objects on heap, adds GC pressure. JVM caches small values (-128 to 127) but not larger ones.
Future: Project Valhalla will improve this significantly.
Long sum = 0L;
for (int i = 0; i < 1_000_000; i++) {
sum += i;
}
long sum = 0L;
for (int i = 0; i < 1_000_000; i++) {
sum += i;
}
Use Primitive Streams
int sum = list.stream()
.reduce(0, Integer::sum);
int sum = list.stream()
.mapToInt(Integer::intValue)
.sum();
Regex
Always Pre-compile in Loops
This advice is not outdated - Pattern.compile is expensive.
for (String input : inputs) {
if (input.matches("\\d{3}-\\d{4}")) {
process(input);
}
}
private static final Pattern PHONE = Pattern.compile("\\d{3}-\\d{4}");
for (String input : inputs) {
if (PHONE.matcher(input).matches()) {
process(input);
}
}
Collections
Capacity Hint (Minor Optimization)
List<User> users = new ArrayList<>(expectedSize);
Map<String, User> map = new HashMap<>(expectedSize * 4 / 3 + 1);
Right Collection for the Job
List<String> allowed = getAllowed();
for (Request r : requests) {
if (allowed.contains(r.getId())) { }
}
Set<String> allowed = new HashSet<>(getAllowed());
for (Request r : requests) {
if (allowed.contains(r.getId())) { }
}
Unbounded Collections
@GetMapping("/users")
public List<User> getAllUsers() {
return userRepository.findAll();
}
@GetMapping("/users")
public Page<User> getUsers(Pageable pageable) {
return userRepository.findAll(pageable);
}
Modern Java (21/25) Patterns
Virtual Threads for I/O (Java 21+)
ExecutorService executor = Executors.newFixedThreadPool(100);
for (Request request : requests) {
executor.submit(() -> callExternalApi(request));
}
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (Request request : requests) {
executor.submit(() -> callExternalApi(request));
}
}
Structured Concurrency (Java 21+ Preview)
try (StructuredTaskScope.ShutdownOnFailure scope = new StructuredTaskScope.ShutdownOnFailure()) {
Future<User> user = scope.fork(() -> fetchUser(id));
Future<Orders> orders = scope.fork(() -> fetchOrders(id));
scope.join();
scope.throwIfFailed();
return new UserProfile(user.resultNow(), orders.resultNow());
}
Performance Review Checklist
🔴 High Severity (Usually Worth Fixing)
🟡 Medium Severity (Measure First)
🟢 Low Severity (Nice to Have)
When NOT to Optimize
- Not a hot path - Setup code, config, admin endpoints
- No measured problem - "Looks slow" is not a measurement
- Readability suffers - Clear code > micro-optimization
- Small collections - 100 items processed in microseconds anyway
Analysis Commands
grep -rn "\.matches(\|\.split(" --include="*.java"
grep -rn "Long\s\|Integer\s\|Double\s" --include="*.java" | grep "= 0\|+="
grep -rn "new ArrayList<>()" --include="*.java"
grep -rn "findAll()" --include="*.java"