I think the main design decision for this behavior is, because the gradient calculation and the update steps are independent from each other.
E.g. you can easily accumulate gradients with multiple forward/backward passes, if a larger batch doesn’t fit in your memory.
Also, you might work with multiple losses, which will use their own .backward() call.
Forcing the optimizer.step() method to kick in right after the first gradient was calculated seems like a huge limiting factor.