A DELETE in PostgreSQL reclaims nothing on its own. It stamps the tuple's t_xmax and moves on: the row is still physically on the page, the indexes still point at it, and the free space map still thinks the page is full. Nothing is reclaimed until VACUUM arrives.
Run DELETE and watch the dead tuples pile up with no change to free space. Hit Page-prune to fire opportunistic cleanup: the bytes come back and the line pointers go LP_DEAD, but the slots stay pinned because the index still points at them, and pruning can't reach the index or the other pages. Then step VACUUM through its three phases: heap scan and prune, index cleanup (the part pruning couldn't touch), and heap cleanup that flips LP_DEAD to LP_UNUSED. Finish with INSERT: rows reuse the reclaimed space instead of extending the table. Click any line pointer for its full state.
Why page pruning isn't enough
Opportunistic page pruning is the shortcut: it runs during ordinary reads and writes, works entirely inside a single page, and does real work. It strips the storage off dead tuples (the bytes come back and the free space map updates) and flags their line pointers LP_DEAD. But that is where it stops. It cannot remove the index entries that still reference those TIDs, so it cannot free the LP_DEAD slots, and it cannot reach across pages.
VACUUM is what finishes the job. Phase 1 scans and prunes the heap, reclaiming the dead tuples' bytes, setting their line pointers LP_DEAD, and collecting the TIDs. Phase 2 walks every index and removes the entries pointing at them, the step pruning can never do. Only then is phase 3 safe: those LP_DEAD line pointers become LP_UNUSED and the slots are reusable. The distinction that trips people up is that the bytes were already reclaimed during the prune; what VACUUM adds is cleaning the index and releasing the slots, everywhere, not just on a page someone happened to read.