One important aspect is whether there is any sort of key material stored on the device that can be used to authenticate the device or if a user can simply take a brand new device, download the app, and log in.
If there is such key material on the device it is possible for the server to apply rate limits per device. Other ways of applying rate limits server side (such as rate limits per IP or per username) are potential DoS attack vectors. That is true regardless of whether the rate limit is implemented by having the server perform CPU heavy calculations as part of password validation or by temporary lockouts.
When you are in control of both client and server code you can design a protocol in which the CPU heavy part of validation is done client side, but the server still does a salted hash to ensure that you still have the benefits of server side validation. Obviously if you implement such a thing from scratch there is a risk of vulnerabilities introduced by flaws in design or implementation.
The advantage of doing the CPU intensive part client side is that it mostly eliminates the DoS attack vector. A drawback of doing the CPU intensive part client side is that the client may be limited in CPU resources.
Combining the above approaches is possible. On first login you could have the client do hashing for multiple seconds for example by sending a salt to the client and have it do many rounds of hashing, finally the server does the last round of hashing with a different salt. If the password is accepted the device is send a token that will allow the client to authenticate to the same account with a secondary much cheaper hash in the future. The server can enforce a limit on how many failed login attempts with the token are permitted before the token is expired and the client has go back to the slower hash.
Again I have to warn that there are plenty of ways to introduce security flaws in a design like this, so you have to way that risk against the risk of brute force attacks.