Статьи Архив статей

Перевод: Мациевский Николай aka sunnybear
Опубликована: 8 марта 2009

Разгоняем ASP.NET: 100 баллов и оценка "A" в YSlow

Примечание: ниже расположен перевод статьи YSlow and ASP.NET: 100 points "A" grade is possible. Автор (Viktar Karpach) рассматривает советы от Yahoo! в области клиентской производительности и возможность их применения для ASP.NET. Как и в случае с Drupal, ценны не столько сами правила, сколько общий ход рассуждений. Мои комментарии далее курсивом.

Если кто-то еще не в курсе насчет YSlow, то это дополнение к Firefox, встроенное в Firebug. Оно анализирует веб-страницы и подробно рассказывает, почему они медленно загружаются. Также можно запустить его для вашего проекта и увидеть свою оценку. Можно произвести некоторые незначительные улучшения и легко получить оценку "D". Однако чтобы добиться лучшей оценки, придется реально постараться (real challenge). Ниже я описываю, как я добился выставления оценки "A" (100 баллов) для своего приложения-блога.

Перво-наперво нужно исключить из оценки производительности все внешние скрипты и информеры, которые не находятся под вашим контролем, например, Google Ads, Google Analytics, Dotnetkicks. На самом деле, это неверный подход, потому что не позволяет контролировать производительность страницы в присутствии этих скриптов. Более грамотный подход должен включать вынесении их на сам сервер, как, например, описано в статье про ускорение Drupal или ускорение счетчиков. Поэтому стоит добавить такую возможность с помощью параметра в строке запроса, который будет прятать или отключать описанные модули. В моем случае это PageType=NoExternal, а полный URL выглядит следующим образом: http://www.karpach.com/default.aspx?PageType=NoExternal. Также нужно добавить ваш сайт в настройки CDN для YSlow. Мой персональный CDN расположен по адресу http://karpach.appspot.com/cdn/. Теперь если вы измерите производительность моей страницы default.aspx?PageType=NoExternal, то ее оценка будет A (100). Давайте теперь разберемся, как этого добиться.

1. Меньше HTTP-запросов

Во-первых, нужно объединить в один файлы таблицы стилей и файлы скриптов. Это стоит сделать только для рабочего (не тестового) выпуска сайта при помощи MS Build:

<ItemGroup>
    <TextFiles Include="*.css" Exclude="global.css"/>
</ItemGroup>
<Exec Command="echo y| type %(TextFiles.Identity) >> global.css"/>

Таким образом мы можем объединить все CSS- и JS-файлы.

Некоторые разработчики зададут резонный вопрос: а что по поводу WebResource.axd? В новом AJAX Control Toolkit есть ToolkitScriptManager, который позволяет объединить большинство ваших файлов WebResource.axd. Также он может применять к ним gzip-сжатие (это будет важно для следующих разделов).

Затем нам нужно создать CSS Sprites. Можно ознакомиться со статьей CSS Sprites: Image Slicings Kiss of Death (или же CSS Sprites: все, что вы знали, но боялись спросить), чтобы лучше понимать, что де это вообще такое. В статье не объясняется, какие образом создавать ресурсные изображения при повторяющемся фоне (в статье на русском очень даже объясняется и даже глубже, чем изложено далее). Идея заключается в том, чтобы группировать такие изображения по оси повторения. Например, все изображения, повторяющиеся по вертикали, сложить в vbackground.gif (о ужас, тут используется GIF? Срочно все переходим на PNG), а все повторяющиеся по горизонтали — в hbackground.gif. Затем можно использовать

background-position: -OffsetPixels 0

для позиционирования внутри вертикального файла и

background-position: 0 -OffsetPixels

для горизонтального. Ниже приведен пример CSS Sprites из моего блога (я осмелился заменить его на собственный пример, ибо приведенный вариант в авторской статье не работал):

2. Используем CDN (Content Delivery Networks)

Сети доставки содержания (англ. CDN, Content Delivery Network стоят довольно дорого. Я попытался найти какой-нибудь дешевый аналог и наткнулся на следующую статью 10 Easy Steps to use Google App Engine as your own CDN. Естественно, придется немного попотеть, прежде чем собрать из Google App Engine свою идеальную CDN.

Сначала нужно увеличить время кэширования для ресурсов. По умолчанию это 600 секунд. Однако YSlow предлагает увеличить это значение минимум до 7 дней. Итак в вашем app.yaml нужно добавить сразу после api_version следующую строку:

default_expiration: "7d"

Дополнительно можно использовать msbuild для изменения файлов стилей при использовании CDN. Ниже приведен мой рабочий пример (в нем пути с ../images/ заменяются на http://karpach.appspot.com/cdn/images/):

<Import Project=".\References\MSBuild.Community.Tasks.targets" />
<Target Name="Release">
    <FileUpdate Files="$(OutputPath)styles\basic.css" Regex="\.\.\/images/([^\)]*)" ReplacementText="http://karpach.appspot.com/cdn/images/$1" />
</Target>

Теперь нужно положить в CDN все ваши изображения, файлы стилей и скриптов. Здесь как раз и всплывает проблема сброса кэша. что будет, если пользователь зашел к вам на сайт как раз перед тем, как вы выложили новую версию, в которой изменился файл стилей и некоторые изображения? По-видимому, этот пользователь будет видеть со старыми стилями и картинками еще 7 дней, и он просто не увидит изменений, пока не истечет срок действия кэша в его браузере. Это не есть хорошо. Если озвученный вопрос становится актуальным, то стоит посмотреть, что Yahoo! сделали на своем вебсайте. В названии картинки они «зашивают» метку даты и версию. Например, trough_2.0_062308.gif. Это выглядит разумно. Если вы изменяете какой-либо из ваших CSS Sprites, то его нужно вручную переименовать при помощи новой даты и версии (на самом деле, процесс переименования файлов должен быть заложен в сам инструмент создания CSS Sprites).

Однако, по всей видимости, этот подход может быть неприменим для файлов стилей. Для начала стоит обеспечить контроль изменений версий таблиц стилей (в моем случае это SVN). Для картинок это не играет существенной роли, потому что они меняются не так часто. Основная идея заключается в том, что у нас может быть один физический файл (например, basic.css), а на странице использоваться адрес другого файла (например, basic_14102262.css), где 14102262 — это текущая версия приложения. Затем мы можем использовать технологию url rewrite, так что basic_14102262.css будет указывать на basic.css. Пользователь с закэшированной версией basic_14102261.css будет вынужден загрузить новую версию basic_14102262.css, потому как изменилось имя файла.

Движок Google Apps позволяет легко реализовывать rewrite для URL. Для этого можно просто изменить ваш app.yaml следующим образом:

- url: /cdn/styles/basic_\d*\.css
  static_files: cdn/styles/basic.css
  upload: cdn/styles/basic\.css

Более подробно о настройке CDN от Google рассказывается в соответствующей заметке.

3. Добавляем заголовок Expires

У всех файлы, расположенных в CDN, уже выставлен этот заголовок. Для всех остальных я создал HttpModule:

private readonly static string[] CACHED_FILE_TYPES = new string[] { ".jpg", ".gif", ".png",".css" };

public void Init(HttpApplication context)
{
    context.AcquireRequestState += new EventHandler(context_AcquireRequestState);
}

void context_AcquireRequestState(object sender, EventArgs e)
{
    HttpContext context = HttpContext.Current;
    if (context != null && context.Response != null)
    {
	string fileExtension = Path.GetExtension(context.Request.PhysicalPath).ToLower();
	if (context.Response.Cache != null && Array.BinarySearch<string>(CACHED_FILE_TYPES, fileExtension) >= 0)
	{
	    HttpCachePolicy cache = context.Response.Cache;
	    HttpCachePolicy cache = context.Response.Cache;
	    cache.SetCacheability(HttpCacheability.Public);
	    cache.SetExpires(DateTime.Now.Add(duration));
	    cache.SetValidUntilExpires(true);
	    cache.SetNoServerCaching();
	    cache.SetMaxAge(duration);
	}
    }
}

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

4. Применяем для компонентов gzip

Для этого лучше всего ознакомиться со статьей Building a GZip JavaScript Resource Compression Module for ASP.NET, в ней подробно рассказывается, как создать свой gzip HTTP-модуль. Но стоит также помнить о том, что в IE6 нет 100% поддержки gzip.

5. Располагаем CSS-файлы в начале страницы

Это делается очень просто. Просто надо ввести в привычку так делать. Помните об этом при разработке модулей на стороне сервера и используйте коллекцию Header.Controls для добавления таблиц стилей на страницу.

6. Располагаем JS-файлы в конце страницы

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

Например:

Library.js

function DoSomething()
{

}

А после этого прямо в коде страницы:

<script>
    if (typeof (DoSomething) == 'undefined')
    {
	alert('Library is not loaded yet');
    }
</script>

Приведенный пример не очень корректен, потому что проверять нужно с определенным интервалом: ведь исходный участок кода может выполниться просто раньше, чем загрузилась библиотека. А если немного подождать и проверить снова — то библиотека может быть и доступна.

7. Избегаем CSS-выражений

CSS-выражения работают только в IE, поэтому на них нельзя закладываться при построении кроссбраузерного приложения. Единственное хорошее их применение — это эмуляция min-width / min-height для IE6. Наиболее ярким примером все же будет эмуляция PNG-прозрачности. Подробнее о CSS-выражениях и их оптимизации.

8. Выносим CSS- и JS-код во внешние файлы

Это часто имеет значение (когда одни и те же стили и скрипты используются для всего сайта). Но также стоит ознакомиться с тем, что предлагает конкретно Yahoo! — https://webo.in/articles/habrahabr/15-yahoo-best-practices/#external

9. Уменьшаем число DNS-запросов

Для этого стоит скопировать внешние ресурсы (картинки, JavaScript-файлы) к себе на сайт.

Например, на блоге располагается иконка валидной страницы от W3. Изначально эта иконка располагалась на сайте W3 Schools, но я скопировал ее в свой проект. Теперь она располагается локально, а при загрузке сайта происходит на 1 DNS-запрос меньше.

10. Уменьшаем JS

Минимизацию JS-кода можно осуществлять при публикации очередного выпуска сайта при помощи MS Build (эт наиболее разумная позиция — осуществлять все оптимизационные процедуры при превращении сайта из тестового в рабочий). Для этой цели я использую YUI compressor. Ниже приведен вариант скрипта для MS Build.

<Target Name="Compress">
    <Message Text="Create temp files ..." />
    <Copy SourceFiles=".\$(ProjectName)\Javascript\ColorPicker.js" DestinationFiles=".\$(ProjectName)\Javascript\ColorPicker.js.full"/>
    <Copy SourceFiles=".\$(ProjectName)\Styles\ColorPicker.css" DestinationFiles=".\$(ProjectName)\Styles\ColorPicker.css.full"/>
    <Exec Command="java -jar yuicompressor-2.4.2.jar --type js .\$(ProjectName)\Javascript\ColorPicker.js.full >.\$(ProjectName)\Javascript\ColorPicker.js"/>
    <Exec Command="java -jar yuicompressor-2.4.2.jar --type css .\$(ProjectName)\Styles\ColorPicker.css.full >.\$(ProjectName)\Styles\ColorPicker.css"/>
</Target>

11. Избегаем редиректов

У вас не должно быть с этим проблем. Просто прочитайте, что советуют инженеры Yahoo!.

12. Удаляем дублирующиеся скрипты

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

13. Настройте ETag

Кэширующий модуль и CDN должны решить эту проблему. Но стоит также иметь в виду, что в ASP.NET есть специальный метод для ETag:

Response.Cache.SetETag

Это уже вторая статья на тему оптимизации определенной CMS / сервера. И уже во второй раз большинство проблем решается правильной настройкой CDN или выдачи статических файлов.

Читать дальше

Все комментарии (habrahabr.ru)