Посты с тэгом «оптимизация»

  • История одной оптимизации

    Летом мы запустили новый купонный проект BigBuzzy. Таких проектов к тому времени было довольно много и чтобы выделиться из толпы мы решили немного поменять бизнес-модель: вместо одного предложения в день выдавать четыре. Но, как это обычно бывает, аппетит приходит во время еды, поэтому уже спустя несколько месяцев на главной странице красовалось не 4, а 30 предложений.

    И мы сразу же начали получать жалобы о жутких тормозах на главной странице. На поиск и устранение проблем у меня ушло два дня. О том, как находились узкие места и будет сегодняшний рассказ. А заодно научимся пользоваться инструментами вроде Web Inspector’s Timeline (если вы их ещё не освоили).

    Поиск проблемы

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

    01-timers

    Самые большие проблемы наблюдались в Firefox: загрузка процессора на главной странице доходила до 70%. Поэтому я начал рассматривать скрипт таймера под микроскопом, а именно в Web Inspector, который по умолчанию входит в состав браузеров Safari и Chrome. Вообще, многие ребята довольно снисходительно относятся к этому инструменту, продолжая по привычке работать в Firebug’е, а зря. Лично для меня Web Inspector стал основным инструментом для отладки: выглядит он приятнее и содержит ряд полезных нововведений.

    Исследуем узкие места

    Так как сам скрипт таймера довольно простой, то не было смысла заниматься его профилированием — проблема явно где-то в reflow и repaint. Поэтому скрипт нужно исследовать через Timeline:

    02-timeline

    Полагаю, что многие читатели ещё ни разу не сталкивались с этим инструментом, поэтому принцип его работы и поиска проблем опишу в небольшом уроке. Стоит отметить, что Web Inspector в Chrome немного круче, чем в Safari, поэтому рекомендую пользоваться первым браузером.

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

    Открываем шаблон в браузере и запускаем Web Inspector, вкладка Timeline. На странице есть красный квадратик и кнопка «Test». Чтобы начать исследование, нужно нажать на кнопку записи вкладки Timeline 03-rec, а потом нажать на кнопку «Test» в основном окне браузера. Наш квадратик посинел и стал больше по высоте, а в Timeline записались следующие события:

    04-test1

    Первые три записи относятся непосредственно к кнопке, которую нажали: применили псевдо-класс :active (Recalculate style), отобразили изменения на экране (Paint), вернули кнопку в исходное состояние, убрав :active (Recalculate style). После того, как пользователь отпустил кнопку мышки сработало событие click, и именно оно и всё, что ниже, нас будет интересовать.

    Во время клика сработал следующий скрипт:

    function test() {
    	var el = document.getElementById('test');
    	el.style.backgroundColor = 'blue';
    	el.style.height = '100px';
    }
    

    Ничего особенного: просто получили ссылку на элемент и поменяли у него цвет фона и высоту. Этот процесс был отображён на временной шкале: пересчитали стили (Recalculate style), пересчитали геометрию объектов (Layout) и отобразили изменения (Paint).

    Как видите, несмотря на то, что мы поменяли два СSS-свойства, пересчёт стилей произошёл всего один раз. Поменяем скрипт:

    function test() {
    	el.style.backgroundColor = 'blue';
    	var height = el.offsetHeight;
    	el.style.width = '100px';
    }
    

    Между присваиванием новых стилей мы решили получить высоту объекта. Но временная шкала сильно преобразилась:

    05-test2

    Теперь у нас уже два события Recalculate style, а у самого события click появилась группировка (треугольник слева от жёлтой полоски), которая указывает, какие именно события произошли во время клика.

    Этот небольшой пример указывает на две очень важные особенности браузеров — это откладывание перерисовки на момент выхода из функции (первый пример) и существование определённых свойств у элемента, которые принудительно вызывают пересчёт стилей (далее restyle; второй пример). О существовании особых свойств, вызывающих restyle, думаю, многие уже знали: это свойства вроде offsetLeft/Right/Width/Height, clientLeft/Right/Width/Height и так далее. Во втором примере, после установки свойства backgroundColor браузер пометил дерево элементов как требующего пересчёта стилей. А обращение к offsetHeight принудительно вызвало этот пересчёт. Затем мы установили свойство width, которое отложило пересчёт стилей, геометрии и отображения на момент выхода из потока JS-функций.

    Отсюда первое правило: нужно стараться не смешивать получение и запись CSS-свойств. Лучше, например, сначала получить нужные свойства элемента, а затем присвоить новые.

    Для любителей jQuery более красноречивым будет вот такой пример:

    function test() {
    	var e = $('#test');
    	var width = e.css('width');
    	if (width == '50px')
    		e.css('width', '100px');
    
    	var height = e.css('height');
    	if (height == '50px')
    		e.css('height', '100px');
    }
    

    Вот его шкала:

    06-test3

    Как видите, помимо лишнего Recalculate style появился Layout (reflow), что сделало выполнение скрипта более медленным. Если немного оптимизировать, переместив получение высоты выше в коде:

    function test() {
    	var e = $('#test');
    	var width = e.css('width'),
    		height = e.css('height');
    
    	if (width == '50px')
    		e.css('width', '100px');
    
    	if (height == '50px')
    		e.css('height', '100px');
    }
    

    …получим совершенно иную картину:

    07-test4

    Лишний Layout (помимо Recalculate style) объясняется тем, что jQuery каждый раз при получении CSS-свойств вызывал window.getComputedStyle(), который принудительно запускает reflow. Справедливости ради стоит отметить, что в функции jQuery.css() есть оптимизация, которая сначала проверяет наличие запрашиваемого свойства в element.style и если его там нет, вызывает window.getComputedStyle(). Но в любом случае, лучше всегда разделять чтение и изменение свойств.

    Таймеры

    Напомню, что я занимался оптимизацией таймеров, которых было несколько. И каждый таймер работает через свой setTimeout(). Посмотрим, что это означает на практике:

    function test() {
    	var el = document.getElementById('test');
    	setTimeout(function(){
    		el.style.backgroundColor = 'blue';
    	}, 10);
    	setTimeout(function(){
    		el.style.width = '100px';
    	}, 10);
    }
    

    08-test5

    У обоих таймеров одинаковый период ожидания и момент исполнения. На шкале видно, что после каждого таймера был запущен пересчёт стилей. Но в реальности момент исполнения будет далеко не всегда одинаковым. Поэтому поменяем задержку у последнего таймера — поставим 11 мс вместо 10 мс:

    function test() {
    	var el = document.getElementById('test');
    	setTimeout(function(){
    		el.style.backgroundColor = 'blue';
    	}, 10);
    	setTimeout(function(){
    		el.style.width = '100px';
    	}, 11);
    }
    

    И мы видим, что на шкале появился дополнительный repaint:

    09-test6

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

    Итак, суммируя всё вышесказанное, для оптимизации я

    • разделил чтение и запись CSS-свойств;
    • дополнительно сделал кэширование текущих значений анимации, чтобы меньше обращаться к элементам;
    • заменил несколько таймаутов на один.

    В итоге в Firefox нагрузка на процессор снизилась… всего на 10%. Вообще, это было крайне странно: даже при наличии всего одного анимированного таймера на странице Firefox грузил процессор на 60%, при том что Webkit грузил всего на 5%. Нужно копать дальше.

    Влияние вёрстки на производительность

    Два года назад я делал большое исследование на тему того, как вёрстка влияет на производительность браузера. Похоже, у моей проблемы схожие корни, поэтому начал перебирать все известные мне варианты оптимизации. Но, к сожалению, ничего не помогало.

    Так как все restyle и reflow процессы я оптимизировал, проблема явно была где-то в repaint. Вспомнил, что в Firefox 3.5 появилось событие mozAfterRepaint, которое позволяет увидеть области, которые были перерисованы во время repaint. Для удобства было поставлено расширение Firebug Paint Events, которое позволяет отслеживать перерисовки экрана.

    Чтобы описать всю бурю эмоций, которые я испытал после просмотра логов, предлагаю читателю посмотреть на скриншот, где указана область перерисовки во время работы всего одного таймера на странице:

    10-bbz

    Я специально оставил только 8 из 30 предложений, чтобы картинка не распирала страницу, но смысл, думаю, ясен: во время анимации даже одного таймера перерисовывалось примерно 90% страницы, 15 раз в секунду. И это при условии, что у цифр таймера указан position:absolute, а у их контейнера overflow:hidden. То есть сама анимация по определению никак не могла повлиять на области вне контейнера (на скриншоте обозначен синим прямоугольником), но перерисовывалась почти вся страница.

    Около часа мне понадобилось на то, чтобы найти причину такого странного поведения. Ей оказалось… свойство float:left у одного из контейнеров. Как только я заменял его на float:none нагрузка на процессор падала ниже 10% (с float:left была около 60%).

    Проблема проявляется стабильно, причём не только в Firefox, но и в Opera и IE8. Я сделал простую демку, где можно в живую увидеть эту проблему. В ней всего несколько блоков, однако у них указан box-shadow — очень тяжелое в плане нагрузки на процессор CSS-свойство. В правом верхнем углу есть кнопка, которая всего лишь переключает float у контейнера. Понаблюдайте за нагрузкой на процессор при разных состояниях кнопки, а также за областью перерисовки.

    В общем виде проблему можно описать так:

    Repaint срабатывает на контейнере самого дальнего родителя, у которого указан float:left|right.

    Схематично это выглядит так:

    11-tree

    Причём проблема не только во float. Я перепробовал различные варианты горизонатльной группировки блоков: display:inline-block, display:table-cell, таблицы и даже новомодные flex box — во всех случаях проблема оставалось. Помогало только абсолютное позиционирование боковых блоков.

    В общем виде я проблему решил: поставил боковую панель в коде перед основным контейнером и только ей указал float. Основной контейнер был без float и repaint происходил именно там, где нужно. Однако на живом сайте решить проблему не удалось, так как на большинстве страниц стояли clearfix-элементы, из-за которых макет разваливался. Поэтому пришлось пока отключить анимацию с таймеров :(

    ***

    Честно говоря, после таких браузерных крендебобелей на всякие пузомерки типа Peackeeper, которыми так хвастаются разработчики с каждым новым релизом своего браузера, без слёз смотреть не получается. Поэтому мой вам совет: заранее узнавайте о всех интерактивных элементах на странице, не увлекайтесь новым CSS3, продумывайте рост сайта заранее и пользуйтесь правильными инструментами для отладки производительности — тогда будет вам счастье и высокая производительность.

    Если хотите узнать больше о профилировании производительности, очень рекомендую статью Стояна Стефанова на эту тему.

  • Как создавалась Айчиталка. Часть 1: движок

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

    Пока пишу с корпоративного аккаунта, а если суппорт Хабра таки отдуплится и поправит баги, то буду писать со своего.

  • punypng

    Хоть и зарекался я не рекомендовать инструменты для пакетной оптимизации графики, но тут все-таки придется.

    Встречайте нового игрока — punypng. Новый сервис, очень похожий (но пока не такой удобный) на известный smush.it.

    После публикации на SM первой части статьи про оптимизацию PNG со мной связались ребята из проекта Ask.com — довольно известного на Западе поисковика. Они как раз начали разрабатывать очередной оптимизатор картинок для веба. Немного проконсультировавшись, они добавили чистку прозрачных пикселей, что в большинстве случаев дает довольно ощутимое сжатие. Так что я теперь являюсь консультантом проекта punypng, а этот пост — наглый пиар :)

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

    Чтобы сравнить результаты, я дополнительно прогнал несколько изображений (наиболее показательным оказалось вот это) через smush.it и ImageOptim.

    smush.it использует утилиту pngcrush, которая по всем меркам является устаревшей. Она пытается перепаковать изображения, не меняя их тип (например, черно-белая RGB-картинка не будет преобразована в Grayscale). Утилита OptiPNG, которая является ответвлением от pngcrush, делает такие преобразования автоматически, но при этом работает гораздо медленнее (поэтому на smush.it используется pngcrush). Так что от этого сервиса ждать каких-то выдающихся результатов пока не приходится. Однако на выходе мы получаем «безопасное» преобразование, которое гарантирует, например, что полноцветный полупрозрачный PNG не будет преобразован в полупрозрачный PNG с палитрой (разница в отображении в IE6).

    ImageOptim, в свою очередь, пытается прогнать изображение через несколько библиотек, выбирая наилучший результат. Среди этих библиотек — PNGOUT Кена Сильвермана, которая априори должа выдавать лучший результат, так как использует собственный алгоритм упаковки данных. Даже она не смогла сжать лучше, чем punypng. Не говоря уже о том, что punypng отработал горазо быстрее, чем ImageOptim.

    Так что от всей души советую попробовать этот сервис. Я, в свою очередь, постараюсь описать больше алгоритмов оптимизации изображений, чтобы сделать сервис еще лучше.

    UPD: Окей, значит, расклад такой.

    PNGOUT сам не умеет перебирать фильтры. Как правильно заметил Павел у себя в блоге, с этим успешно справляется OptiPNG, выдавая в консоль статистику обработки файла. Соответственно, правильный способ максимального сжатия файла такой: сначала пропускаем через OptiPNG, получаем от него номер N примененного фильтра и отдаем его в качестве параметра -fN для PNGOUT.

    По поводу ImageOptim. Судя по исходнику, там PNGOUT используется без указания правильного фильтра, и, более того, применяет библиотеки не друг за другом на одном и том же файле, а независимо, выбирая наименьший результат (поправьте меня если не прав, так как Objective-C не знаю). Поэтому и результат сжатия у меня получился не самый лучший.

    PNGSquash, напротив, применяет библиотеки в цепочке, поэтому получается так сильно сжать файлы. Так что эта программка работает лучше, чем ImageOptim.

    Сейчас узнаю, как работает punypng.

    UPD2: punypng не использует PNGOUT из-за лицензионных ограничений. Есть вероятность того, что ребята сделают офлайновую версию punypng: для этого нужно всей толпой проголосовать за это предложение.

  • Руководство по оптимизации PNG

    Специально для Smashing Magazine написал руководство по оптимизации PNG (вернее, наоборот: руководство по оптимизации изображений для PNG). По сути, это собранные вместе статьи из Техногрета и моего блога, которые были немного переосмыслены, дополнены, упрощены и переведены на английский язык.

    Так как статья получилась довольно большой, ее разделили на две части. Вторая часть выйдет через неделю. Комментарии, вопросы и пожелания можно оставлять здесь.

  • Пакетная оптимизация PNG

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

    optipng -o5 *.png

    Создают даже целые сервисы вроде smush.it.

    Так вот, лично я никогда не пользовался такими советами. И пока не рекомендую делать этого ребятам, которые действительно хотят качественно оптимизировать графику. По одной простой причине: «меньше» не всегда значит «лучше».

    Что бы не говорили злые языки, но доля IE6 на рынке все еще слишком велика, чтобы сбрасывать его со счетов. Именно у этого браузера больше всего проблем с отображением разных вариаций PNG. В частности, полноцветный полупрозрачный PNG (PNG24 в терминологии фотошопа) он просто так не покажет, нужно использовать фильтр AlphaImageLoader или VML. А у полупрозрачных индексированных PNG покажет только непрозрачные пиксели. Из этого следует вывод, что нужно контролировать оптимизацию каждого файла.

    Рассмотрим несколько примеров. Для начала возьмем полупрозрачную плашку:

    …и вызовем на ней OptiPNG:

    optipng -o5 mate.png

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

    optipng -o5 -nc mate.png

    Рассмотрим второй пример:

    При попытке оптимизировать этот файл OptiPNG преобразует изображение в 8-битную палитру, однако это будет не оптимальный способ хранения изображения и вы увидите сообщение, что файл уже оптимизирован. Но я готов пожертвовать «гладкими» краями в IE6, лишь бы не извращаться с фильтрами и VML, потому что этот файл будет использоваться 50 раз на странице. Кто не в курсе: AlphaImageLoader жрет очень много памяти и процессорного времени, VML менее требователен, но все равно больше, чем обычная картинка или фон. Поэтому в данном случае нужно форсировать преобразование палитры:

    optipng -o5 -force corner.png

    Еще одним плюсом ручной оптимизации для меня является соответствие моих ожиданий действительности. Очень часто при сохранении черно-белых PNG я забывают выставить им режим цветовой Grayscale, а лог OptiPNG постоянно мне об этом напоминает, когда процент сжатия меньше 20. Кстати, smush.it не умеет преобразовывать цветовую палитру в черно-белую.

← cтарое