Replacing app.js and hoping every cache forgets the old version is a fairly unpleasant deployment strategy. I prefer filenames that change when the file content changes. The browser can cache the old file confidently, and new HTML can point to the new file without needing the two versions to fight over one URL.
CloudFront’s invalidation guidance explains both invalidation and versioned filenames. Invalidation is useful, but it is not the whole caching story: a browser or another cache can still have its own copy. Giving changed assets a new path makes the intended version explicit.
The deployment sequence matters too. Upload the new assets before publishing HTML that references them, and keep older assets available long enough for existing pages to keep working. Otherwise a release can leave somebody with an old page asking for a file you just deleted. HTML and long-lived static assets generally need different caching decisions.
This is one of those changes that makes the operational explanation shorter. You can inspect a page and see which build it references, compare the files, and reason about rollback. A cache should make the site faster. It should not turn every release into a discussion about why one person’s browser still looks different.