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.