Skip to content
MA MagicAjax.NET /* dev archive */
Open menu
Legacy & Modernization ·

ViewState — What It Actually Was, Why It Broke Things, and What Modern Frameworks Quietly Learned From It

Written by Alex

ViewState — What It Actually Was, Why It Broke Things, and What Modern Frameworks Quietly Learned From It

If you started writing web applications after about 2014, there is a reasonable chance that the first time you opened the DevTools on a WebForms application, you saw a hidden field called __VIEWSTATE containing what looked like several megabytes of base64-encoded gibberish — and decided, on the spot, that you wanted nothing to do with whatever generation of frameworks had produced that. The reaction is honest. It is also incomplete.

ViewState was the most important architectural decision in ASP.NET WebForms, and the single most misunderstood. Two decades after it shaped how a generation of developers thought about web UI, it is worth a careful re-reading — partly because a significant amount of working software still runs on top of it, partly because the problem it was solving has not actually gone away, and partly because the modern frameworks that replaced it are, in some specific places, less clever than they look.

What ViewState actually was

Mechanically, ViewState was straightforward. On every WebForms page, ASP.NET serialized the state of the page's server controls — properties that had been set programmatically, the contents of bound controls, the configuration that the framework needed to round-trip across a request to keep the controls behaving consistently — into a __VIEWSTATE hidden field. The field was base64-encoded, optionally encrypted, and re-posted to the server on the next request, where the framework deserialized it and used it to reconstitute the page's control tree before running any of the developer's event handlers.

Conceptually it was answering a question that the underlying protocol made awkward to answer. HTTP is stateless. A button click on a rendered page produces a POST containing only the values of the form fields the browser knows about — not the server-side configuration of the controls that produced those fields, not the state of a GridView's sort column, not whether a Panel had been hidden by a previous event handler, not the contents of a dynamically-built Repeater whose items came from a database call that already executed. WebForms wanted to give developers a programming model in which a Page object behaved consistently across requests — as if it were a long-lived in-memory object — and ViewState was the mechanism that made the illusion work. Save the state on the way out, restore it on the way in, and the event handler in Button_Click can look at the controls as if no postback ever happened.

This is not a small thing to have built. WebForms predated jQuery by several years, predated React by close to a decade, and was being designed at a time when most server-side web frameworks demanded that developers manually reconstruct page state on every request from Request.Form values. The abstraction was, in its moment, genuinely powerful. The problem was the cost of the abstraction, and the way that cost was distributed.

Why it broke things

The trouble with ViewState was not the idea. It was the defaults, the failure modes, and the way the framework's friendliness to developers shielded them from the consequences of their own decisions until those consequences arrived all at once, usually in production.

The most visible pathology was bloat. Every server control on a page contributed to ViewState by default, and large data-bound controls — GridView, DataGrid, Repeater over hundreds of rows — could trivially produce ViewState payloads of one, two, five megabytes. That payload was serialized into the page on the way out and deserialized on the way back. A page that should have been 50 kilobytes on the wire became 2 megabytes. A render that should have been instantaneous became measurable. A high-traffic page with bad ViewState hygiene could put a load on a web tier that was almost entirely a function of base64 encoding and decoding two megabytes of hidden state on every request — a bottleneck that, when you finally tracked it down with a packet capture, did not look like a code problem because the code was not doing anything visibly wrong.

The opacity made it worse. ViewState was binary-equivalent to the developer. You could not casually open a page and see what was in it; you needed a ViewState decoder, a willingness to deserialize the payload, and some confidence in what you were looking for. The framework offered EnableViewState=false as an escape hatch, but turning it off on a control could silently break behaviour that developers depended on without realising — paging, sorting, dynamic events, the lot. The combination of opacity and load-bearing function meant that, for many teams, ViewState was the thing nobody touched.

Here is roughly what a careless bloat scenario looked like in code, simplified:

csharp

public partial class Reports : Page
{
    // Anti-pattern: binding a large dataset on every page load,
    // ViewState enabled by default, ridiculous payload result.
    protected void Page_Load(object sender, EventArgs e)
    {
        if (!IsPostBack)
        {
            // 5,000-row table bound to a GridView with ViewState on.
            // Every row's cell contents, formatting and bound values get
            // serialised into __VIEWSTATE on the way out.
            gridReports.DataSource = _reportingService.GetAllRows();
            gridReports.DataBind();
        }
        // Click a column header to sort? Postback. 2 MB up, 2 MB down.
    }
}

The fix was not arcane — disable ViewState on the grid, re-bind on each postback, use Session or Cache for the small subset of state that genuinely needed to round-trip — but the fact that the framework's defaults made the broken version the easiest thing to write is what tells you the most about why ViewState got the reputation it did.

A few related sins compounded it. EnableViewStateMac was sometimes turned off "for performance," exposing the application to ViewState tampering. ViewState was being used as a general-purpose key-value store for application data because it was conveniently typed and survived postbacks, blurring the line between control state and domain state and making it nearly impossible to reason about what was being persisted where. And the ViewStateMode property, introduced in .NET 4 to give developers finer-grained control, arrived after a decade of habits that were not going to change.

What modern frameworks quietly learned

The frameworks that replaced WebForms learned three specific things from the ViewState experience, even when they were not naming the ancestor.

The first lesson was about where state should live. ViewState put UI state on the wire, in a hidden field, round-tripped through the client. Modern component frameworks keep UI state in memory on whichever side it logically belongs — server-side in Blazor Server, client-side in React, with explicit synchronization mechanisms when state needs to cross the boundary. The point is not which side is "better"; it is that the decision is now explicit. ViewState's failure mode was that it made the decision implicit and difficult to override.

The second lesson was about what state to persist. ViewState defaulted to persisting everything; the developer had to opt out. Modern frameworks default to persisting nothing; the developer has to opt in. That single inversion of defaults eliminates an entire class of bloat problems before they have a chance to grow. React's useState, Blazor's component fields, Vue's ref — all of them are opt-in, scoped, and visible. There is no equivalent of the giant __VIEWSTATE blob, because the framework does not give you one for free.

The third lesson, and the one most often missed, was about hypermedia. The frameworks that have pushed back hardest against client-side state management — HTMX most prominently — have done so by arguing that for many applications, the right level of abstraction is HTML over the wire, with the server holding canonical state. That is, in many respects, the lesson WebForms would have taught if its implementation had not buried it under serialization overhead. The hypermedia pattern was not wrong. The cost structure was.

If you still maintain WebForms

A surprising number of production systems still run on WebForms, and the goal for the teams maintaining them is rarely a green-field rewrite. The practical wisdom that survived the era is worth preserving.

Disable ViewState selectively rather than globally. Use the ViewStateMode property at the control level to keep it off for everything you do not need it for; turn it on explicitly where the framework's lifecycle depends on it. Never put application data in ViewState — use Session, Cache, or an actual data store. For grids and repeaters, prefer re-binding on every postback over persisting their contents; the database round-trip is usually cheaper than the serialization cost. Compress ViewState if you cannot reduce it. Encrypt it. And, when migrating, treat each page as an opportunity to ask whether the control was using ViewState because it needed to, or because nobody ever turned it off.

ViewState was both a brilliant abstraction and a long-running cautionary tale. The thing it tried to solve — how to maintain UI state across stateless requests, in a developer-friendly programming model — has not stopped being a hard problem. The frameworks that came after did not eliminate it; they redistributed it, and the parts of the distribution that they got right are the parts that learned, explicitly or implicitly, from what the ViewState years made painful. A modern developer who has only ever seen the new patterns is missing the part of the lineage that explains why those patterns took the shape they did. Reading ViewState carefully, even now, is one of the more useful things a .NET developer can do for their architectural intuition.

A

// author

Alex

More by author →