Посты с тэгом «float»

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

    Летом мы запустили новый купонный проект 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, продумывайте рост сайта заранее и пользуйтесь правильными инструментами для отладки производительности — тогда будет вам счастье и высокая производительность.

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

  • Две float-колонки одинаковой высоты

    Хочу поделиться своим способом верстки двухколоночных макетов, где обе колонки должны иметь одинаковую высоту. Тема, казалось бы, уже избитая: любой западный (а с ними и отечественный) ресурс выдаст вам с десяток способов сделать это. Но с одим нюансом: это будут макеты-«кирпичи», то есть макеты фиксированной ширины. Предел возможностей — одна растягивающаяся колонка. Я покажу как сделать две (и более) растягивающиеся колонки, причем это будет не эмуляция в виде толстого цветного бордера, а именно полноценная колонка, которой можно задать, например, свою фоновую картинку.

    Правильный способ

    Наиболее правильным на сегодняшний день способом верстки многоколоночных макетов является использование CSS-свойства display: table-*, например, вот так:

    <div style="display:table-cell;width:50%">
    	<div style="display:table-row">
    		<div style="display:table-cell;background:red">column 1</div>
    		<div style="display:table-cell;background:blue">column 2</div>
    	</div>
    </div>
    

    Проблема в том, что это не работает в IE6/7, которые составляют значительную долю рынка браузеров. Что ж, придется воспользоваться альтернативным способом.

    Альтернативный способ

    Как обычно, сначала разберем проблему на составляющие. Предположим, у нас есть двухколоночный макет, первая колонка шириной 25% от контейнера, а вторая — 50%:

    col1

    Проблема заключается в том, что мы не знаем, в какой именно колонке будет больше контента. Как правило, текстовой информации больше, чем меню, но может случится и так, что меню будет больше текста (это особенно актуально для крупных корпоративных сайтов).

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

    col2

    Какие свойства есть у float-блоков? Они находятся в потоке, значит, могут влиять на высоту контейнера. То есть если мы обрамим блоки контейнером и создадим у него правильный контекст форматирования (либо через clear-элемент, либо через overflow: hidden), наш контейнер примет высоту наибольшей колонки:

    col3

    А если мы добавим не один, а два контейнера, то оба они будут высотой с наибольший блок:

    col4

    Обращаем внимание на размеры колонок: первая 25%, вторая 50%. То есть вторая колонка ровно в 2 раза больше первой. Соответственно, если первому — внешнему — контейнеру мы задаим ширину в 25%, а второму — внутреннему — 200% (две ширины внешнего контейнера, что будет соответствовать 50% относительной всей страницы) и сместим его на ширину контейнера вправо, мы получим подобие того, чего хотим добиться:

    col5

    Осталось разобраться с текстовыми колонками. У нас появился новый контейнер, от которого рассчитываются размеры колонок. Так как левая колонка должна быть шириной в 25% от страницы, а ширина внутреннего контейнера равна 50% от страницы, то новая ширина колонки будет равна 50% (50% × 0.5 = 25%). Сама колонка не должна влиять на ширину, но все еще должна быть в потоке, поэтому подавляем влияние ширины колонки на поток с помощью margin-right:-100%, а сам элемент смещаем влево на половину ширины контейнера, то есть на 50%. Получаем именно то, что нам нужно:

    col6

    А вот сам HTML-код, с помощью которого реализуется эта конструкция:

    <style type="text/css">
    	.col-wrap1 {
    		width:25%;
    		background:blue;
    	}
    
    	.col-wrap2 {
    		width:200%;
    		margin-right:-100%; /* чтобы IE6 не раздвигал контейнер */
    		position:relative;
    		left: 100%;
    		background:red;
    	}
    
    	.col1 {
    		float:left;
    		width:50%;
    		margin-right:-100%;
    		position:relative;
    		left:-50%;
    	}
    
    	.clear {
    		clear:both;
    		font-size:0;
    		overflow:hidden; /* тройной презерватив для IE */
    	}
    </style>
    <div class="col-wrap1">
    	<div class="col-wrap2">
    		<div class="col1">left column</div>
    		<div class="col2">center column</div>
    		<div class="clear"></div>
    	</div>
    </div>
    

    Резюмируя все вышесказанное: я создал два контейнера, который являются дублерами основных колонок, и раздвигаются по высоте этими самыми колонками. Остается добавить, что этот способ является более гибким, чем использование CSS-свойств display: table-*, потому что сами колонки можно перемещать с помощью свойств top и left. Чтобы продемонстрировать потенциал этого решения, я сделал специальный пример. Обратите внимание, что у каждой колонки есть свой бордюр и фоновая картинка, выровненная по правому нижнему краю, что в принципе не возможно в других известных способах.

    На основе этого способа можно создать и больше растягивающихся колонок одинаковой высоты. Пример: сайт ВТБ24. Там три колонки одинаковой высоты; верстка осложняется тем, что первые две колонки должны быть в общей рамке, между которыми есть вертикальный разделитель. Когда я готовился к одному из мастер-классов, в одной книге про «качественную верстку» (естественно, западного автора), я прочил, что такое реализовать невозможно :) Для меня это стало очередным подтверждением, что там не умеют верстать качественные растягивающиеся сайты.

    Два слова о верстке макетов

    На первый взгляд может показаться, что этот способ слишком специфический и подходит далеко не для каждого макета. Это не так. Основной трюк заключается в том, чтобы правильно определить модульную сетку и ширину колонок и контейнеров. Пока дизайнеры не слышат, признаюсь: первое, что я делаю при верстке макета — удаляю гайды, которые нарисовал дизайнер. Они мне нужны лишь для того, чтобы понять, как должны выравниваться блоки, саму модульную сетку я делаю на основе тщательного анализа макетов (на это может уйти целый рабочий день). После этого 7 колонок превращаются в 2 контейнера-дублера с 2…4 колонками в каждом. Может, потом подробнее опишу этот момент, пока могу дать общий совет, старайтесь подбирать такие ширины контейнеров и колонок, чтобы они делили 100 без остатка, а именно: 50%, 25%, 20%, 10%, 5%, 2%, 1%. Тогда вы сможете без особых хлопот выравнивать блоки по горизонтали в вертикали в независимых контейнерах.

    Метки: , , ,