Главная > .NET, JavaScript > Клиентская оптимизация в ASP.NET MVC 3. Менеджеры ресурсов

Клиентская оптимизация в ASP.NET MVC 3. Менеджеры ресурсов

Первоначально данная статья была опубликована в февральском номере журнала MSDeveloper.RU.

Содержание

  1. Сжатие JS- и CSS-файлов
  2. Менеджеры ресурсов

В предыдущей статье мы научились производить минимизацию JS- и CSS-файлов в Visual Studio.

В данной статье мы рассмотрим другие аспекты клиентской оптимизации, связанные с JS- и CSS-файлами и решаемые с помощью менеджеров ресурсов (asset managers). Перечислим рекомендации команды Exceptional Performance, которые можно реализовать с помощью менеджеров ресурсов:

  1. Выносите JavaScript и CSS во внешние файлы
  2. Размещайте таблицы стилей в начале страницы
  3. Размещайте скрипты в конце страницы
  4. Используйте CDN
  5. Уберите повторяющиеся скрипты
  6. Уменьшите количество HTTP-запросов (с помощью объединения JS- и CSS-файлов)
  7. Добавьте HTTP-заголовок Expires или Cache-Control
  8. Сжимайте содержимое с помощью GZIP
  9. Настройте ETag

На основе перечисленных выше рекомендаций сформулируем требования, которым должен соответствовать менеджер ресурсов:

  1. Регистрировать ресурсы (файлы) в шаблонах (мастер-страницах) и представлениях
  2. Предотвращать дублирование ресурсов
  3. Выбирать нужные версии ресурсов в зависимости от режима работы веб-приложения (отладка или релиз)
  4. Задавать URL ресурса, размещенного на CDN, в качестве альтернативного пути к ресурсу
  5. Объединять код ресурсов в один файл
  6. Минимизировать ресурсы «на лету» (в нашем случае необязательное требование)
  7. HTTP-хэндлер, с помощью которого выводятся обработанные ресурсы, должен добавлять HTTP-заголовок Expires или Cache-Control
  8. HTTP-хэндлер должен сжимать ресурсы с помощью GZIP/Deflate
  9. HTTP-хэндлер должен поддерживать ETag

Остановимся подробно на 3-ем требовании. Режим работы ASP.NET-приложения задается в файле Web.config:

<system.web>
    <compilation debug="false" targetFramework="4.0">
    ...

Значение атрибута debug определяет режим, в котором находится веб-приложение: true – отладка или false – релиз. В ASP.NET-коде всегда можно определить в каком режиме находится веб-приложение с помощью свойства HttpContext.Current.IsDebuggingEnabled.

Необходимый нам менеджер ресурсов должен использовать данную информацию при рендеринге ссылок на файлы. Например, в режиме отладки должны быть подключены отладочные версии файлов:

<link href="/Content/basic.css" media="all" rel="stylesheet" type="text/css" />
...
<script src="/Scripts/MicrosoftAjax.debug.js" type="text/javascript"></script>
<script src="/Scripts/jquery-1.7.1.js" type="text/javascript"></script>

А в режиме релиза минимизированные версии:

<link href="/Content/basic.min.css" media="all" rel="stylesheet" 
    type="text/css" />
...
<script src="/Scripts/MicrosoftAjax.js" type="text/javascript"></script>
<script src="/Scripts/jquery-1.7.1.min.js" type="text/javascript"></script>

Менеджеры ресурсов для ASP.NET MVC

Для ASP.NET MVC существует достаточно много менеджеров ресурсов. Кратко рассмотрим наиболее известные из них и проверим их на соответствие нашим требованиям (см. табл. 1):

Табл. 1. Сравнение основных возможностей менеджеров ресурсов

Сравнительная таблица основных возможностей менеджеров ресурсов

* — можно реализовать самостоятельно

ScriptRegistrar и StyleSheetRegistrar из Telerik Extensions for ASP.NET MVC

Демонстрационный сайт набора элементов управления Telerik Extensions for ASP.NET MVC

Разработчик: Telerik
Сайт продукта: http://www.telerik.com/products/aspnet-mvc.aspx
Типы лицензий: Open Source (GPL v2) и коммерческая лицензия (999 USD в год)
Рассматриваемая версия: 2011.3.1306

Соответствует требованиям: 1, 3, 5, 7 и 8. Четвертое требование поддерживается только для групп ресурсов.

Достоинства:

  • Наличие очень удобных и гибких средств настройки объединения ресурсов
  • Позволяет задавать номер версии для объединенных ресурсов (помогает решить проблемы с кэшем браузера)
  • Позволяет указывать продолжительность нахождения объединенного ресурса в кэше браузера (в днях)
  • Позволяет включать/отключать GZIP-сжатие для объединенных ресурсов

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

Cassette

Сайт менеджера ресурсов Cassette

Прежнее название: Knapsack
Разработчик: Equin Ltd
Сайт продукта: http://getcassette.net
Типы лицензий: Open Source (MIT)
Рассматриваемая версия: 1.0.0

Соответствует требованиям: 1, 2, 4, 5, 6, 7, 8 и 9.

Использует для минимизации ресурсов Microsoft Ajax Minifier.

Достоинства:

  • Может также использоваться в ASP.NET Web Forms
  • Использует информацию из документирующего тега reference для подключения JS-файлов, от которых зависит регистрируемый JS-ресурс
  • Поддерживает серверную трансляцию кода на CoffeeScript в JavaScript и кода на динамическом языке стилевой разметки LESS в CSS
  • Регистрирует в мастер-страницах и представлениях клиентские HTML-шаблоны, обрабатываемые JS-библиотеками: jQuery-tmpl и KnockoutJS, а также поддерживает их компиляцию на стороне сервера
  • С помощью метода Bundles.AddPageData можно передавать данные из серверной части страницы в клиентскую в виде JSON-объектов
  • Возможность преобразования рисунков, указанных в CSS-коде, в Data-URI (пока экспериментальная)

Недостатки:

  • Нельзя группировать ресурсы в абстрактные группы (объединение ресурсов происходит только на основе структуры директорий в файловой системе)
  • Порядок, в котором расположен код ресурсов в объединенном ресурсе, можно задать только с помощью конфигурационного файла bundle.txt
  • При обработке ресурсов относительные пути к рисункам в CSS-коде не преобразуются в абсолютные

SquishIt

Страница менеджера ресурсов SquishIt на сайте GitHub

Прежнее название: Bundler
Разработчик: Justin Etheredge
Сайт продукта: https://github.com/jetheredge/SquishIt
Тип лицензии: Open Source (MIT)
Рассматриваемая версия: 0.8.2

Соответствует требованиям: 1, 4, 5 и 6. Требования 7, 8 и 9 также поддерживаются, но нужно отметить, что SquishIt не использует HTTP-хэндлер для вывода обработанных ресурсов, а сохраняет обработанные ресурсы в файловую систему как статические файлы. Соответственно для статических файлов все 3 последних требования реализуются средствами веб-сервера, а не менеджера ресурсов.

Поддерживает следующие алгоритмы минимизации: YUI Compressor, Microsoft Ajax Minifier, JSMin (только для JS) и Google Closure Compiler (только для JS).

Достоинства:

  • Может также использоваться в ASP.NET Web Forms
  • Поддерживает серверную трансляцию CoffeeScript в JavaScript и LESS в CSS
  • Автоматически управляет обновлением обработанных ресурсов в кэше браузера
  • Позволяет задавать произвольные HTML-атрибуты для тегов link (например, можно задать атрибут media) и script
  • Поддерживает методы, имитирующие локальное переключение режима веб-приложения: ForceDebug (в режим отладки) и ForceRelease(в режим релиза)

Недостатки:

  • В текущей версии некорректно работает разбивка ресурсов на виртуальные группы с помощью метода AddToGroup (будет удален в следующей версии), что не позволяет гибко настроить объединение ресурсов
  • Объединенные ресурсы сохраняются на диск и не удаляются с него автоматически
  • При обработке ресурсов относительные пути к рисункам в CSS-коде не преобразуются в абсолютные

Combres

Страница менеджера ресурсов Combres на сайте CodePlex

Разработчик: Buu Nguyen
Сайт продукта: http://combres.codeplex.com
Тип лицензии: Open Source (Apache License 2.0)
Рассматриваемая версия: 2.2.2.2

Отвечает требованиям: 1, 5, 6, 7, 8 и 9. Третье требование можно реализовать путем написания своего фильтра (класса, реализующего интерфейс ISingleContentFilter из пространства имен Combres) и отключения минимизации по умолчанию (нужно установить для набора ресурсов в качестве минимизатора класс NullMinifier из пространства имен Combres.Minifiers).

Поддерживает следующие алгоритмы минимизации: YUI Compressor, Microsoft Ajax Minifier и Google Closure Compiler (только для JS).

Достоинства:

  • Может также использоваться в ASP.NET Web Forms и Windows Azure
  • Многие компоненты можно заменить собственными реализациями (фильтры, минимизаторы и генераторы версий)
  • Позволяет настроить параметры алгоритмов минимизации в файле конфигурации combres.xml
  • Серверная трансляция LESS
  • Автоматически управляет обновлением обработанных ресурсов в кэше браузера
  • Позволяет указывать продолжительность нахождения обработанного ресурса в серверном и браузерном кэше (в днях)
  • Позволяет включать/отключать GZIP-сжатие для обработанных ресурсов

Недостатки:

  • Все настройки задаются в XML-файле конфигурации

LuckyAssetManager

Страница менеджера ресурсов LuckyAssetManager на сайте CodePlex

Разработчик: Frank Hadder
Сайт продукта: http://github.com/luckyllama/LuckyAssetManager
Тип лицензии: Open Source (MIT)
Рассматриваемая версия: 1.0.5

Соответствует требованиям: 1, 4, 5, 6 и 8. Частично соответствует 2-му требованию, т.к. механизм предотвращающий дублирование не может работать с ресурсами, добавленными в группу, и ресурсами, у которых перенастроен рендеринг (с помощью методов ForMediaType и ForIE). 3-е требование можно реализовать самим: путем замены обработчика минимизации на свой собственный. 7-е и 8-е требования можно реализовать путем создания собственного HTTP-хэндлера.

Поддерживает алгоритм минимизации YUI Compressor.

Достоинства:

  • Возможность замены стандартных обработчиков минимизации и объединения на собственные (например, можно реализовать минимизацию с помощью другого алгоритма минимизации или реализовать трансляцию CoffeeScript и LESS)
  • Возможность указывать для тега link значение HTML-атрибута media с помощью метода ForMediaType
  • Возможность заключать теги link и script в условные комментарии Internet Explorer с помощью метода ForIE

Недостатки:

  • Обработчик относительных путей в CSS-коде, используемый по умолчанию, добавляет в пути название домена сайта (избыточно) и пропускает некоторые пути при обработке
  • В памяти веб-сервера всегда хранится только одна версия обработанного ресурса. Добавление кода новой версии ресурса в кэш происходит при рендеринге представления, в котором производится регистрация соответствующих ресурсов. Таким образом, может возникнуть ситуация (например, если в приложении ASP.NET MVC включено кэширование вывода действия контроллера), когда при обращении к HTTP-хэндлеру нужный элемент кэша будет пуст и из-за этого возникнет ошибка.

Web.Require

Страница менеджера ресурсов Web.Require на сайте GitHub

Разработчик: Александр Зайцев
Сайт продукта: https://github.com/hazzik/Web.Require
Тип лицензии: Open Source
Рассматриваемая версия: 1.0.6.0

Соответствует требованиям: 1 и 2.

Достоинства:

  • Поддерживает асинхронную загрузку JS-файлов

Недостатки:

  • Возможностей текущей версии продукта недостаточно для проведения комплексной клиентской оптимизации, но в планах разработчика реализовать в следующих версиях продукта минимизацию и объединение ресурсов.

ASP.NET Optimization – Bundling

Страница менеджера ресурсов ASP.NET Optimization – Bundling на сайте NuGet Gallery

Разработчик: Microsoft
Сайт продукта: http://nuget.org/packages/Microsoft.Web.Optimization
Тип лицензии: бесплатная
Рассматриваемая версия: 0.1

В предварительной версии ASP.NET 4.5 появился встроенный менеджер ресурсов, возможности которого подробно описаны в статье Скотта Гатри «New Bundling and Minification Support (ASP.NET 4.5 Series)». ASP.NET Optimization – Bundling является портом данного менеджера ресурсов под ASP.NET 4.0.

Данный менеджер ресурсов соответствует требованиям: 5, 6, 7 и 8. Третье требование поддерживается только частично: если свойству EnableFileExtensionReplacements объекта типа Bundle присвоить значение true, то будут использоваться версии файлов с расширениями *.min.css и *.min.js. Но лучше не использовать данное свойство по 3 причинам:

  1. Свойство EnableFileExtensionReplacements не реализовано в версии для ASP.NET 4.5
  2. Данный механизм не поддерживает аналогичную функциональность для файлов с расширениями *.debug.js
  3. Встроенные обработчики (JsMinify и CssMinify) производят повторную минимизацию кода (нужно реализовать свой обработчик, который не будет производить минимизацию)

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

Продукт поддерживает минимизацию JS- и CSS-кода, но узнать, с помощью каких алгоритмов она реализована, не удалось.

Достоинства:

  • Данный менеджер ресурсов можно использовать в Web Forms, MVC и Web Pages, т.к. он работает на уровне ядра ASP.NET
  • Возможность замены стандартных обработчиков ресурсов на собственные (нужно создать класс, реализующий интерфейс IBundleTransform из пространства имен Microsoft.Web.Optimization)
  • Написанный код можно будет легко перенести на ASP.NET 4.5 (нужно просто заменить пространство имен Microsoft.Web.Optimization на System.Web.Optimization)

Недостатки:

  • Нет встроенных средств регистрации ресурсов в мастер-страницах и представлениях (минимизация и объединение ресурсов настраиваются в файле Global.asax), поэтому лучше его использовать в связке с регистратором ресурсов (например, Web.Require)
  • В любом режиме работы веб-приложения ресурсы находятся в объединенном состоянии, что затрудняет процесс отладки
  • Портированная версия, в отличие от ASP.NET 4.5, при обработке игнорирует файлы с расширением *.debug.js
  • Ресурсы, входящие в объединенный ресурс, по умолчанию расположены в алфавитном порядке, а не в порядке объявления. Чтобы изменить данное поведение нужно реализовать собственный сортировщик (класс, реализующий интерфейс IBundleOrderer из пространства имен Microsoft.Web.Optimization) и присвоить его экземпляр свойству Orderer объекта типа Bundle.
  • При обработке ресурсов относительные пути к рисункам в CSS-коде не преобразуются в абсолютные

Обзор возможностей LuckyAssetManager

Наиболее интересным, с точки зрения удобства использования в ASP.NET MVC, возможностей расширения функционала и требований лицензии, выглядит LuckyAssetManager, поэтому мы его рассмотрим более подробно.

Установка

LuckyAssetManager устанавливается с помощью пакетного менеджера NuGet (не ниже версии 1.6). Чтобы установить LuckyAssetManager выполните следующие действия:

  1. Щелкните правой кнопкой мыши по узлу References проекта и в появившемся контекстном меню выберите пункт Manage NuGet Packages.
  2. В открывшемся окне пакетного менеджера NuGet в поле Search Online введите следующий текст: LuckyAssetManager. Должны отобразиться результаты поиска:

    Результаты поиска с помощью NuGet
    Рис. 1. Результаты поиска с помощью NuGet

  3. В результатах поиска в строке, соответствующей LuckyAssetManager, нажмите кнопку Install.
  4. После завершения установки нажмите кнопку Close.

Настройка

В процессе установки LuckyAssetManager в файл Web.config добавляются следующие настройки: секция под названием lucky (в ней объявлены обработчики минимизации, объединения и корректировки путей в CSS-файлах), объявления пространств имен и регистрируется HTTP-хэндлер для вывода обработанных ресурсов.

Рассмотрим секцию lucky более подробно:

<lucky>
    <assetManager debug="true">
        <css>
            <processors>
                <add name="RelativePathing" type="Lucky.AssetManager.
Processors.CssRelativePathProcessor, Lucky.AssetManager" />
                <add name="Combine" type="Lucky.AssetManager.Processors.
CombineProcessor, Lucky.AssetManager" />
                <add name="Minimize" type="Lucky.AssetManager.Processors.
YuiMinimizeProcessor, Lucky.AssetManager" />
            </processors>
        </css>
        <javascript alternateName="cdn">
            <processors>
                <add name="Combine" type="Lucky.AssetManager.Processors.
CombineProcessor, Lucky.AssetManager" />
                <add name="Minimize" type="Lucky.AssetManager.Processors.
YuiMinimizeProcessor, Lucky.AssetManager" />
            </processors>
        </javascript>
    </assetManager>
</lucky>

В элементах processors объявлены обработчики ресурсов:

  1. CssRelativePathProcessor. Преобразует относительные пути, содержащиеся в CSS-коде, в абсолютные (с точки зрения веб-приложения).
  2. CombineProcessor. Объединяет ресурсы, входящие в одну группу и имеющие одинаковые настройки рендеринга, в один ресурс (файл).
  3. YuiMinimizeProcessor. Минимизирует код ресурса «на лету» с помощью YUI Compressor.

Вызов обработчиков осуществляется в порядке их объявления в элементе processors.

В элементах css и javascript может быть задан атрибут alternateName, значение которого определяет какие необходимо использовать альтернативные пути при рендеринге ссылок на CSS- и JS-файлы. Например, если мы зарегистрировали ресурс следующим образом:

Assets
    .MasterJavascript("~/Scripts/jquery-1.7.1.js")
    .WithAlternatePath("cdn", 
        "http://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.7.1.min.js")
    .Add()
    ;

И в атрибуте alternateName указали значение, совпадающее с ключом альтернативного пути (в данном случае, cdn), то во время рендеринга вместо обычного пути к файлу будет использоваться альтернативный путь.

Любой из этих обработчиков можно заменить собственным. Чтобы создать собственный обработчик вы должны создать класс, реализующий интерфейс IProcessor из пространства имен Lucky.AssetManager.Processors.

Для своих проектов я реализовал собственные обработчики:

  1. SelectPathProcessor. Выбирает нужные версии ресурсов в зависимости от режима работы веб-приложения (отладка или релиз), т.е. реализует 3-е требование.
  2. FinalProcessor. Производит обработку путей в CSS-коде и объединение ресурсов, т.е. объединяет в себе функционал CssRelativePathProcessor и CombineProcessor. Данный вариант обработчика стал более производительным благодаря правильной реализации кэширования обработанных ресурсов. Кроме того, были исправлены ошибки, возникающие при обработке путей в CSS-коде.

Также я написал собственный вариант HTTP-хэндлера AssetsHandler. При совместном использовании с FinalProcessor`ом у AssetsHandler`а появляется поддержка следующих возможностей:

  1. Кэширование обработанного ресурса в кэше браузера посетителя (с помощью HTTP-заголовков Expires и Cache-Control).
  2. Возможность отключить GZIP/Deflate-сжатие.
  3. Поддержка ETag`ов и HTTP-заголовка Last-Modified.
  4. Автоматическое управление обновлением обработанных ресурсов в кэше браузера, которое реализовано с помощью нового механизма генерации ключа ресурса. Теперь при генерации ключа обработанного ресурса учитывается дата и время последнего изменения файлов, ассоциированных с этим ресурсом.
  5. Также теперь HTTP-хэнлер может выводить сразу несколько версий обработанного ресурса. Выбор версии ресурса осуществляется на основе значения ключа, переданного HTTP-хэндлеру. Самая последняя версия обработанного ресурса хранится в памяти веб-сервера, а ее копия и более старые версии ресурса хранятся в файловом кэше (расположен в директории App_Data\AssetFileCache). Благодаря данной возможности можно без всякого риска использовать кэширование вывода действия контроллера.

Исходный код вышеперечисленных обработчиков и HTTP-хэндлера опубликован в виде проекта под названием LuckyAssetManagerContrib на сайте CodePlex. Кроме того, вы можете добавить их в свой проект с помощью NuGet. Исходный код обработчиков может вам пригодится даже, если вы не будете использовать LuckyAssetManager в своих проектах. Например, исходный код класса SelectPathProcessor можно использовать в качестве основы при написании аналогичных обработчиков для Combres и ASP.NET Optimization – Bundling.

Остановимся более подробно на конфигурации LuckyAssetManagerContrib. При установке LuckyAssetManagerContrib с помощью NuGet в элемент configSections файла Web.config добавляется объявление секции luckyAssetManagerContrib:

<section name="luckyAssetManagerContrib" 
    type="LuckyAssetManagerContrib.Configuration.AssetManagerSettings" 
    allowDefinition="Everywhere" 
    allowLocation="true" />

И сама секция luckyAssetManagerContrib:

<luckyAssetManagerContrib>
    <selectPathProcessor 
        memoryCacheDurationInMinutes="15"
        useMemoryCacheSlidingExpiration="false" />
    <finalProcessor 
        disableCombiningAssetsInDebugMode="true" />
    <assetFileCacheManager 
        fileCacheDurationInMinutes="30" 
        fileCacheRegistrySaveIntervalInMinutes="1" 
        useFileCacheSlidingExpiration="true" />
    <assetsHandler 
        clientCacheDurationInDays="15" 
        enableCompression="true" 
        useLastModifiedHeader="true" 
        useETagHeader="true" />
</luckyAssetManagerContrib>

Рассмотрим каждый элемент секции.

В элементе selectPathProcessor задаются настройки обработчика SelectPathProcessor:

  1. memoryCacheDurationInMinutes="(число)". Задает продолжительность хранения нужной версии пути к файлу ресурса в кэше, который находится в памяти сервера (в минутах).
  2. useMemoryCacheSlidingExpiration="(true|false)". Разрешает использование скользящего времени устаревания элемента кэша.

В элементе finalProcessor задаются настройки обработчика FinalProcessor:

  1. disableCombiningAssetsInDebugMode="(true|false)". Отключает объединение ресурсов в режиме отладки.

В элементе assetFileCacheManager задаются настройки менеджера файлового кэша, с помощью которого обеспечивается доступность нескольких версий обработанного ресурса:

  1. fileCacheDurationInMinutes="(число)". Задает продолжительность хранения элемента файлового кэша (в минутах).
  2. fileCacheRegistrySaveIntervalInMinutes="(число)". Задает интервал сохранения реестра файлового кэша на диск (в минутах).
  3. useFileCacheSlidingExpiration="(true|false)". Разрешает использование скользящего времени устаревания элемента кэша.

В элементе assetsHandler задаются настройки HTTP-хэндлера:

  1. clientCacheDurationInDays="(число)". Задает продолжительность нахождения текстового содержимого обработанного ресурса в кэше браузера (в днях).
  2. enableCompression="(true|false)". Включает GZIP/Deflate-сжатие обработанного ресурса.
  3. useLastModifiedHeader="(true|false)". Разрешает использовать HTTP-заголовок Last-Modified для оповещения браузера об изменении файлов, ассоциированных с обработанным ресурсом.
  4. useETagHeader="(true|false)". Разрешает использовать HTTP-заголовок ETag для оповещения браузера об изменении файлов, ассоциированных с обработанным ресурсом.

Остальные настройки придется вносить вручную.

Во-первых, нужно удалить атрибут debug из элемента assetManager конфигурационной секции lucky, после чего нужно заменить старые обработчики на новые. После внесенных изменений содержимое секции lucky должно принять следующий вид:

<lucky>
    <assetManager>
        <css>
            <processors>
                <add name="SelectPath" type="LuckyAssetManagerContrib.
Processors.SelectPathProcessor, LuckyAssetManagerContrib" />
                <add name="Final" type="LuckyAssetManagerContrib.Processors.
FinalProcessor, LuckyAssetManagerContrib" />
            </processors>
        </css>
        <javascript alternateName="cdn">
            <processors>
                <add name="SelectPath" type="LuckyAssetManagerContrib.
Processors.SelectPathProcessor, LuckyAssetManagerContrib" />
                <add name="Final" type="LuckyAssetManagerContrib.Processors.
FinalProcessor, LuckyAssetManagerContrib" />
            </processors>
        </javascript>
    </assetManager>
</lucky>

Во-вторых, нужно найти в Web.config все объявления HTTP-хэндлера, который имеет значение атрибута type равное Lucky.AssetManager.Web.AssetsHandler, и заменить это значение на LuckyAssetManagerContrib.Web.AssetsHandler.

После завершения настройки можно приступить к использованию LuckyAssetManager.

Регистрация ресурсов

Рассмотрим применение LuckyAssetManager на конкретном примере. В проекте используется мастер-страница _Layout.cshtml, содержащая следующий код:

@using Lucky.AssetManager;
@using Lucky.AssetManager.Web

@{
    // Регистрируем группу таблиц стилей
    Assets.MasterCss("~/Content/basic.css", "CommonStyles").Add();
    Assets.MasterCss("~/Content/skins/lefteast/jquery-ui-1.8.16.custom.css", 
        "CommonStyles").Add();
    Assets.MasterCss("~/Content/skins/lefteast/jquery-ui-timepicker-addon.css", 
        "CommonStyles").Add();
    Assets.MasterCss("~/Areas/Admin/Content/admin.css", "CommonStyles").Add();

    // Регистрируем скрипт HTML5 Shiv 
    // (обеспечивает поддержку HTML5-тегов 
    // в старых версиях Internet Explorer)
    Assets.MasterJavascript("~/Scripts/html5.js", "Html5Shiv")
        .ForIE(IE.Equality.LessThan, IE.Version.IE9)
        .Add()
        ;

    // Регистрируем группы скриптов
    Assets.MasterJavascript("~/Scripts/MicrosoftAjax.js", 
        "CommonScripts").Add();
    Assets.MasterJavascript("~/Scripts/jquery-1.7.1.js", 
        "CommonScripts").Add();
    Assets.MasterJavascript("~/Scripts/jquery-ui-1.8.16.custom.js", 
        "CommonScripts").Add();
    Assets.MasterJavascript("~/Scripts/jquery-ui-timepicker-addon.js", 
        "CommonScripts").Add();
    Assets.MasterJavascript("~/Scripts/jquery.validate.js", 
        "CommonScripts").Add();
    Assets.MasterJavascript("~/Scripts/jquery.validate.unobtrusive.js", 
        "CommonScripts").Add();
    Assets.MasterJavascript("~/Scripts/jquery.ba-bbq.js", 
        "CommonScripts").Add();

    Assets.MasterJavascript("~/Areas/Admin/Scripts/LeftEast/JsConstants.ashx", 
        "LeftEastScripts")
        .IgnoreProcessing()
        .Add()
        ;
    Assets.MasterJavascript("~/Areas/Admin/Scripts/LeftEast/JsLanguage.ashx", 
        "LeftEastScripts")
        .IgnoreProcessing()
        .Add()
        ;
    Assets.MasterJavascript("~/Areas/Admin/Scripts/LeftEast/Common.js", 
        "LeftEastScripts").Add();
    Assets.MasterJavascript("~/Areas/Admin/Scripts/LeftEast/Utils.js", 
        "LeftEastScripts").Add();         
    Assets.MasterJavascript("~/Areas/Admin/Scripts/LeftEast/Helpers/
DateTimePickerHelpers.js", 
        "LeftEastScripts").Add();
}
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>@ViewBag.Title</title>
    @* Выводим ссылку на группу таблиц стилей *@
    @Assets.RenderCss("CommonStyles")
    
    @* Выводим содержимое секции, в которую 
       представления могут добавлять ссылки 
       на таблицы стилей *@
    @RenderSection("StyleSheets", false)

    @* Выводим ссылку на скрипт HTML5 Shiv *@
    @Assets.RenderJavascript("Html5Shiv")
</head>
<body>
    <div id="wrapper">
        @Html.Partial("_Header")
        <div id="main" class="group">
            <div id="left">
                @Html.Partial("_NavigationMenu")
            </div>
            <div id="content">
                <div id="innerContent">
                    @RenderBody()
                </div>
            </div>
        </div>
        @Html.Partial("_Footer")
    </div>

    @* Выводим ссылки на группы скриптов *@
    @Assets.RenderJavascript("CommonScripts")
    @Assets.RenderJavascript("LeftEastScripts")

    @* Выводим содержимое секции, в которую 
       представления могут добавлять ссылки 
       на скрипты *@
    @RenderSection("Scripts", false)
</body>
</html>

Регистрация и вывод ссылок на ресурсы осуществляется с помощью методов статического класса Assets.

Для регистрации CSS-файлов предназначены методы: Css и MasterCss, а для регистрации JS-файлов: Javascript и MasterJavascript. Методы, названия которых начинаются со слова Master, специально предназначены для регистрации CSS- и JS-файлов в мастер-страницах. Вышеперечисленные методы имеют схожие сигнатуры: в первом параметре метода задается путь к файлу ресурса, а во втором параметре метода задается название группы ресурсов. Второй параметр является необязательным и если его не указать, то ресурс будет входить в группу по умолчанию. Информация о распределении ресурсов по конкретным группам используется при объединении ресурсов, поэтому лучше всегда явно указывать к какой группе относится ресурс, чтобы обеспечить оптимальное кэширование объединенных ресурсов в памяти сервера и кэше браузера. Методы Css и Javascript также имеют третий необязательный параметр onLayoutPage, если задать ему значение равное true, то данные методы будут работать аналогично методам, начинающимся со слова Master. Но вызова вышеперечисленных методов недостаточно для завершения регистрации ресурсов, необходимо еще вызвать метод-расширение Add, после чего ресурс будет окончательно зарегистрирован.

Если вам нужно, чтобы ресурс не проходил обработку (выбор нужной версии файла, минимизация, объединение и т.д.), то до вызова метода-расширения Add нужно вызвать конфигурационный метод IgnoreProcessing. В нашем примере мы отключаем обработку HTTP-хэндлеров JsConstants.ashx и JsLanguage.ashx, которые используются для вывода серверных констант и локализации веб-приложения.

Метод IgnoreProcessing является не единственным конфигурационным методом. Ранее мы уже рассматривали другие конфигурационные методы: ForMediaType (только для CSS), ForIE и WithAlternatePath.

Рассмотрим метод ForIE более подробно. В нашем примере, мы используем скрипт HTML5 Shiv, который обеспечивает поддержку HTML5-тэгов в старых версиях Internet Explorer (ниже 9-й). Нам нужно подключать данный скрипт только для версий IE ниже 9 и для этого можно использовать метод ForIE:

Assets.MasterJavascript("~/Scripts/html5.js", "Html5Shiv")
    .ForIE(IE.Equality.LessThan, IE.Version.IE9)
    .Add()
    ;

В первом параметре нужно указать тип оператора сравнения, а во втором номер версии IE. На выходе мы получим тег script, заключенный в условные комментарии IE:

<!--[if lt IE 9]>
<script src="/Scripts/html5.js" type="text/javascript"></script>
<![endif]-->

Для вывода ссылок на CSS- и JS-файлы предназначены следующие методы класса Assets: RenderCss и RenderJavascript. Данные методы принимают всего один необязательный параметр, в котором указывается название группы. Если не указать название группы, то будут выведены ссылки на ресурсы, входящие в группу по умолчанию. Использование разделения ресурсов на группы позволяет гибко управлять размещением ссылок на CSS- и JS-файлы в HTML-коде веб-страницы. В нашем примере, мы используем рекомендации команды Exceptional Performance: в теге head выводим ссылки на CSS-файлы, а перед закрывающимся тегом body выводим ссылки на JS-файлы. Но ссылка на скрипт HTML5 Shiv является исключением и мы ее выводим в теге head, т.к. нам нужно, чтобы данный скрипт отработал до вывода содержимого. Если вы используете JS-библиотеку Modernizr, которая содержит аналогичную функциональность, то вы должны также выводить ссылку на нее в теге head (только без условных комментариев).

Теперь рассмотрим процесс регистрации ресурсов в представлении на примере кода представления Properties.cshtml:

@using Lucky.AssetManager.Web
@using LeftEast.Blog.Resources
@using LeftEast.Blog.Web.Infrastructure.Helpers

@model LeftEast.Blog.Web.ViewModels.PostViewModel

@* Регистрируем группу скриптов, необходимых 
   для работы визуального редактора *@
@Html.Partial("ScriptGroups/_VisualEditorPartial")
             
@if (!string.IsNullOrWhiteSpace(ViewBag.Message))
{
    <div class="information-message">@ViewBag.Message</div>
}

<h2>@Html.IIf(Model.IsNew, PostStrings.Header_AddNewPost, 
String.Format(PostStrings.Header_EditPost, Model.Post.Title))</h2>

@using (Html.BeginForm())
{
    ...
}

@section Scripts
{
    @* Выводим ссылку на группу скриптов, 
       необходимых для работы визуального 
       редактора *@
    @Assets.RenderJavascript("VisualEditorScripts")

    <script type="text/javascript">
        $(document)
            .ready(function () {
                var $content = $("#content");
                $h.initAllDateTimePickers($content);
                $h.initAllVisualEditors($content);
            })
            .unload(function () {
                var $content = $("#content");
                $h.destroyAllVisualEditors($content);
            })
            ;
    </script>
}

В самом начале кода представления подключается частичное представление _VisualEditorPartial.cshtml, содержащее код регистрации группы JS-файлов, под названием VisualEditor. Использование частичного представления с кодом регистрации ресурсов позволяет упростить использование одной группы ресурсов сразу в нескольких представлениях.

Затем в секции Scripts (объявлена в мастер-странице) мы выводим ссылку на эту группу и производим ненавязчивую инициализацию элементов управления. Таким образом, мы обеспечиваем добавление ссылок на JS-файлы представления после ссылок на JS-файлы мастер-страницы.

Приведу код частичного представления _VisualEditorPartial.cshtml:

@using Lucky.AssetManager.Web

@{
    // Признак, того что приложение запущено 
    // в режиме отладки
    bool isDebugMode = HttpContext.Current.IsDebuggingEnabled;

    // Определяем имя скрипта CKEditor, 
    // соответствующее текущему режиму 
    // работы веб-приложения
    string ckeditorScriptName = isDebugMode ? 
        "ckeditor_source.js" : "ckeditor.js";

    Assets.Javascript(String.Format("~/Areas/Admin/Content/ckeditor/{0}", 
        ckeditorScriptName),
        "VisualEditorScripts").IgnoreProcessing().Add();
    Assets.Javascript(
        "~/Areas/Admin/Scripts/LeftEast/Helpers/VisualEditorHelpers.js", 
        "VisualEditorScripts").Add();
}

Регистрация скриптов необходимых для работы визуального редактора производится с помощью метода Javascript. Для скрипта CKEditor мы отключаем обработку, т.к. он использует нестандартное соглашение об именовании отладочных и минимизированных версий файлов. Также данный JS-файл нельзя объединять с другими JS-файлами, потому что он динамически загружает необходимые для своей работы скрипты и таблицы стилей. В случае попадания в объединенный ресурс, скрипт CKEditor теряет информацию о своем расположении и соответственно не может правильно вычислить пути, по которым расположены динамически подгружаемые ресурсы.

Результат оптимизации

Подведем итоги нашей работы. В процессе клиентской оптимизации веб-приложения мы использовали предварительно минимизированные файлы (минимизированные JS-файлы, идущие в комплекте с JS-библиотеками, и остальные ресурсы, минимизированные с помощью Microsoft Ajax Minifier) и возможности менеджера ресурсов LuckyAssetManager, использующего обработчики и HTTP-хэндлер из LuckyAssetManagerContrib.

Сначала измерим, то, как изменился размер файлов ресурсов. Для этого воспользуемся утилитой YSlow, которая была разработана командой Exceptional Performance компании Yahoo!. На вкладке Statistics утилиты YSlow отображаются круговые диаграммы, которые показывают долю каждого компонента в весе веб-страницы. Рассмотрим диаграммы двух вариантов одной страницы: до оптимизации и после оптимизации (используются предварительно минимизированные версии файлов ресурсов, объединение ресурсов и GZIP-сжатие).

Круговая диаграмма, отображающая размеры файлов компонентов веб-страницы до оптимизации
Рис. 2а. Размеры файлов компонентов веб-страницы до оптимизации

Круговая диаграмма, отображающая размеры файлов компонентов веб-страницы после оптимизации
Рис. 2б. Размеры файлов компонентов веб-страницы после оптимизации

Из диаграмм видно, что суммарный размер JS-файлов сократился почти в 5,7 раза (с 829,9 до 146,4 Кбайт), а CSS-файлов почти в 1,7 раза (c 11,0 до 6,6 Кбайт). Высокий процент сжатия JS-файлов объясняется наличием большого количества комментариев в отладочных версиях JS-библиотек (особенно в Microsoft AJAX).

Теперь посмотрим, как оптимизация отразилась на скорости загрузки компонентов веб-страницы. Для этого воспользуемся информацией с вкладки Network инструментов F12 Developer Tools (встроены в Internet Explorer начиная с 9-й версии). Также как и при анализе размеров файлов компонентов веб-страницы сравним скорость загрузки компонентов для двух версий страницы.

График скорости загрузки компонентов веб-страницы до оптимизации
Рис. 3а. Скорость загрузки компонентов веб-страницы до оптимизации

График скорости загрузки компонентов веб-страницы после оптимизации
Рис. 3б. Скорость загрузки компонентов веб-страницы после оптимизации

Сразу замечу, что измерение скорости загрузки компонентов веб-страницы не может считаться точным, т.к. оно зависит от множества внешних факторов (например, пропускной способности). В данном случае нам важно узнать лишь приблизительный эффект от нашей оптимизации. Из полученных данных видно, что скорость загрузки (учитываем, что ресурсы загружаются параллельно) JS-файлов увеличилась в 5 раз (время загрузки уменьшилось с 312 до 62 миллисекунд), а CSS-файлов — в 2 раза (время загрузки уменьшилось с 32 до 16 миллисекунд).

Заключение

Рассмотренные методики оптимизации JS- и CSS-файлов позволили нам значительно увеличить скорость загрузки веб-страницы. Но возможности клиентской оптимизации на этом не исчерпываются, можно достичь большего эффекта, если еще оптимизировать остальные компоненты веб-страницы: графику и HTML-код.

Ссылки

  1. Best Practices for Speeding Up Your Web Site
  2. Документация к ScriptRegistrar и StyleSheetRegistrar
  3. Документация к Cassette
  4. SquishIt – The Friendly ASP.NET JavaScript and CSS Squisher
  5. Документация к Combres
  6. Документация к LuckyAssetManager
  7. Web.Requre — client script dependency framework
  8. New Bundling and Minification Support (ASP.NET 4.5 Series)

UPD 1: У менеджера ресурсов Cassette изменился тип лицензии. Раньше было 3 типа лицензии (Open Source (GPL v3), 40 USD за сервер (не более 14 серверов) и безлимитная лицензия за 600 USD), а сейчас используется только 1 тип — Open Source (MIT). Разработчик продукта Andrew Davey также принимает добровольные пожертвования на дальнейшее развитие проекта.

UPD 2: Проект LuckyAssetManager переехал с CodePlex на GitHub. Также изменился тип лицензии: теперь вместо лицензии Apache License 2.0 используется MIT License.

UPD 3: В LuckyAssetManagerContrib 1.0.4 был переименован класс для работы с настройками. Теперь код регистрации секции luckyAssetManagerContrib в файле Web.config должен выглядеть следующим образом:

<section name="luckyAssetManagerContrib" 
    type="LuckyAssetManagerContrib.Configuration.AssetManagerContribSettings" 
    allowDefinition="Everywhere" 
    allowLocation="true" />
  1. Nik
    18.02.2012 в 09:42

    Информация относительно лицензирования Cassette неактуальная — сейчас он распространяется под MIT.

    • 18.02.2012 в 10:41

      Nik, спасибо за информацию!

      Данная статья была написана еще в январе (нельзя было публиковать до выхода журнала) и некоторая информация могла за это время устареть. В обзоре я специально указываю какая версия продукта была рассмотрена.

  2. 06.03.2012 в 07:15

    I have been surfing online more than three hours these days, yet I never found any fascinating write-up like yours. It’s fairly worth enough for me. In my opinion, if all website owners and bloggers made great content as you did, the web is going to be much more useful than ever before.

  3. 15.03.2012 в 18:19

    Автору респект, спасибо за статью!

  4. 15.03.2012 в 21:39

    I am not sure where you are getting your info, but good topic. I needs to spend some time learning more or understanding more. Thanks for fantastic info I was looking for this info for my mission.

  5. 23.03.2012 в 16:20

    Вот это да, еще пишут же хорошие статьи, молодец, автору огромный респект.

  6. ardi
    23.05.2013 в 13:17

    Использую luckyAssetManagerContrib, когда разворачиваю на сервере например: serv/app, то вместо абсолютного пути к приложению выводится относительный путь.
    Выводится так: http://serv/assets.axd?type=Javascript&key=-1700467829
    надо так: http://serv/app/assets.axd?type=Javascript&key=-1700467829
    подскажите как исправить?
    еще не нашел минификацию, есть ли возможность ее включить?
    спасибо.

    • 23.05.2013 в 14:13

      Здравствуйте, Ardi!

      По адресу /assets.axd находится HTTP-хэндлер, который возвращает объединенный код ресурсов. Адрес HTTP-хэндлера изменить нельзя (и я не вижу смысла в этом).

      По поводу минификации (минимизации): если Вы внимательно читали данную статью, то должны понимать, что LuckyAssetManagerContrib – это набор расширений для LuckyAssetManager (http://github.com/luckyllama/LuckyAssetManager), который позволяет работать с предварительно минимизированными версиями файлов. Если Вы хотите производить минимизацию «на лету», то просто используйте LuckyAssetManager без LuckyAssetManagerContrib.

      Хочу Вас предупредить, что проект LuckyAssetManager с апреля прошлого года находится в замороженном состоянии и, возможно, больше не будет развиваться. Поэтому рекомендую Вам попробовать связку ASP.NET Web Optimization Framework + Bundle Transformer (на моем сайте есть статьи о них).

  7. ardi
    23.05.2013 в 15:00

    Спасибо, что качается ASP.NET Web Optimization Framework + Bundle Transformer, если к примеру есть js или css файлы в partial view, то как быть?

    • 23.05.2013 в 16:37

      Придется создавать отдельный бандл для CSS- или JS-файлов, и подключать его вместе Partial View (с помощью хелперов Styles.Render и Scripts.Render). К сожалению, Microsoft не оставил нам других вариантов.

  8. Grey
    23.08.2013 в 16:51

    Спасибо. обязательно попробую ваш вариант. Вот только хотелось рассказать о своей ситуации. Хостинг паркинг.ру. Там достаточно часто application pool перезагружается. В первом запросе к сайту будут генерироваться файлы ( насколько я понял хранятся в памяти сервера ). И так постоянно. Интересует возможность расширения ваших наработок, так что б результирующие файлы не пересоздавались после падения application pool и хранились не в памяти а как файлы на хостинге. Можете что то посоветовать?

    • 23.08.2013 в 17:00

      Рекомендую попробовать VS-расширение Web Essentials (http://vswebessentials.com). Оно позволяет минимизировать и объединять файлы, не выходя из Visual Studio.

      Также вы можете попробовать Grunt. У команды портала «Дневник.ру» есть статья на эту тему — http://habrahabr.ru/company/dnevnik_ru/blog/181352/.

  1. No trackbacks yet.

Добавить комментарий

Заполните поля или щелкните по значку, чтобы оставить свой комментарий:

Логотип WordPress.com

Для комментария используется ваша учётная запись WordPress.com. Выход / Изменить )

Фотография Twitter

Для комментария используется ваша учётная запись Twitter. Выход / Изменить )

Фотография Facebook

Для комментария используется ваша учётная запись Facebook. Выход / Изменить )

Google+ photo

Для комментария используется ваша учётная запись Google+. Выход / Изменить )

Connecting to %s

%d такие блоггеры, как: