I hope this is the right approach!
What I've done is added some (hacky?) plumbing so that for Wasm we export the start/end of .init_array and .fini_array using the well-known __{init,fini}_array_{start,end} symbols.
(Aside: the way this is implemented on desktop (Linux) platforms is that the driver which invokes LLD adds some special files "crtbegin.o" and "crtend.o" to the link line. Those are shipped as part of the platform, and register dummy functions with the magic names __{init,fini}_array_{start,end}, and in turn libc has a weak dependency on those symbols so that it can traverse the array of symbols.)
I had a go with the same approach as on desktop Linux - but it didn't work quite as simply as I'd like, plus there's the nuisance of actually shipping and maintaining the crtbegin/end.o files...
The approach I've gone for instead is to explicitly fudge in those symbols in LLD, and export their addresses manually.
It all works - which is perhaps the only redeeming virtue of this sort of special-casing.
Note this approach should be portable to other libc implementations (not just Musl), since the symbols __init_array_start etc are mandated by the ABI (Musl doesn't create/define those, it relies on GCC to supply them). So, creating those symbols ourselves in LLD is OK, we can add a note to the C ABI document on GitHub to say that crt1.o and __init_array_start from Musl's point of view will work exactly as on desktop linux.
Testing
I've tested with Musl, and the tests in lld/tests/wasm showcase a basic C++ file that uses a global static constructor and a C function annotated with __attribute__((constructor)). The Wasm file at the end does indeed run nicely in the browser, calling each global constructor exactly once.