MCP

воскресенье, 22 ноября 2009 г.

Сжатие ViewState

ViewState — в ASP.NET весьма интересная и полезная технология, но у неё есть один существенный недостаток. ViewState может очень сильно увеличивать размер страницы, и кроме всего прочего все эти данные пойдут на сервер назад, что для пользователей с узким каналом может выливаться в "тормоза" приложения, хотя само-то оно будет быстрым.

И что самое неприятное, все эти проблемы активно вылезают обычно ближе к окончанию проекта, когда всё уже сделано, и менять уже логику поздно. Точнее, смена логики может привести к непредсказуемым глюкам, а если всё тестировать заново, то это займёт слишком много ресурсов, так что обычно всё так и оставляют, и на будущее все программисты твёрдо себе говорят: в следующем проекте будем следить за ViewState. Но вы поняли. 

Собственно, есть ситуация, проект близится к концу, нужно что-то с ViewState'ом делать. Для начала, стоит взять типичную страницу, забрать из неё ViewState (он хранится в hidden-поле __VIEWSTATE), убрать кодирование в base-64 (сам ViewState не должен быть зашифрован, естественно), и посмотреть на него. На данном этапе иногда можно выкинуть некоторые элементы, которые случайно попали туда. Например, в одном проекте часть рендера страницы выводилась через asp:Literal, который по умолчанию пихает всё свое содержимое во ViewState, что, например, для русского текста длинной в 100 байт превратится в 300 байт на клиенте.

Но, когда базовые вещи исключены, всё равно может остаться большой вьюстейт (например, у нас в одном проекте с хитрым гридом он был 80 килобайт), и с ним надо что-то кардинально сделать. В этом случае, можно попробовать самое простое решение: сжать вьюстейт, благо ASP.NET предоставляет достаточно простую возможность для этого.

Вначале, нам необходимо переопределить PageStatePersister, собственно, тот класс, который и отвечает за хранение. В ASP.NET существуют две реализации, которые хранят ViewState в hidden-поле (по умолчанию) или в сессии. Нам нужен свой, который будет сжимать содержимое:


protected override PageStatePersister PageStatePersister
{
    get
    {
        return _persister ?? (_persister = new CompressionPageStatePersister(this));
    }
}

Для этого перекрываем property PageStatePersister у страницы (Page), и возвращаем инстанс нашего класса:


public class CompressionPageStatePersister : PageStatePersister
{
    public CompressionPageStatePersister(Page page) : base(page) { }
    public override void Load()
    {
        string requestViewStateString = base.Page.Request.Form["__CVIEWSTATE"];
        if (!string.IsNullOrEmpty(requestViewStateString))
        {
            Pair pair = (Pair)
                ViewStateCompressor.ViewStateDecompress(
                    Convert.FromBase64String(
                        requestViewStateString));
            base.ViewState = pair.First;
            base.ControlState = pair.Second;
        }
    }

    public override void Save()
    {
        Control control = base.Page.FindControl("__CVIEWSTATE");
        if ((base.ViewState != null) || (base.ControlState != null))
        {
            string s = Convert.ToBase64String(
                ViewStateCompressor.ViewStateCompress(
                    new Pair(base.ViewState, base.ControlState)));
            ScriptManager.RegisterHiddenField(base.Page, "__CVIEWSTATE", s);
        }
    }
}

Как видно, реализация простейшая, перекрываются методы Save и Load, которые сохраняют и загружают ControlState (неотключаемая часть, необходимая для работы контролов) и ViewState. Всё это сохраняается в собственное hidden-поле __CVIEWSTATE, т.к. к обычному у нас нет доступа (но если интересно, то с использованием Reflection, нужно взять private field у класса Page с именем _clientState, и записать значение туда).

Да, и в base-64 кодировать обязательно, ибо особенности стандарта HTTP таковы, что даже если вы попробуете другую кодировку, в реальности будет передано гораздо больше байт.

Ну, и собственно про сжатие, точнее про ViewStateCompressor, это небольшой класс, который сериализует данные и сжимает с помощью GZip:


public static byte[] ViewStateCompress(object viewStateToCompress)
{
    ObjectStateFormatter formatter = new ObjectStateFormatter();
    MemoryStream output0 = new MemoryStream();
    DeflaterOutputStream ds = new DeflaterOutputStream(output0, new Deflater(6, true));
    formatter.Serialize(ds, viewStateToCompress);
    ds.Close();
    return output0.ToArray();
}

public static object ViewStateDecompress(byte[] data)
{
    ObjectStateFormatter formatter = new ObjectStateFormatter();
    InflaterInputStream ds = new InflaterInputStream(new MemoryStream(data), new Inflater(true));
    object value = formatter.Deserialize(ds);
    ds.Close();
    return value;
}

Небольшие пояснения:

  • Для сжатия используется библиотека #zipLib (родная медленнее и хуже жмёт, использовать не стоит)
  • Степень сжатия (6) — это по умолчанию, наилучший компромисс качества и скорости
  • Не пишутся заголовки GZip, так как они нам не нужны, т.е. другими словами использоуется не GZip, а Deflate — алгоритм сжатия без дополнительной информации
  • В качестве сериализатора используется ObjectStateFormatter, который и используется в качестве стандартного (точнее там используется LosFormatter, но итогом работы служит строка в base-64, а мы сами потом трансформируем уже запакованные данные
  • В данном случае мы теряем встроенную возможность шифрования ViewState, но её при необходимости легко реализовать, да и расшифровать ViewState будет уже сложнее

Как видно, всё просто: два класса для удобства и один override, и результат — размер данных на клиенте в 2-5 раз меньше, и не на магистральных каналах страницы начинают работать гораздо быстрее.

Да, и постарайтесь не сохранять во ViewState собственные объекты, ибо ObjectStateFormatter достаточно эффективно сериализует базовые типы, но если он не понял про тип, то начинает использовать недостаточно эффективный, но универсальный BinaryFormatter. Если будет время, я постараюсь расписать, как в некоторых случаях можно подменить сериализацию для своих типов, чтобы уменьшить размер ViewState (который из-за этого может тоже весьма значительно сократиться).

2 комментария:

  1. Я предпочитаю не сжимать ViewState, а переносить хренения состояния со станицы на веб-сервер.
    Код можно посмотреть здесь
    http://stud-work.ru/index.php/kak-udalit-viewstate-so-stranitsy

    ОтветитьУдалить
    Ответы
    1. В принципе тот ещё костыль, особенно в том плане, что ViewState сделали как раз для того, чтобы на сервере не хранить эти данные, а мы их опять на сервер.
      Хотя теперь, с наличием MVC и общего ускорения интернета, проблема уже менее актуальна.

      Удалить