Rewrite from scratch.
Step zero is to check the unit tests that test the old function. If there is none, write some. Even if you stop here, you already improved your code.
First I remove the whole piece of code I want to refactor (donʼt forget that I have “backup” in the VCS.) I do this because old (dead) code often remains in the code base during refactoring, making it much more difficult to digest (not mentioning the fact that it adds to the size of your running code, thus, may affect performance.)
After that, I rewrite it from scratch. If I wrote the original code, thereʼs a high chance I will come up with a better solution. The only thing that remains is the original method signature (ie. the number, type, and order of parameters.)
If I have to change the parameters and the language supports optional parameters, I add an optional one. Otherwise (and also if the old function is part of the public API) I restore the old function and create a new one, marking the old one deprecated (if the language/compiler supports it, I mark it as deprecated in the code, too, not just the documentation; it helps me find the locations where the old function is used.) Then I open an issue to use the new one (unless there are only a few invocations, is which case I change them to the new one immediately). This can be important as my new code may cause problems or side effects I didnʼt think of. If I can, I even use an experimenting library like Laboratory!, so I can test in production which one is better. The issue also serves as a reminder to remove the old function; again, to eliminate dead code.
The final step of dead code elimination (and the one that many forget) is the removal of other functions and global variables that were used only by the code now gone. That can significantly impact performance on the long run.