Игорь Чакрыгин Игорь Чакрыгин

У любой задачи существует по крайней мере одно очевидное и невероятно простое для понимания неправильное решение

ASP.NET - Минимизация html-разметки на этапе компиляции

Если открыть html-разметку большинства сайтов, то можно обнаружить, что она содержит значительное количество ненужных символов, таких как пробелы, переносы строк, а иногда даже html-комментарии. Все эти символы можно удалить из разметки не нарушив при этом корректную работу сайта.

Я бы хотел рассказать о том, как можно выполнить такую оптимизацию на этапе компиляции сайта.

Немного теории

Прежде всего, необходимо вспомнить, как генерируется html-разметка в asp.net.

При обработке aspx-, ascx- или master-файла asp.net проверяет, обрабатывался ли этот файл ранее. Если файл обрабатывается впервые, то файл парсится и создаётся связанный с ним ControlBuilder (который используется при повторной обработке файла).

Каждый раз когда необходимо создать Control, связанный с каким-либо файлом, asp.net сначала получает соответствующий ControlBuilder, а потом с помощью него создаёт сам Control.

Это означает, что чтобы вмешаться в генерируемую каким-то файлом html-разметку и оптимизировать её, необходимо это сделать на этапе создания ControlBuilder`а.

Реализация

Для рашения поставленной задачи создадим собственный PageParserFilter. Этот класс позволяет добавить дополнительную логику в обработку, связанную с парсингом страниц.

Прежде всего переопределим его виртуальные методы и свойства:

  • AllowBaseType
  • AllowCode
  • AllowControl
  • AllowServerSideInclude
  • AllowVirtualReference
  • NumberOfControlsAllowed
  • NumberOfDirectDependenciesAllowed
  • TotalNumberOfDependenciesAllowed

Все они должны возвращать true или -1 в зависимости от типа возвращаемого значения (true указывает на допустимость той или иной возможности, а -1 - на отсутствие ограничений на число чего-либо, например, дочерних элементов управления).

Также переопределим метод ParseComplete, добавив в него вызов метода MinifyHtmlInControlBuilder, который который и будет производить всю работу.

Класс ControlBuilder имеет два свойства: SubBuilders и TemplatePropertyEntries.

Свойство SubBuilders имеет тип ArrayList и может содержать в себе как строки, которые нам и надо оптимизировать, так и другие ControlBuilder`ы. Данное свойство используется, когда один элемент управления содержит в себе другие элементы управления. Например, элемент управления PlaceHolder может содержать в себе другие элементы управления.

Свойство TemplatePropertyEntries имеет тип ICollection и содержит объекты типа TemplatePropertyEntry. Данное свойство используется, когда элемент управления содержит в себе шаблоны. Например, элемент управления ListView может содержать в себе шаблоны LayoutTemplate, ItemTemplate и другие.

К сожалению, оба этих свойства не являются публичными, поэтому доступ к ним получим при помощи рефлексии и методов расширений.

Добавим реализацию метода MinifyHtmlInControlBuilder.

Во-первых, пройдём по всем объектам в свойстве SubBuilders. В случае, если объект является строкой, удалим из неё все лишние символы. Иначе, если объект является ControlBuilder`ом, рекурсивно вызовем для него метод MinifyHtmlInControlBuilder.

Во-вторых, пройдём по всем объектам в свойстве TemplatePropertyEntries и, получая соответствующий для него ControlBuilder через свойство Builder, рекурсивно рекурсивно вызовем метод MinifyHtmlInControlBuilder.

Чтобы не углубляться в способы оптимизации html-разметки, возьмём самый простой алгоритм удаления лишних символов, основанный на регулярных выражениях. Заключается он в том, чтобы удалить все разделительные символы (пробелы, табуляции, переносы строк) до и после символов «<» и «>» соответственно, а затем заменить два или более разделительных символа на один пробел.

Данный алгоритм, к сожалению, не всегда корректно может обрабатывать страницу. Например, он не удаляет html-комментарии, может нарушать текст внутри тегов pre или script. Но в большинстве случаев, если сайт не содержит большого количества скриптов непосредственно в html-разметке, проблем с его использованием возникнуть не должно.

Теперь зарегистрируем наш PageParserFilter в файле web.config.

Осталось только проверить как данный метод будет работать.

Тестирование

Для того, чтобы проверить, как данный метод будет работать на реальном сайте, я установил свежую версию NET Forge CMS с демонстрационными данными, и получил следующие результаты (для страниц, которые есть в главном меню):

  • страница «Записи» - до оптимизации 42,9 кб, после оптимизации 40,0 кб (93,2%);
  • страница «Блоги» - до оптимизации 14,7 кб, после оптимизации 13, 5 кб (91,8%);
  • страница «Форумы» - до оптимизации 14,4 кб, после оптимизации 12,8 кб (88,9%);
  • страница «Люди» - до оптимизации 22,3 кб, после оптимизации 20,3 кб (91,0%).

Как видно, в среднем удалось уменьшить размер страниц на 8-10 процентов.

Учитывая, что демонстрационных данных в NET Forge CMS было не так уж и много, думаю, что на активно используемом сайте можно добиться больших результатов.

К тому же, результаты будут ещё лучше, если вы в своих проектах предпочитаете использовать пробелы вместо символов табуляции (в NET Forge CMS используется табуляция).

Заключение

Возможно, 8-10 процентов не настолько уж и большой результат, поэтому стоит хорошо подумать, готовы ли вы расплатиться за неё небольшой задержкой при первом открытии страницы и возможными проблемами в работе сайта, которые теоретически могут иметь место.

В данной статье я использовал самый простой алгоритм удаления лишних символов. При желании можно создать более сложные и быстрые алгоритмы, которые могут, например, удалять ненужные кавычки у атрибутов без пробелов, удалять ненужные закрывающие теги (такие как li, tr или td) и при этом не будут нарушать текст в тегах pre или script.

Скачать демо (ASP.NET 4.0, Web Site)

23 комментария

  1. Привет,

    Это не опечатка?
    var subBuilder = subBuilders[index] as ControlBuilder;
    var html = subBuilders[index] as string;
    Коллекция та же, индекс тот же, а типы разные. У ControlBuilder-а implicit\explicit конвертации нету.

    Добавлю еще один минус подхода, то что для каждой страницы будет в результате такого вырезания будет создаватся много мусора в памяти в ввиде строк по 4 для каждого ControlBuilder-а на странице.

    ОтветитьУдалить
  2. А с ASP.Net MVC этот способ будет работать?

    ОтветитьУдалить
  3. Нет, это не опечатка.
    Я написал, что коллекция имеет тип ArrayList, она не типизирована и может содержать как строки, так и ControlBuilder`ы.

    Не понял про мусор при вырезании. Данный код для каждого файла (aspx, ascx, master) запускается только один раз в тот момент, когда этот файл будет компилироваться. При повторном обращении этот код уже не будет вызываться, и никаких других затрат не будет.

    На мой взгляд это вполне приемлемо.

    ОтветитьУдалить
  4. Если в ASP.NET MVC используется WebForms Engine, то, думаю, что должно работать. Правда, насколько я знаю, там уже используется какой-то PageParserFilter, а двух их быть не может, поэтому нужно будет унаследоваться от того класса, который там уже используется.

    Если используется Razor Engine, то там нужно будет делать иначе (хотя похоже). Хотел написать статью на примере Orchard CMS, но в ней почему-то и не работает, хотя должно. Если разберусь - напишу :о)

    ОтветитьУдалить
  5. Sergey Litvinov

    Кажется понял, что имеется ввиду под словами "для каждой страницы в результате такого вырезания будет создаватся много мусора в памяти в ввиде строк по 4 для каждого ControlBuilder-а на странице"

    Создаваться-то они будут, но они также будут собираться сборщиком мусора, т.к не хранятся в пуле интернирования, так что не стоит над этим переживать :о)

    ОтветитьУдалить
  6. Что-то не совсем понял, что значит оптимизация на этапе компилирования ? Ведь кодина будет отрабатывать каждый раз при запросе страницы, если меняются входные параметры для нее. Или не так ?

    ОтветитьУдалить
  7. Нет, не так.

    Оптимизация будет делаться не "каждый раз при запросе страницы", а только при первом запросе страницы, когда эта страница будет компилироваться.

    Если проще говоря - В asp.net работает динамическая компиляция. Т.е весь сайт не компилируется целиком при запуске, т.к это долго. Когда пользователь запрашивает какую-то страницу она компилируется в dll и подгружается к приложению и потом используется повторно.

    Эта dll содержит скомпилированные страницы, весь html в них генерируется примерно так:
    output.Write("\t<h1>\r\n\t\tHello, World!\r\n\t</h1>");

    Я перехватываю момент динамической компиляции страницы и заменяю это на
    output.Write("<h1>Hello, World!</h1>");

    В браузере нет никакой разницы, а размер страницы меньше.

    Надеюсь теперь стало понятнее :о)

    ОтветитьУдалить
  8. >>> Т.е весь сайт не компилируется целиком при запуске, т.к это долго. Когда пользователь запрашивает какую-то страницу она компилируется в dll и подгружается к приложению и потом используется повторно

    Это зависит от настроек.

    Если я запрашиваю страницу, передавая в нее некий параметр, от которого зависит текст, который мне будет показан (например, в страницу для вывода новостей я передаю дату), ваша кодина отработает заново ?

    ОтветитьУдалить
  9. Это зависит от настроек в VisualStudio, на IIS сайт всё равно не будет компилироваться целиком. Максимум, что там можно настроить, чтобы IIS за один раз компилировал несколько файлов сразу.

    В любом случае, когда на страницу передаются какие-то параметры, они передаются на уже скомпилированную страницу. А она уже оптимизирована. Поэтому код отработает только один раз.

    Повторно он вызовется только, если изменить сам aspx-файл (или ascx, или master), т.к asp.net нужно будет перекомпилировать изменившуюся страницу.

    Если интересно это проверить, то вот исходник:
    narod.ru/disk/23295245001/HtmlMinifer.cs.html
    Можно добавить в код логгер, который будет писать что-то в файл и посмотреть сколько раз он вызывается.

    ОтветитьУдалить
  10. Вот теперь понял, спасибо. Невнимательно прочитал статью.

    ОтветитьУдалить
  11. "Закончился срок хранения файла. Файл удален с сервиса." - Это по поводу вашей ссылки!

    ОтветитьУдалить
  12. Не удивительно, учитывая что файл был ещё в августе.
    Уже думаю, как лучше исходники выкладывать.

    ОтветитьУдалить
  13. Да, уж выкладывайте. Любопытно было бы взглянуть... А вообще - большая претензия к Microsoft Visual Studio/Visual Web Developer. Все эти функции по логике относятся к инструментам и утилитам среды разработки, и никакого отношения они не имеют к программированию самой бизнес-логики! Поэтому мы CSS и Javascript минимизируем с помощью одних средств, а HTML внутри ASP.NET - c помощью класса PageParserFilter! Хотя можно было бы объединить данные операции на уровне IDE! А так это - извращение, неуважение к разработчикам со стороны российского отделения Microsoft. Балду гоняют.

    ОтветитьУдалить
  14. Взглянуть-то можно и так, весь код выше, там больше ничего и не нужно. (т.е если всё это подключить к работающему сайту, то должно работать)
    По поводу претензий к MSVS не совсем понял (вернее совсем не понял =).

    ОтветитьУдалить
  15. Посмотрел из любопытства
    Смог уронить, добавив на мастер-страницу контрол в таком виде:
    <%@ Register TagPrefix="uc" TagName="test" Src="~/Controls/Test.ascx" %>

    Ошибка была именно в парсинге:
    Parser Error

    Description: An error occurred during the parsing of a resource required to service this request. Please review the following specific parse error details and modify your source file appropriately.

    Parser Error Message: Object reference not set to an instance of an object.

    так-что немного не рабочий вариантик..

    ОтветитьУдалить
    Ответы
    1. А код контрола можно посмотреть?

      Удалить
    2. он пустой, а трейс неправильно строку показывал

      проблема была в доставании дочерних билдеров, я написал так:
      builderType.GetProperty("SubBuilders")
      Стало все отлично, для темплейтов также, сейчас отличненько отрабатывает

      Удалить
    3. Да, все интересные свойства контролбилдеров закрытые, приходится доставать через рефлексию. Иначе можно было просто так их задавать.

      Удалить
    4. итого - проблема была не в добавлении контрола (ошибочный трейс), а в том, что выборка свойства была пустой из-за условия выборки
      сейчас отличненько работает, метод убирания лишнего в прекомпиленной версии хорош, спасибо )

      тестил под iis express 8.0, .net 4.0, web site, рендер капабилити 3.5

      Удалить
    5. Что имеется ввиду под "ошибочный трейс"?)

      Удалить
    6. На экране с ошибкой отображалась область регистрации контрола, в то время как следовало вывести место самой ошибки, т.е.:
      return (ArrayList) builderType
      .GetProperty("SubBuilders", BindingFlags.NonPublic | BindingFlags.Instance)
      .GetValue(builder, null);

      Удалить
    7. Хм, наверное нужно просто компиляцию сайта отключить при запуске, тогда он будет показывать нужное место и можно будет использовать дебаг.

      Удалить

© Игорь Чакрыгин. Все права защищены при помощи чёрной магии. Технологии Blogger.