A. Marking blocks deleted internally instead of passing deletion down to the FS level (to prevent the overhead of zeroing blocks).
B. Copying data on write (to allow recovery of crash during a write).
C. Having overhead for metadata.
D. Allowing blocks to be part empty (but the whole block is still used as far as the device level knows).
etc.
So while the filesystem may know that certain blocks are free for re-use, this is never passed down to the device level so those blocks are still included in those snapshots.
You can try setting up discard (aka TRIM) which may help this as it makes linux pass more information from the filesystem level down to the device level. There is some info on this at
https://cloud.google.com/compute/docs/disks/performance#optimizing_persistent_disks. Note that enabling discard won't immediately make
snapshot size go down, it would only do so for future freed blocks. This also is a best practice for taking
snapshots.