Оптимизация клиентских библиотек

104

Пакеты оптимизации клиентских библиотек, которые мы рассмотрели в предыдущей статье, удобны в качестве инструмента управления и сопровождения, т.к. они освобождают нас от рутинной работы по поддержанию элементов <script> и <link> в актуальном состоянии. Однако пакеты позволяют предпринять еще один трюк - с их помощью можно оптимизировать контент, отправляемый браузеру. Существуют два способа оптимизации пакетов. Первый - это локальная оптимизация, а второй - применение сети доставки контента (Content Delivery Network - CDN). Мы объясним оба способа в последующих разделах, но прежде чем делать это, мы более подробно рассмотрим стандартное поведение пакетов.

По умолчанию содержимое файлов, указанных в каждом элементе <script> и <link>, запрашивается индивидуально через отдельные сетевые подключения. Браузеры поддерживают фиксированное количество параллельных сетевых запросов, из-за чего часто приходится ожидать загрузки файла сценариев или таблицы стилей, прежде чем веб-сайт отобразится в браузере. Браузер делает только ограниченное число параллельных запросов к одному веб-сайту, обычно равное шести. В этом можно удостовериться, запустив приложение и активизировав инструменты <F12> браузера.

Перейдите на вкладку Network (Сеть) для отображения профилировщика сети. Профилировщик отобразит детали сетевых подключений, которые были созданы для получения файлов HTML, CSS и JavaScript, требуемых для визуализации веб-формы, как показано на рисунке ниже. (В зависимости от конфигурации и производительности вашей машины разработки могут быть получены несколько отличающиеся результаты.)

Профилирование запросов браузера, требуемых для визуализации веб-формы

Как видно на рисунке, браузеру понадобилось выполнить 24 запроса для загрузки 728 Кб данных. Эти запросы завершились быстро, но причина в том, что браузер и приложение функционируют на одном устройстве, на котором доступен большой объем памяти и не делается какая-то другая работа. В случае подключения через Интернет накладные расходы будут более существенными.

Многие из 24 запросов относились к файлам CSS для библиотеки jQuery UI, которые были добавлены в целях демонстрации работы пакетов, и веб-форме они в действительности не нужны. Тем не менее, количество запросов по-прежнему мало по сравнению с реальными проектами, в которых веб-формы нередко содержат 100 и более файлов, особенно когда используются неупакованные решения наподобие шаблонов управления контентом.

Использование локальной оптимизации

Локальная оптимизация означает, что контент минимизируется и объединяется, чтобы требовать для своей загрузки меньшее количество запросов. Чтобы включить локальную оптимизацию, мы модифицируем файл Web.config, изменяя атрибут debug в элементе system.web/compilation на false. Это указывает, что приложение должно вести себя так, как после развертывания. (Упомянутый атрибут изменяется автоматически при развертывании приложения.) Изменение представлено в примере ниже:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <system.web>
    <compilation debug="false" targetFramework="4.5.1" />
    <httpRuntime targetFramework="4.5.1" />
  </system.web>
  ...
</configuration>

Изменение атрибута debug оказывает более широкое влияние, чем просто включение оптимизаций пакетов - это также приводит к включению оптимизаций компилятора и удалению символов отладчика Visual Studio. Если нужно активизировать оптимизации пакетов, не изменяя остальное, можно установить статическое свойство BundleTable.EnableOptimizations в true. Обычно это делается в глобальном классе приложения.

Снова запустите приложение (выбрав пункт Start Without Debugging (Начать без отладки) в меню Debug среды Visual Studio) и повторите профилирование запросов, выполненных браузером. Результат от включенных оптимизаций показан на рисунке ниже:

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

Браузер сделал 6 запросов для загрузки 178 Kb данных, т.е. значительно уменьшился как объем передаваемых данных (за счет использования минимизированного контента), так и количество сетевых запросов (за счет объединения содержимого множества файлов).

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

Исправление проблемы дублирования файлов

Первая проблема, которую придется решить, связана с тем, что включение оптимизаций отключает средство обнаружения ссылок на файлы во множестве пакетов. Это можно заметить в профилировщике при запросе файлов JavaScript по двум URL:

/bundle/jquery?v=cRpZpqaBtXGD5K6oveCrw6zWYGnzQmjc9FSFuRkN9OM1
/bundle/jqueryui?v=tNjSByHblyunjhDAi7ICDaSfjSbXY7DWtE5tEEr_C6U1

Показанные URL ссылаются на имя пакета и содержат уникальный идентификатор версии, который позволяет контенту кешироваться браузером, не препятствуя развертыванию более новых версий пакетов. В рассматриваемом примере это означает, что минимизированное содержимое файла jQuery отправляется браузеру дважды - один раз сам по себе и еще один раз в виде объединения с минимизированным контентом файла библиотеки jQuery UI.

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

...
<%: System.Web.Optimization.Scripts.Render("~/bundle/jqueryui") %>
...

Чтобы увидеть результат изменения, запустите приложение и выполните профилирование заново:

Устранение дублирования контента из оптимизированных пакетов

Благодаря такому изменению, браузеру нужно делать только 5 запросов для загрузки 141 Kb данных.

Кроме излишнего расхода полосы пропускания дублирование файлов JavaScript может привести к более серьезным проблемам. Порядок загрузки файлов JavaScript важен из-за того, что зависимости между библиотеками полагаются на глобально доступные переменные и функции. При наличии дублированных файлов, так что порядок следования библиотек выглядит как jQuery, jQuery UI и снова jQuery, второй файл jQuery сбросит глобальные переменные, с которыми работает библиотека jQuery UI, и это приведет к ошибке. Включая локальную оптимизацию, тщательно тестируйте приложение, а не просто полагайтесь на то, что все будет работать после развертывания.

Исправление проблемы относительного местоположения изображений

Внимательно просмотрев результаты профилирования для оптимизированных пакетов, вы заметите, что последний запрос завершился с кодом состояния 404, который свидетельствует о том, что запрошенный файл не был найден. Этот запрос касался файла изображения, которое библиотека jQuery UI использует для добавления визуального эффекта к кнопкам. Чтобы увидеть это, сравните кнопки, отображаемые браузером с включенными и отключенными оптимизациями. Когда упомянутый файл изображения не доступен, кнопки отображаются с одним сплошным цветом:

Результат недоступности файла изображения для кнопок jQuery UI

Проблема возникает из-за того, что библиотека jQuery UI пытается загрузить файл изображения относительно местоположения файла CSS, а локальная оптимизация изменила URL, применяемый для запроса контента пакетов. Вот как выглядит элемент <link>, который использовался при запрашивании файла CSS для jQuery UI:

<link href="/bundle/jqueryUICSS?v=IfKWRhytSz8OG0BC4OBAnF3VTbrsxIWjj6ZXUW6VGWw1"
	rel="stylesheet"/>

Контент объединенных таблиц стилей основан на имени пакета, а не на местоположении файла. В результате попытка библиотеки jQuery UI загрузить файл изображения с применением URL вроде показанного ниже ни к чему не приводит:

http://localhost:32160/bundle/images/ui-bg_glass_75_dadada_1x400.png

Простейший способ исправить это предусматривает изменение URL, который создан для пакета, позволяя jQuery UI находить свои файлы изображений, как показано в примере ниже:

using System;
using System.Web.Optimization;
using System.Web.UI;

namespace ClientDev
{
    public class BundleConfig
    {
        public static void RegisterBundles(BundleCollection bundles)
        {
            // ...
            Bundle jqueryUIStyles = new StyleBundle("~/Content/themes/base/jqueryUICSS")
                    .IncludeDirectory("~/Content/themes/base", "*.css");

            // ...
        }
    }
}

Мы изменили URL пакета для соответствия с местоположением файлов на диске. Это обеспечивает работу запросов к файлам, сделанных относительно URL пакета. В примере ниже демонстрируется применение нового URL пакета к веб-форме Default.aspx:

...
<head runat="server">
    <title></title>
    <%: System.Web.Optimization.Styles.Render("~/bundle/basicCSS", 
        "~/Content/themes/base/jqueryUICSS") %>
    ...
</head>
...

Вновь запустив приложение и выполнив профилирование, вы увидите, что браузер находит файл изображения и корректно отображает кнопки.

Насколько широко должна применяться локальная оптимизация?

С помощью оптимизации пакетов нам удалось снизить количество запросов до пяти, но можно было бы продолжить, поскольку два запроса относятся к контенту CSS, который может быть объединен в единственный пакет (и, следовательно, в один сетевой запрос).

В реальном проекте мы останавливаемся в этой точке и принимаем тот факт, что веб-форма требует выполнения двух запросов для таблиц стилей CSS. Причина в том, что стоит только выйти за рамки создания пакетов, представляющих четкие функциональные блоки (специальные стили CSS для веб-формы, стили CSS для jQuery UI и т.д.), как очень быстро окажется, что вы создаете и сопровождаете пакеты сценариев и стилевых таблиц для каждой веб-формы в приложении. Это дает лучшие результаты в плане оптимизации, но возвращает те же самые проблемы управления и сопровождения, о которых говорилось ранее.

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

Использование сетей доставки контента CDN

Совершенно другой вид оптимизации заключается в применении сети доставки контента (Content Delivery Network - CDN), когда контент загружается из серверов, которые управляются компаниями вроде Microsoft и Google и размещаются у поставщиков услуг Интернета, находящихся поблизости к клиенту.

Сети CDN хранят только широко используемые библиотеки JavaScript, но их преимущество в том, что файлы будут загружаться без потребления вашей полосы пропускания. Поскольку многие веб-сайты пользуются тем же самым набором основных библиотек (jQuery, jQuery UI и т.д.), браузер пользователя может уже располагать кешированными версиями этих библиотек из других приложений. Даже если браузер должен загрузить файл, сервер CDN находится близко к пользователю. На запросы к сети CDN не распространяется предел параллельных запросов к серверам приложений, что может ускорить передачу данных, требуемых браузеру для отображения нужного контента.

В примере ниже показано добавление поддержки обращения к CDN за пакетом jQuery UI:

using System;
using System.Web.Optimization;
using System.Web.UI;

namespace ClientDev
{
    public class BundleConfig
    {
        public static void RegisterBundles(BundleCollection bundles)
        {
            Bundle jquery = new ScriptBundle("~/bundle/jquery");
            jquery.CdnPath = 
                "http://ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js";
            jquery.Include("~/Scripts/jquery-{version}.js");

            Bundle jqueryui = new ScriptBundle("~/bundle/jqueryui");
            jqueryui.CdnPath = 
                "https://ajax.googleapis.com/ajax/libs/jqueryui/1.11.2/jquery-ui.min.js";
            jqueryui.Include("~/Scripts/jquery-{version}.js", "~/Scripts/jquery-ui-{version}.js");

            Bundle basicStyles = new StyleBundle("~/bundle/basicCSS")
                    .Include("~/MainStyles.css", "~/ErrorStyles.css");

            Bundle jqueryUIStyles = new StyleBundle("~/Content/themes/base/jqueryUICSS");
            jqueryUIStyles.CdnPath =
                "https://ajax.googleapis.com/ajax/libs/jqueryui/1.11.2/themes/smoothness/jquery-ui.min.css";
            jqueryUIStyles.IncludeDirectory("~/Content/themes/base", "*.css");

            bundles.UseCdn = true;

            bundles.Add(jquery);
            bundles.Add(jqueryui);
            bundles.Add(basicStyles);
            bundles.Add(jqueryUIStyles);
        }
    }
}

При использовании сети CDN мы избрали другой подход, определив пакет для каждой библиотеки JavaScript, которая будет применяться. Это связано с тем, что для получения контента пакета из сети CDN должен предоставляться одиночный URL, а в сетях CDN размещаются индивидуальные файлы (по-видимому, для увеличения шансов хеширования контента).

Используемый URL для CDN указывается с помощью свойства CdnPath. Мы применили сеть Google CDN, которая проста и бесплатна. (Подробные сведения и размещаемых библиотеках и доступных версиях можно получить по адресу Google Hosted Libraries.)

Также необходимо установить свойство BundleCollection.UseCdn в true, чтобы включить средство CDN. В примере ниже оба пакета сценариев используются для получения файлов библиотек jQuery и jQuery UI:

...
<head runat="server">
    <title></title>
    <%: System.Web.Optimization.Styles.Render("~/bundle/basicCSS", 
        "~/Content/themes/base/jqueryUICSS") %>
    <%: System.Web.Optimization.Scripts.Render("~/bundle/jquery", "~/bundle/jqueryui") %>
    <script>
        $(document).ready(function () {
            $('input[type=submit]').button();
        });
    </script>
</head>
...

Благодаря внесенным изменениям, файлы JavaScript загружаются из сервера ASP.NET, когда оптимизация пакетов отключена, и через указанные URL, когда оптимизация пакетов включена, что можно видеть в результате профилирования запроса:

Использование сети CDN для загрузки файлов JavaScript

Браузер по-прежнему выполняет то же количество запросов, но файлы jQuery и jQuery UI поступают из сети Microsoft CDN. Это, по крайней мере, сузит полосу пропускания, требуемую для функционирования приложения. Кроме того, есть неплохой шанс, что пользователь получит лучше реагирующий интерфейс, т.к. контент будет поступать из сервера, являющегося локальным для поставщика услуг Интернета.

Сети CDN могут быть полезны, однако они сводят на нет ряд преимуществ в плане управления, которые предлагают пакеты. Отсутствует возможность сопоставления версий библиотек JavaScript, используемых локально, и библиотек, получаемых из сети CDN. Это означает необходимость ручного обновления применяемых URL для CDN, когда обновляются пакеты NuGet, или тестирования двух версий используемых библиотек. В случае работы с сетями CDN обеспечьте серьезное тестирование и синхронизацию локальных файлов и удаленных URL.

Обеспечение доступности библиотек для элементов управления

Для работы ряда встроенных элементов управления требуется библиотека jQuery: это касается и сложных элементов управления данными, которые были описаны в разделе Основы ASP.NET. Средство упаковки ASP.NET не располагает какой-либо поддержкой для управления требованиями к библиотекам сценариев со стороны элементов управления. В такой ситуации в Microsoft полагаются на более старый подход, предусматривающий применение классов ScriptManager и ClientScriptManager для обеспечения доступности библиотеки jQuery.

Классы ScriptManager и ClientScriptManager пытаются гарантировать, что элементы управления не генерируют дублированные элементы <script> для необходимых файлов. Они делают это за счет требования предварительной координации между элементами управления и веб-формами для определения файлов, которые будут использоваться. В итоге исчезает ряд преимуществ, связанных с применением элементов управления, и снова появляются проблемы с управлением, которые пакеты стараются предотвратить. Классы ScriptManager и ClientScriptManager не были интегрированы со средством упаковки. Другими словами, нам нужен способ установления связи между ожиданиями элементов управления данными и пакетами, которые определены в приложении.

К счастью, элементы управления данными не являются слишком жесткими при проверке доступности jQuery с применением класса ClientScriptManager. Мы можем добавить в файл App_Start\BundleConfig.cs простой оператор, который удовлетворят требованиям элементов управления:

using System;
using System.Web.Optimization;
using System.Web.UI;

namespace ClientDev
{
    public class BundleConfig
    {
        public static void RegisterBundles(BundleCollection bundles)
        {
            // ...

            ScriptManager.ScriptResourceMapping.AddDefinition("jquery",
                new ScriptResourceDefinition { Path = "~/bundles/jquery" });
        }
    }
}

Хотя класс ScriptManager не был интегрирован в средство упаковки, он модифицирован так, чтобы не добавлять элементы <script> для файлов, которые включены в пакеты. Тем не менее, мы получали несогласованные результаты, и этот класс вообще не работает при отключенной оптимизации пакетов. Соблюдайте осторожность, чтобы не получить множество версий файла jQuery внутри HTML-разметки.

В случае специальных элементов управления мы рекомендуем пробовать включать элементы <script> либо напрямую, либо через пакеты. Не существует хорошего способа согласования имен файлов и версий. Довольно быстро можно создать ситуацию, когда клиенту отправляются разные версии одной и той же библиотеки. Взамен мы предпочитаем проверять, загрузила ли веб-форма библиотеку, необходимую для элемента управления, и сообщать об ошибке, если это не было сделано. Такая ошибка обнаруживается во время разработки и тестирования, после чего соответствующий файл JavaScript добавляется в пакеты, используемые веб-формой.

В примере ниже представлен простой пример пользовательского элемента управления под названием SimpleUserControl.ascx, который требует для своей работы библиотеку jQuery:

<%@ Control Language="C#" AutoEventWireup="true" CodeBehind="SimpleUserControl.ascx.cs" 
    Inherits="ClientDev.SimpleUserControl" %>

<script>
    if (jQuery) {
        $(document).ready(function () {
            $('#nameSpan').text("Simple User Control");
        });
    } else {
        throw new Error("Требуется библиотека jQuery");
    }
</script>

<div>
    Сообщение из элемента управления <span id="nameSpan"></span>
</div>

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

<%@ Page Language="C#" AutoEventWireup="true" 
    CodeBehind="Default.aspx.cs" Inherits="ClientDev.Default" %>
<%@ Register Src="~/SimpleUserControl.ascx" TagPrefix="cc" TagName="SUC" %>

...
<body>
    <form id="form1" runat="server">
        ...
    </form>
    <cc:SUC runat="server" />
</body>
</html>
Проверка наличия библиотеки jQuery

Добавление поддержки версий для CDN

В завершение статьи мы покажем, как создать специальный пакет сценариев, который поддерживает конструкции {version} для URL в CDN, а также в локальных файлах сценариев. В примере ниже приведено содержимое созданного файла класса по имени CdnScriptBundle.cs:

using System.Linq;
using System.Text.RegularExpressions;
using System.Web;
using System.Web.Optimization;

namespace ClientDev
{
    public class CdnScriptBundle : ScriptBundle
    {
        public CdnScriptBundle(string path)
            : base(path)
        { }

        public Bundle CdnInclude(string filePath, string cdnPath)
        {
            Bundle result = base.Include(filePath);

            BundleContext ctx = new BundleContext(
                new HttpContextWrapper(HttpContext.Current),
                BundleTable.Bundles, Path);

            Regex regexp = new Regex(@"(\d+(?:\.\d+){1,3})", RegexOptions.IgnoreCase);
            string version = regexp.Match(EnumerateFiles(ctx).First().VirtualFile.Name).Value;
            CdnPath = cdnPath.Replace("{version}", version);
            return result;
        }
    }
}

На момент написания этих строк исходный код сборки System.Web.Optimization еще не был опубликован Microsoft, но в Microsoft пообещали вскоре сделать это (хотя данное обещание не было исполнено в течение определенного времени). С помощью декомпилятора мы выяснили, как работает средство упаковки (нам нравится инструмент .NET Reflector от Red Gate, но доступны и другие инструменты подобного рода), и создали класс, унаследованный от ScriptBundle.

Класс ScriptBundle и класс Bundle, от которого он унаследован, написаны без расчета на расширяемость, поэтому для получения желаемого поведения нам пришлось поступить несколько нестандартно. Мы определили метод CdnInclude(), который позволяет базовому классу иметь дело с конструкцией {version} и сопоставлять ее с физическим файлом. Это используется для извлечения номера версии и вставки его внутрь URL в сети CDN, который передается методу CdnInclude(). Применение этого нового класса пакета демонстрируется в примере ниже, в котором показано содержимое файла BundleConfig.cs:

using System;
using System.Web.Optimization;
using System.Web.UI;

namespace ClientDev
{
    public class BundleConfig
    {
        public static void RegisterBundles(BundleCollection bundles)
        {
            Bundle jquery = new CdnScriptBundle("~/bundle/jquery")
               .CdnInclude("~/Scripts/jquery-{version}.js",
                   "http://ajax.aspnetcdn.com/ajax/jQuery/jquery-{version}.min.js");

            // ...
        }
    }
}

Мы определяем локальный файл как "~/Scripts/jquery-{version}.js", что соответствует физическому файлу jquery-2.1.3.js. Номер версии этого файла затем применяется к аргументу URL, так что сети CDN будет передан запрос http://ajax.aspnetcdn.com/ajax/jQuery/jquery-2.1.3.min.js, сохраняя локальную версию и версию из CDN библиотеки jQuery в синхронизированном состоянии.

Соблюдайте осторожность при использовании этого класса в своих проектах. Он предполагает, что на сервере CDN имеется версия файла библиотеки, которая соответствует версии файла, установленной локально. После выхода новой версии обновление серверов CDN может занять несколько дней, и в течение этого времени наш специальный класс пакета будет запрашивать несуществующий URL.

Пройди тесты
Лучший чат для C# программистов