Clojure and the JVM are so dynamic its hard to infer what namespaces/vars/classes might be needed during runtime. That makes static analysis like tree-shaking difficult. Whose to say some strings are concatenated together at runtime and used to load a namespace that might have been tree-shaken out? The only way to really know is to run the program.
IMHO, it doesn't need to be overly complicated, but I might be wrong. The optimizer doesn't have to optimize all the Clojure code out there (for the start). Still, it could adopt a Graalvm-like approach, explicitly stating which elements the optimizer can optimize and encouraging developers to write their code accordingly. Alternatively, it can go with CL "declare"-like construct [1], allowing developers to explicitly insert hints for the optimizer.
[1] https://www.lispworks.com/documentation/HyperSpec/Body/s_dec...
Interesting thought, I wonder if there's a way to reason about the magnitude of effect this would have.
IMHO, while optimizations in the JVM are always welcome, they primarily address surface-level issues and don't tackle Clojure's core limitation: the lack of a proper tree shaker that understands Clojure semantics. Graalvm offers help here by doing whole-program optimization at the bytecode level, but a Clojure-specific tree shaker could take things further: it could eliminate unused vars before/during Clojure AOT, thereby reducing both program size and startup time. These improvements would happen before the JVM optimizations kick in, making everything that follows a nice extra bonus.