Как стать автором
Обновить

Ускорение загрузки AJAX приложения, + предзагрузка изображений

Время на прочтение 10 мин
Количество просмотров 5.9K
Всё началось с создания сложного AJAX приложения с применением java технологий GWT, GXT, Spring, Hibernate, Terracota, AndroMDA, ActiveMQ и множеством других волшебных звуков за которыми прячется вся мощь и могущество java технологий создаваемых десятками тысяч гениальнейших программистов уже второе тысячелетие подряд …

Но статья не об этом. Требовалось решить скромную, но очень интересную задачу, — не осилив всю технологичность, продуманность и совершенство решений на базе платформы java, снизить время загрузки клиентской части приложения.

Входные данные: размер текстовых данных js, css, xml, html, images коло ~1,2MБ(+ флэш), время загрузки в Москве более полутора минут, при хождении по ссылкам заметное время(1-15 секунд) на загрузку картинок, при повторном обращении картинка загружалась повторно.

Данные приблизительные по следующим соображениям
1)GWT для каждого браузера генерирует свой js размер которого около 700КБ
2)Сначала была сделана оптимизация а потом пришла идея, что это может быть полезно сообществу

Как выяснилось, для меня страшного ни чего не было, это глаза боялись, а руки смело делали и переделывали своё дело, но обо всём по порядку.

Сжатие контента средствами web сервера
Сервером оказался Tomcat. Файл настроек был интуитивно понятен, сделан с соблюдением формата xml и всё в нём было хорошо, сжатие было включено, был включен эффективный nio коннектор — но этот чудесный по всем описаниям коннектор не желал использовать компрессию.
Смена коннектора на идущий по умолчанию HTTP/1.1, заставила компрессию чудесными образом заработать. В голове поселилась мысль, как так, в старом добром коннекторе есть компрессия а в дите высоких технологий — нет? — тут что-то не так.
В процессе изучения документации по томкату выяснилось что компрессия есть. Также в документации оказалось ещё несколько любопытных опций позволяющих сжимать файлы по различным признакам таким как размер и тип файла, также можно указать качество компрессии.
Компрессия тем не менее от полученного знания не заработала.
Дальнейшие размышления о глубине, широте и высокой интегрированности java платформы подсказали мне, что возможно приложение может самостоятельно конфигурировать сервер под свои нужды.
Было принято решение проверить это предположение задав вопрос разработчикам, — как выяснилось java программисты действительно отключили эту функцию у всего приложения для nio коннектора так как она мешала работе чата.
Дальше в этом направлении всё шло предсказуемо. Java девелоперы пока очень заняты и не поправили багу. Компрессия работает с HTTP/1.1 а в файле конфигурации красуется закоментированный nio коннектор и 700КБ js файл стал весть уже 190КБ и это воспринималось как гора с плеч.

Кусочек конфигурационного файла настраивающего коннектор.
   <Connector port="8080" protocol="HTTP/1.1"
        maxThreads="64000" 
        connectionTimeout="20000"
        redirectPort="8443"
        proxyPort="80"
        compression="on"
        compressionMinSize="4000"
        noCompressionUserAgents="gozilla, traviata"
        compressableMimeType="text/html,text/xml,text/javascript,text/css" />

  <!--Connector port="8080" compression="on"
        protocol="org.apache.coyote.http11.Http11NioProtocol"
        connectionTimeout="20000"
        backlog="500"
        maxThreads="4"
        redirectPort="8443"
        proxyPort="80"
        compressionMinSize="2048"
        noCompressionUserAgents="gozilla, traviata"
        compressableMimeType="text/html,text/xml,text/javascript,text/css,application/x-javascript"
        socketBuffer="64000"/-->

* This source code was highlighted with Source Code Highlighter.
Думаю сам кусочек конфига будет полезен очень малому количеству пользователей, но знание возможности и подход к решению вопроса должен быть полезен большинству.

Отключение лишних файлов
Вторым делом было решено посмотреть что за js грузятся в таком количестве
Их грузилось около 10 штук и порядка 7 из них не относилось GWT ни каким образом. Как водится любопытство взяло верх и все непонятные js были отключены. Каково же было удивление, когда после чистки кэша и перезагрузки страницы всё прекрасно работало (в firefox 3.1 под Linux). Об этом факте было сообщено разработчикам приложения, с вопросом а действительно ли всё это используется? Разработчики после недолгого размышления оставили только один их предложенных мною к удалению файлов, сообщив что этот один необходим для IE. Тем не менее выкидывание 6 лишних запросов и порядка 70КБ жатого js кода существенно сокращало время загрузки.

Объединение файлов
Также была изучена возможность сливания файлов js в один файл, данная возможность оказалась не удобной на данном этапе развития проекта так как новые билды идут иногда по 3 за день, а возможность сливания файлов была ограничена двумя файлами несущественного размера. Возможно это будет сделано поже по средством простого скриптика на bash поставленного в крон :) `cat file1 file2 > file3; mv file 3 file1; egrep -v -o «file2» file.html > tmp.file.html; mv tmp.file.html file.html`, ну или как то так с необходимыми проверками и резервными копиями.

Сжатие js средствами yuicompressor
developer.yahoo.com/yui/compressor
С целью ещё более сильного уменьшения был изучен оптимизатор от Yahoo YUI он действительно сокращал, пусть и несущественно, размер несжатого js файла прошедшего через обфускацию средствами GWT, но после сжатия коннектором размер файла был несколько большего размера чем файл без сжатия этим средством. По этой причине дальнейшие раскопки в данном направлении были прекращены.
$ java -jar yuicompressor-2.4.2.jar -o modified_script_yui.js --charset utf-8 --type js modified_script.js
$ ls -lh modified_script*
-rw-r--r-- 1 user user 725K 2009-10-13 11:49 modified_script.js
-rw-r--r-- 1 user user 719K 2009-10-13 11:51 modified_script_yui.js
$ gzip modified_script*
$ ls -lh modified_script*
-rw-r--r-- 1 user user 199K 2009-10-13 11:49 modified_script.js.gz
-rw-r--r-- 1 user user 203K 2009-10-13 11:51 modified_script_yui.js.gz


* This source code was highlighted with Source Code Highlighter.


Сжатие CSS средствами yuicompressor
CSS файл только один, в нём много пробельных символов и написан он без оптимизаций, по этой причине он был прогнан через yuicompressor но результат в конечном итоге был только в 700 байт что в моих масштабах было не существенно, по этой причине было решено не использовать yuicompressor в текущей активно разрабатываемой версии вообще.

$ java -jar yuicompressor-2.4.2.jar file.css -o file.css.yuic --type css
$ ls -lh file*
-rw-r--r-- 1 User User 38K 2009-10-06 08:26 file.css
-rw-r--r-- 1 User User 5.7K 2009-10-08 01:18 file.css.gz
-rw-r--r-- 1 User User 30K 2009-10-08 01:18 file.css.yuic
-rw-r--r-- 1 User User 5.0K 2009-10-08 01:19 file.css.yuic.gz


* This source code was highlighted with Source Code Highlighter.


Оптимизация изображений
Их порядка 600 штук. Было решено прогнать эти картинки через автоматизированные сервисы и это дало выигрыш приблизительно в 15 — 18% те около 1,5 мегабайт из 10 возможных.
К сожалению исходников большинства этих картинок не сохранилось а может изначально и не было так как они сразу приобретались в форматах png и jpg.

Дальнейшие эксперименты показали что методом брутфорса можно было бы добиться ещё 1% уменьшения по png файлам которых большинство и вероятно это будет сделано перед отправлением проекта в плавание.

Оптимизация картинок проводилась не мной по этому больше информации пока не дам

Оптимизация проводилась со знанием и пониманием
www.artlebedev.ru/tools/technogrette/img

Географическое распределение серверов
В этот момент время загрузки колебалось от 15 до 30 секунд в Москве и порядка 4-6 секунд в Калифорнии. Эта несправедливость объяснялась местом расположения датацентра amazon. В хранилице amazon s3 к этому времени уже хранились все изображения.
aws.amazon.com/s3.

Устранить это досадное недоразумение за разумные деньки помогло другое решение амазона aws.amazon.com/cloudfront

После установки cloudfront время загрузки в Москве приравнялось времени загрузки в Калифорнии из чего можно сделать допущение что в большинстве мест на планете время загрузки равнялось 4-6секундам если это позволяла «последняя миля».

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

Решилось это через создание скрипта предварительной загруки изображений. В задачи скрипта входил минимум задач.
1)Закачать изображения из заранее определённого списка
2)Удалять изображения из списка после закачки, даже если она неудачна
3)Останавливать закачку
4)Продолжать закачку

В процессе написания данного скрипта открылись некоторые интересные особенности js.
Закачка происходит асинхронно, поэтому обычный цикл(while, for) не подходил, так как 600 одновременно закачиваемых файлов подвешивали браузер на некоторое время в зависимости от мощности компьютера.

Это обходилось через вставку анонимной функции в onload и onerror которая вызывала родительскую функцию, те получалась рекурсия не позволяющая высвободить уже неиспользуемую память. Это проявилось в IE через ошибку «out of memory».

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

preloadimg.js (внесены изменения Wed Oct 14 07:53:31 PDT 2009)
  1. stopPreloadImg = false;
  2. arrayImgCached = new Array();
  3. arrayImg = null;
  4. baseUrl = null;
  5.  
  6. function preloadImg(preloadArray, baseUrl) {
  7.   this.arrayImg = preloadArray;
  8.   this.baseUrl = baseUrl;
  9.  
  10.   cacheImage();
  11. }
  12.  
  13. function safeMemoryInRecursion() {window.setTimeout(cacheImage, 0)}
  14.  
  15. function cacheImage()
  16. {
  17.   if (arrayImg.length > 0 && stopPreloadImg == false) {
  18.     var img = new Image();
  19.     img.onerror = function(){safeMemoryInRecursion()};
  20.     img.onload = function(){safeMemoryInRecursion()};
  21.   img.src = baseUrl + arrayImg[0];
  22.     
  23.     arrayImg.shift();
  24.     arrayImgCached[window.arrayImgCached.length] = img;
  25.   }
  26. }
* This source code was highlighted with Source Code Highlighter.


Функцию предзагрузки изображений предполагалось использовать следующим образом

После введения этого кода в эксплуатацию, загрузка изображений перестала ощущаться вообще. Единственный случай когда видно загрузку картинок, если очень быстро ввести логин/пароль и идти на самую тяжёлую страницу, ожидается что в реальной жизни такое будет практически невероятным.
Итого
Сегодняшняя проверка приложения показала
Загружаемый размер вместе с флэшем 591КБ, из них
флэша 348КБ — эта часть оптимизировалась, но подробности мне известны недостаточно полно чтобы об этом что-то писать.
Html+js+xml+css = 243КБ
Время загрузки 5 — 10 секунд (есть отличие от заявленных выше 4-6 секунд, видимо произошли изменения на канале).
При переходе по внутренним страницам подгрузка изображений видна в редких случаях.
<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  </head>
  <body>
    <h1>Страница с предзагрузкой изображений</h1>
      <p>содержимого страницы которое может содержать в себе всё что
      угодно кроме предзагружаемых изображений. Предзагружаемые изображения
      использоватьб здесь нет смысла тк эти изображения могут загрузиться
      дважды.</p>

      <ol>
        <li>Первый раз как используемые на этой странице</li>
        <li>Второй раз как предзагружаемые изображения</li>
      </ol>

      <p>Крайне желательно чтобы эта сраница использовалась
      продолжительное время, тк в противном случае предзагрузка может
      отработать не полностью. Если у пользователя в добавок ко всему отключен
      кэш то при переходе на следующую страницу браузер всёравно будет
      вынужден загрузить заново все изображения</p>

      <script type="text/javascript" src="preloadimg.js"></script>
      <script type="text/javascript">
      var srcArray= new Array(
        "/photo_com/650/3257.jpg",
        "/photo_com/650/3298.jpg",
        "/images/404_habra_error.png"
      );
      preloadImg(srcArray, "http://asha-city.ru");
      </script>
  </body>
</html>

* This source code was highlighted with Source Code Highlighter.


Возможности дальнейшей оптимизации
css — 700байт
далнешая оптимизация изображений — от 1%
Включение сжатия на уровне протокола вносит дополнительную нагрузку на CPU, это можно обойти как минимум двумя способами
1) — предварительное сжатие файлов, и раздача правильных заголовков. На сколько я понимаю в мире java это делается легко и без дополнительных издержек.
2) — Кэширование на прокси серевере или балансировщике нагрузки (nginx, haproxy, …)
Но это относится к нагрузочному тестированию и это уже совсем другая другая история, хоть и не менее увлекательная. Там есть свои инструменты, свои колоссальные проблемы и интереснейшие решения :)

ps: Ссылку на оптимизированный сайт пока не дам, находится в стадии разработки и многое на нём меняется.
pps: повторная загрузка изображений была связанна с временным глюком S3, — видомо обновляли софт и у них это получилость не так как ожидалось — проблема наблюдаласть часов 6 и именно в тот момент когда изучался вопрос что и как оптимизировать.
Теги:
Хабы:
+32
Комментарии 45
Комментарии Комментарии 45

Публикации

Истории

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн
Геймтон «DatsEdenSpace» от DatsTeam
Дата 5 – 6 апреля
Время 17:00 – 20:00
Место
Онлайн