Separate state changes from drawing
Use requestAnimationFrame for the next visual update, then pass its timestamp into a small loop coordinator. Update positions, timers, and rules from elapsed time; draw the current state after updates. The renderer should not decide whether a collision happened, and a collision check should not depend on whether a sprite happened to be painted.
For a small platform test, move a marker horizontally at a chosen number of world units per second. Run the same route on a 60 Hz display and a high-refresh display. If the marker travels farther in the same wall-clock interval on one screen, some movement is still being applied per callback instead of per unit of time.
Choose a recovery policy for late frames
A long pause can produce an enormous elapsed interval. Clamp the elapsed value for a casual real-time game so a returning player does not see a character teleport through several obstacles. For deterministic simulations, use a fixed update step with an accumulator, run a bounded number of catch-up updates, and render between simulation states if interpolation is useful.
Record the longest callback gap during a repeatable obstacle course, then inspect the game after switching away and back. Decide whether the game should pause, catch up a little, or resume from the last state; make that choice explicit instead of letting a single delayed frame determine it.
Questions about browser game loop
Should I use setInterval for drawing?
Use requestAnimationFrame for visual work because it is scheduled near repaint and is commonly paused for hidden documents. A timer can still schedule nonvisual tasks, but it should not be treated as a display clock.
Where does the frame timestamp come from?
The requestAnimationFrame callback supplies a high-resolution timestamp. MDN documents its timing and one-shot behavior; use that timestamp to calculate progress rather than counting callbacks.