A salt must be unique not only for your service, but for all services.
So you must introduce a service-salt too.
$$\text{salt} = H(\text{service-identifier} \| \text{username})$$
This is an absolute minimum. To defend against multiple server compromises, you may want to add a counter, which may be randomized when the user registers and incremented when they change the password.
Now we have:
$$\text{salt} = H(\text{service-identifier} \| \text{counter} \| \text{username})$$
I recommend the first case when you derive keys in the browser using Argon2i from the user's user + passphrase. When registering you may generate a suggested default password using 6 words from EFF' wordlist. The server can use the second salt with a quick $\text{HMAC}(\text{salt}_2, \text{argon2i_output})$ when storing the verification string.
You should only send passwords to the server when the user's browser does not support JavaScript (or if it is disabled), including a disclaimer that the form will send the password to the server and if the user does not want this, they should enable JavaScript.
Rogue JavaScript can obtain the user' credentials with or without this defence.
Additionally Argon2i enables the user to spend the computational cost when registering and logging in without imposing slow-hashing load on the server.
Edit: Q. Necessity of Randomness of Salts? includes more information and one note worth a mention here is that if the salts can be publicly computed, then an adversary can precompute many guesses before compromising the server. Whereas with the random salt, they cannot. This is safe with the hybrid solution I described where the user uses $\text{salt}_1$ and the server uses $\text{salt}_2$ with a randomized $\text{counter}$.