The short answer: it depends.
Specifically, it depends on the kinds of operations that you are using in your reducers. If you find yourself frequently creating / modifying complex subtrees of your app's sate tree, you might want to look at libraries that efficiently handle immutability (see Immutable.js).
If you profile your application and find that you are needlessly modifying some parts of your sate's subtrees, you could take a look at redux-ignore, which allows you to ignore certain actions based on predicate functions.
With that said, most applications don't need either of the two libraries above as you will most likely only be modifying a small section of your state's tree as a response to an action. If you've split your root reducer into smaller reducers that handle specific parts of your tree (technique explained here), then you are most likely creating a few objects for every action, regardless of how many total objects exist in your state tree.
While it's true that your root reducer is returning a new object for every state change, you are not required to create a deep copy of your previous state for every state change. Most of the time, the object returned by your root reducer will reuse a majority of the objects that were in your previous state.
in your example of having a huge object containing large strings like so:
let tree = {
student1: ..., // huge string
student2: ... // enormous string
... // more huge strings
};
let newTree = Object.assign({}, tree);
Our newtree is actually created extremely quickly, as the strings in the object are passed by reference, so Object.assign's job is very easy: it just has to copy the existing references to the student strings into a new object that will serve as our application's updated state tree.
The same conclusion is true if our student properties were pointing to huge objects. Those objects would not be recreated when we create a new tree. Instead, the object references will be copied over to the new state tree.
In summary, as long as you are not making deep copies of your state tree for each action, your new state tree will reuse most of your old tree's properties, making the state update performant.