In vblank, you have 2270 cycles. To make the best use of them, you'll need to
buffer the changes to sprites and backgrounds and push them to the PPU
before you start calculating the changes for the next frame. There are a few video memory buffer libraries floating around; I wrote one called
Popslide.
As for how NMI relates to vblank, there are 3 different ways to divide the tasks associated with every frame: everything in NMI, everything in main, or calculate updates in main and apply them in NMI. Each approach has its pros and cons.
Everything in mainNMI:
1. Increment a variable
Main:
1. Read the controller
2. Move the game objects
3. Calculate new sprite display list and background and palette updates
4. Wait for NMI handler to increment the variable
5. If a new sprite display list is ready, push it to OAM
6. If background and palette updates are ready, push them to video memory
7. Run music as many times as the variable was incremented
Everything in NMINMI:
1. If a new sprite display list is ready, push it to OAM
2. If background and palette updates are ready, push them to video memory
3. Run music
4. If in a lag frame, return
5. Read the controller
6. Move the game objects
7. Calculate new sprite display list and background and palette updates
Main:
1. Goto 1
Split responsibilityNMI:
1. If a new sprite display list is ready, push it to OAM
2. If background and palette updates are ready, push them to video memory
3. Run music (bank switching for this can prove tricky)
4. Increment a variable
Main:
1. Read the controller
2. Move the game objects
3. Calculate new sprite display list and background and palette updates
4. Wait for NMI handler to increment the variable
See also
tokumaru's explanation