The server controls caching mainly through the Cache-Control header. A value like Cache-Control: max-age=31536000 tells a cache the response is fresh for one year, so it can be served straight from local storage with no network call at all. Adding public lets shared caches store it; private restricts it to the browser; no-store forbids storing it entirely.
When a cached copy expires, the cache does not just throw it away. It revalidates. The server sends an ETag, a short fingerprint of the content like a hash, and a Last-Modified timestamp. On the next request the browser sends back If-None-Match with that ETag or If-Modified-Since with the timestamp. If nothing changed, the server replies 304 Not Modified with an empty body, and the browser reuses its stored copy. A 304 round trip still costs a request but transfers almost no data, which is much cheaper than re-downloading a 300 KB bundle.
The common production pattern is content hashing in filenames, such as app.4f3a9b.js. The file is served with a one-year max-age and treated as immutable. When the code changes, the build tool generates a new hash, so the HTML points at a brand new URL and the browser fetches the new file while keeping the old one cached harmlessly.