I think every JSF developer runs up against this eventually. The true problem lies in the fact that you can't devise a truly reliable stateful system in which the browser will signal back to the ViewScoped bean that it is done with the page, allowing the backing bean to destroy itself. This is why JSF implementations have LRU caches to limit the memory used by a session, which is a great catch-all solution for everyday apps.
There are a few cases in which you know that you are done with the ViewScoped bean, such as a redirect from that bean. For these, you could write your own view handler to perform a smarter caching system, but that's not a trivial task, and frankly, not worth the effort.
The simplest solution I came up with is to use a javascript timer to execute an ajax postback to the server on every page with a ViewScoped bean. (Setting this timer to execute every 30 seconds seems reasonable.) This will move the ViewScoped bean(s) associated with the page to the bottom of the LRU cache, ensuring they aren't expired.
In particular, I use a primefaces poll component to pull this off and stick in a template to be used by all ViewScoped beans. By placing this component in its own form, the request size remains small.