Vec<T> and Option<Vec<T>> have the same size (3 words, you can easily check that using size_of). That's because Vec has a non-null field internally that allows Option to use the null value as None.
Runtime-wise both should be similar too, both None and Vec::new() are constants.
If you really want to save memory in the empty case, you can use eg. Option<Box<Vec<T>> (1 word, but additional indirection) or Option<Box<[T]>> (2 words, with no additional indirection (Box<[T]> is like Vec, but not growable)).
Does the semantics of what you are trying to do differentiate between "empty vector" and "nonexistent vector"? If so, go for Option. If not, go for the plain Vec. There is no point in wrapping a collection into an Option just for the sake of it. It only makes code more complex, and introduces additional conditional jumps when you need to access the contents.
The core of the problem is that I incorrectly assumed that Vec::new() does heap allocation for elements. Clearly I am wrong. Is there a easy way to verify this from source?
Sure, e.g. you can see that Vec::new() is const. Furthermore, if you go to its source, you can see that the underlying RawVec buffer is initialized using a constant, RawVec::NEW. Transitively, its construction only calls Unique::empty(), which only returns a dangling non-null pointer made up out of thin air.