offsetHeight или нечаянный спуск лавины reflow

    В заметке Владимира Токмакова, утверждается:
    HTML-элемент в документе может быть скрыт с помощью JavaScript или CSS-свойства display. Логику, заложенную в CSS, воспроизводить в JavaScript сложно и не нужно. Проще запросить offsetHeight объекта (если 0 = элемент скрыт).

    Проще-то оно, конечно, проще, вот только какой ценой?



    Для проверки видимости элемента принято проверять значение стиля display или наличие класса hide. Когда мы пишем функцию скрытия/отображения сами, то знаем, какое значение стиля display у объекта по умолчанию, или какой класс какому состоянию соответствует. Однако универсальная (библиотечная) функция знать об этом не может.
    Проведем тестирование скорости вычисления значений offsetHeight и style.display.
    Для удобства профайлинга вынесем доступ к этим значениям в отдельные функции:
    function fnOffset(el)
    {
        return !!el.offsetHeight;
    }
    
    function fnStyle(el)
    {
        return el.style.display=='none';
    }
    

    Где el – тестовый контейнер.
    Проведя тест на тысяче итераций, видим, что доступ к offsetHeight всего в два раза медленнее, чем к style.display.
    Теперь усложним тест – на каждой итерации будем добавлять в тестовый контейнер элемент SPAN.
    Результаты профайлинга:
    profile results
    Теперь соотношение между тестируемыми совсем другое. Почему это произошло? Давайте проверим время, затрачиваемое на добавление тысячи элементов, без вызова тестовых функций – тест "clean". Проведем тестирование во всех браузерах, замеряя время следующим способом:
    var time_start=new Date().getTime();
    /* ... тест ... */
    var time_stop=new Date().getTime();
    var time_taken=time_stop-time_start;
    

    Где time_taken – это время, затраченное на тест, в миллисекундах.
    Данные тестов приведены в миллисекундах и взято среднее значение за 5 прогонов:
    test results
    Судя по результатам тестов доступ к offsetHeight медленнее в 50-150 раз.
    Получается, что по оттдельности и offsetHeight и добавление элементов работает быстро, а вместе — очень медленно. Как же так?
     

    Почему такой разрыв между тестируемыми?
    Немного теории.



    Reflow – это процесс рекурсивного обхода ветви дерева DOM, вычисляющий геометрию элементов и их положение относительно родителя. Начало обхода – изменившийся элемент, но возможно и распространение снизу вверх. Существуют следующие типы reflow:
    • начальный – первое отображение дерева;
    • инкрементный – возникает при изменениях в DOM;
    • изменение размеров;
    • изменение стилей;
    • "грязный" – объединение нескольких инкрементных reflow имеющих общего родителя.

    Reflow делятся на неотложные (изменение размеров окна или изменение шрифта документа) и асинхронные, которые могут быть отложены и объединены в последствии.
    При манипулировании DOM происходят инкрементные reflow, которые браузер откладывает до конца выполнения скрипта. Однако, исходя из определения reflow, "измерение" элемента вынудит браузер выполнить отложенные reflow.Т.к. возможно распространение снизу вверх выполняются все reflow, даже если измеряемый элемент принадлежит к неизменившейся ветви.
    Reflow очень ресурсоемки и являются одной из причин замедления работы веб-приложений.
     
    Если судить по тесту "clean", все браузеры хорошо справляются с кэшированием многочисленных reflow. Однако, запрашивая offsetHeight, мы "измеряем" элемент, что вынуждает браузер выполнить отложенные reflow. Таким образом, браузер делает тысячу reflow в одном случае и только один в другом.
    Замечание: У Оперы reflow выполняется еще и по таймеру, что, однако, не мешает ей пройти тест быстрее остальных браузеров. Благодаря этому в Опере виден ход тестов – появляются добавляемые звездочки. Такое поведение оправдано, т.к. вызывает у пользователя ощущение большей скорости браузера.
     
    Подведем итог. Что же показало тестирование? По меньшей мере, некорректно сравнивать универсальный (offsetHeight) и частный (style.display) случаи. Тестирование показало, что за универсальность надо платить.
    А если все-таки хочется универсальности, то можно предложить другой подход – определение Computed Style – конечного стиля элемента (после всех CSS преобразований).
    getStyle=function()
    {
        var view=document.defaultView;
    
        if(view && view.getComputedStyle)
        return function getStyle(el,property)
        {
            return view.getComputedStyle(el,null)[property] || el.style[property];
        };
    
        return function getStyle(el,property)
        {
            return el.currentStyle && el.currentStyle[property] || el.style[property];
        };
    }();
    

    Проведем тестирование этого способа и сведем все результаты в таблицу.
    test results 2

    В IE и FF computed style вычисляется столь же быстро, как стиль самого элемента, а в Опере и Сафари – даже чуть дольше offsetHeight. В цитируемой статье явно указано, что вызов getComputedStyle также вызывает reflow, и причины отсутствия этого в IE и FF непонятны, хотя и радуют. UPD: рано радовались ;) Спасибо AKS за указание на то, что getComputedStyle в IE и FF возвращает некорректные результаты.
    При поиске в интернете способов оптимизации вычисления computed style для Оперы и Сафари была найдена статья Computed vs Cascaded Style, в которой Erik Arvidsson рекомендует не пользоваться такими универсальными функциями (getStyle есть практически в каждой js-библиотеке), а реализовывать необходимую функциональность в каждом конкретном случае. Ведь если мы договоримся, что скрытые элементы должны иметь класс hide, то все сведется к определению наличия этого класса у элемента или его родителей.

     

    Оптимизация: определение класса hide


    Давайте подробнее остановимся на предложенном мной решении. Предлагаю следующую реализацию:
    function isHidden(el)
    {
        var p=el;
        var b=document.body;
        var re=/(^|\s)hide($|\s)/;
        while(p && p!=b && !re.test(p.className))
            p=p.parentNode;
        return !!p && p!=b;
    }
    

    Предполагается, что корневые элементы DOM скрывать не имеет смысла и поэтому проверки ведутся только до document.body.
    Предложенное решение явно не спустит лавину reflow т.к. никаких вычислений и измерений не проводится. Однако немного смущает траверс до корня документа: что же будет при большой вложенности элементов? Давайте проверим. Тест isHidden проводится для вложенности 2 (document.body / test_div). А тест isHidden2 для вложенности 10 (document.body / div * 8 / test_div).
    test results 3
    Как показывают тесты, даже при большой вложенности падение скорости невелико. Таким образом, мы получили универсальное решение, которое быстрее доступа к offsetHeight в 30-100 раз.
    Данная статья предназначена не столько для решения проблемы выяснения видимости элемента в общем случае, сколько для объяснения одного из наиболее часто встречающихся узких мест взаимодействия с DOM и детального разбора методов оптимизации. В ходе тестов я намеренно воспроизводил наихудший случай. В реальных ситуациях, такой прирост скорости получится только при использовании в анимации. Однако понимание причин и механизма reflow позволит писать более оптимальный код.

    Ссылки:
    • Заметка Владимира Токмакова, которая послужила поводом написания этой статьи.
    • Статья Efficient JavaScript, в которой рассказываются способы оптимизации JavaScript, и в частности способы минимизации количества reflow.
    • Статья Notes on HTML reflow, в которой описываются нюансы реализации reflow в Gecko.
    • Статья Computed vs Cascaded Style в которой рассматриваются недостатки функций getStyle

    Тесты: для всех браузеров, для профайлинга в FireBug.
    PS. Спасибо AKS за коментарии, заставившие меня более четко сформулировать определение reflow.
    Поделиться публикацией
    Реклама помогает поддерживать и развивать наши сервисы

    Подробнее
    Реклама
    Комментарии 41
    • 0
      Круто. Плюс за такие археологические раскопки, однозначно.
      Хотя, лично я отслеживаю состояние всех скрывающихся элементов глобальными переменными, т.е. на любую динамически скрывающуюся штуку у меня всегда есть глобальная булевая переменная _show_status, которая имеет значение по умолчанию и меняется при изменении состояния элемента. Имхо, гораздо удобнее.
      • 0
        Имел ввиду [elementName]_show_status.
        • +1
          А если необходимо узнать виден ли вложеный элемент? Такое бывает в сложных приложениях (н-р контрол в соседнем табе). И если есть десятка два контролов, это становится очень неудобно.
          То, что я предложил в конце - это самое простое и правильное решение:
          если мы договоримся, что скрытые элементы должны иметь класс hide, то все сведется к определению наличия этого класса у элемента или его родителей.
          • 0
            добавил еще одну страницу в статью с описанием этого решения.
        • 0
          Замечательный текст!
          Спасибо большое - очень полезно!
          • 0
            Хотелось бы добавить вот что: есть методика скрывания ненужных в данный момент абсолютно спозиционированных элементов свойствами "top: -9999px" / "left: -9999px" вместо "display: none" (такая практика бывает иногда необходимой, чтобы привести в чувство IE).

            Как нетрудно догадаться, запрос offsetHeight скрытых таким образом элементов не даст нам ничего хорошего.
            • 0
              да, но это отлично подойдет для решения с классом hide
              причем для ie можно сделать одну реализацию а для остальных другую
              • 0
                а пример необходимости top: -9999px можно? я как-то никогда с таким не сталкивался. С visibility косяки у IE знаю, а с display не встречал
                • 0
                  Пепелсбей сейчас у себя на сайте перелопатил, поэтому ссылка из веб архива:
                  http://web.archive.org/web/20061201201724/pepelsbey.net/pro/ddm/
                  • 0
                    за сам пример спасибо. Но если заменить top: -9999px на display:none, то все по прежнему работает. Какой смысл тогда использовать top: -9999px ?
                    • 0
                      За сам пример - спасибо. Но если заменить top: -9999px на display:none то все по-прежнему работает. Какой тогда смысл использовать top: -9999px ?
                • 0
                  Большое спасибо, особенно за ссылки. Теперь понятно, почему у меня jquery-код в некоторых случаях так необычайно тормозит.
                  • 0
                    хм, мне очень любопытно чем руководствовался человек поставивший минус комментарию. в статье ведь объясняется теория, объясняющая замедление. и это имеет отношние к любой библиотеке.
                  • +3
                    var view=document.defaultView;

                    if(view && document.defaultView)

                    В чём смысл этого кода?
                    • 0
                      спасибо, опечатка.
                    • 0
                      function fnOffset(el)
                      {
                      return !!el.offsetHeight;
                      }

                      function fnStyle(el)
                      {
                      return el.style.display=='none';
                      }

                      Где c – тестовый контейнер.


                      Может быть, всё-таки, тестовый контейнер «el»?..
                      • 0
                        да, спасибо, опечатка
                      • –1
                        Кстати, недавно обсуждаемый пример с техногрета, про PNG-рамку, мне как-то тоже "красивым" не показался.
                      • 0
                        спасибо за статью. В полку оптимизаторов прибыло? :)
                        • 0
                          Пожалуйста. :)
                          Если честно, прочитав заметку Владимира Токмакова, я сразу же подумал, что предлагаемый им подход не оптимален из-за reflow. Но это наложилось на очередное заявление одного знакомого, который ссылается на Кодоводство, как на библию, и на Лебедева, как пророка ее. Вот я и решил аргументировано объяснить, что прислушиваться к советам великих нужно, но также нужно понимать разницу между безоговорочным доверием и осознанным согласием. Это было поводом. А что получилось в результате – Вы видите. ;) Отсюда такой странный заголовок. Всю остальную мораль я убрал, чтобы не портить статью, а заголовок забыл.

                          На счет "оптимизаторства". Я учился программировать с ассемблера. Добивался, чтоб программки не тормозили на двойке с двумя метрами памяти и влезали в загрузочный сектор дискетки. Это наложило свой отпечаток на мое профессиональное развитие. Потом я изучил Си, затем Си++, но понимание того, как все устроено у компилятора внутри, было всегда. Затем я не мог принять интерпретируемые языки из-за того, что практически ни на что нельзя повлиять. Однако постепенно осознал, что скорость кода, в какой-то мере, допустимо приносить в жертву скорости разработки. Однако неоптимальный код я не могу писать до сих пор. Наверное, это стало чертой профессионального характера. А накопив багаж знаний по какому-либо языку или технологии, рефракторинг с целью оптимизации приходится проводить все реже.
                          • 0
                            и влезали в загрузочный сектор дискетки

                            ай-яй... собственный One-Half?!
                            • +1
                              да нет, просто прожки работающие без доса, юзая тока функции биоса. на сколько помню написал простейший файловый менеджер и текстовый редактор. потом забил...
                              • 0
                                да нет, просто прожки работающие без доса, юзая тока функции биоса.

                                а... :")
                                ну тады респектище!
                        • 0
                          круто написано.
                          спасибо за статью!
                          • НЛО прилетело и опубликовало эту надпись здесь
                            • +1
                              Здравствуйте, автор статьи!
                              Ответьте, пожалуйста, на следующий вопрос (если ответ не является секретом). На основании чего Вы сделали вывод о том, что запрашивая offsetHeight, мы "измеряем" элемент, что вынуждает браузер сделать reflow? Мне понятно то, что таким образом мы "измеряем" элемент, но почему этот процесс становится причиной повторного вывода (laying out) элемента?
                              • 0
                                почему этот процесс становится причиной повторного вывода (laying out) элемента?

                                Это не так. Этот процесс коммитит отложеные reflow. Если их небыло, то reflow не происходит - смотрите самый первый тест.

                                Что же произойдет в случае если они были.
                                Представьте, что в DOM есть таблица с двумя ячейками. Ширина таблицы - 100px, ширина ячейки - 50%.

                                examples

                                Мы меняем содержимое первой ячейки и хотим узнать какова ее ширина. Казалось бы - 50px. Сравним таблицы 1 и 2.
                                Как браузер должен отличить первый случай от второго? Для этого требуется сделать layout не только для самого элемента, но и для его детей. Теперь рассмотрим третий случай. Как мы видим от содержимого могут измениться параметры родительского элемента, что потребует его пересчета. Примеры 4 и 5.
                                Таким образом от изменившегося элемента распространяется лавина reflow.

                                Efficient JavaScript:
                                As stated earlier, the browser may cache several changes for you, and reflow only once when those changes have all been made. However, note that taking measurements of the element will force it to reflow, so that the measurements will be correct. The changes may or may not not be visibly repainted, but the reflow itself still has to happen behind the scenes.

                                This effect is created when measurements are taken using properties like offsetWidth, or using methods like getComputedStyle. Even if the numbers are not used, simply using either of these while the browser is still caching changes, will be enough to trigger the hidden reflow.
                                • 0
                                  Я извиняюсь за то, что невнятно высказался. Лучше я приведу пример. В вашем тесте в функцию test_offset добавьте вот это:

                                  var o = document.getElementsByTagName('meta').item(0);

                                  И пусть в цикле в вызове функции fnOffset будет именно этот аргумент (переменная "o" вместо переменной "с").
                                  • 0
                                    Вы это имели ввиду:
                                    function test_offset()
                                    {
                                        var c=init();
                                        var o = document.getElementsByTagName('meta').item(0);
                                        profile('offsetHeight');
                                            for(var i=0; i<1000; i++){
                                                fnOffset(o);
                                                c.appendChild(t.cloneNode(true));
                                            }
                                        profileEnd();
                                        clean(c);
                                    }

                                    У меня цифры остаются прежними во всех браузерах.

                                    Коммитятся все отложенные reflow, а не только те, которые связаны с каким-то элементом. Понятно почему? Если нет постараюсь объяснить еще раз.
                                    • +1
                                      >Коммитятся все отложенные reflow, а не только те...

                                      Вы ведь только теперь написали: "не только те" (об этом и речь).

                                      >Понятно почему? Если нет...

                                      А вот так не надо.

                                      >Мы получили универсальное решение...

                                      Проверьте свое "универсальное решение" на этом примере:

                                      <div class='hide'>
                                      <span id='test'>test</span>
                                      </div>

                                      <script type='text/javascript'>

                                      alert(isHiddenFast(document.getElementById('test')));

                                      </script>

                                      • 0
                                        >>Коммитятся все отложенные reflow, а не только те...
                                        Я думаю эта фраза некорректна. Я так написал чтоб было понятнее. reflow не связан с каким либо элементом, у reflow есть только точка старта, от которой распространяются изменения. И будет ли затронут тот или иной элемент зарание не известно.

                                        >Вы ведь только теперь написали: "не только те" (об этом и речь).
                                        Теперь понял, что Вы имели ввиду. Четкого определения reflow я не нашел ни на русском ни на английском. Если можете поправить определение reflow чтобы это было понятно - внесите свой вклад :)

                                        >>Понятно почему? Если нет...
                                        >А вот так не надо.
                                        да ладно Вам, я ж по-доброму ;)

                                        >>Мы получили универсальное решение...
                                        >Проверьте свое "универсальное решение" на этом примере:
                                        О! спасибо Вам большое за обнаружение этой ошибки.
                                        В цитируемой статье явно указано, что вызов getComputedStyle также вызывает reflow, и причины отсутствия этого в IE и FF непонятны, хотя и радуют.

                                        Теперь понятны "причины отсутствия" - по тому что не работает :)) Странно, что я не заподозрил подставу сразу.

                                        Т.о. "универсальным решением" будет функция isHidden. ;)
                                        • 0
                                          >И будет ли затронут тот или иной элемент зарание не известно.

                                          Почему же не известно? Если верить статьям, то:
                                          Incremental reflow подразумевает отложенное выполнение, т.е. объединение всех заданий по вычислениям для последующей обработки (нечто, напоминающее пакетный режим). Однако любые дополнительные запросы к системе за вычислениями вызывают Reflow method корневой структуры-html, от которой начинается рекурсивное движение к целевой структуре по дереву документа и выполнение вычислений на каждом этапе. И тут, по причине асинхронной природы этого процесса, начинается еще и одновременное выполнение всех тех задач, которые были ранее поставлены в очередь.

                                          >Если можете поправить определение reflow чтобы это было понятно - внесите свой вклад :)

                                          Если бы мог, то уже бы внес. ;)
                                          Хотя, как говорится, попытка не пытка, и если в качестве базиса выбрать статью Notes on HTML Reflow, то можно попытаться определиться следующим образом.
                                          Reflow - это вычисление параметров геометрии форматируемых структур. В HTML такая структура, как правило, соответствует одному элементу, и она (эта структура) имеет форму прямоугольника (при вычислениях используются его высота, ширина и отступ от родительской структуры).

                                          >Нужно подобрать лучший параметр ветвления вместо window.GeckoActiveXObject.

                                          Было бы разумно проверять в условии наличие тех свойств и методов, которые необходимо использовать:
                                          if (document.defaultView && …

                                          >Т.о. "универсальным решением" будет функция isHidden. ;)

                                          Обе isHidden жестко привязаны к имени класса, и, следовательно, не могут называться универсальными. К тому же та, что внутри isHiddenFast будет работать некорректно (проблема: return !!p && p!=b; => нет проверки класса элемента, если элемент - это body). Чтобы увидеть это, измените ранее упомянутый пример на:

                                          <body class='hide'>
                                          <span id='test'>test</span>
                                          </body>
                                          • 0
                                            > Было бы разумно проверять в условии наличие тех свойств и методов, которые необходимо использовать:
                                            > if (document.defaultView && …
                                            там дело немного в другом document.defaultView есть и в Safari, но в Safari быстрее работает isHidden

                                            >>Т.о. "универсальным решением" будет функция isHidden. ;)
                                            > Обе isHidden жестко привязаны к имени класса, и, следовательно, не
                                            > могут называться универсальными.
                                            если они не привязаны к классу, то это должны быть не isHidden, а doParentshaveTheFollowingClassName ;) но если это принципиально, то исправить, чтобы имя класса передавалось параметром - не проблема.

                                            > К тому же та, что внутри
                                            > isHiddenFast будет работать некорректно (проблема: return !!p &&
                                            > p!=b; => нет проверки класса элемента, если элемент - это body).
                                            а какой смысл скрывать body? это сделано нарочно.

                                            Таки собрался дать более четкое определение reflow. Думаю теперь все четко написано.

                                            PS. спасибо большое за комментарии!
                                            • 0
                                              >спасибо большое за комментарии!

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

                                              >а какой смысл скрывать body?

                                              Дело в том, что все может быть несколько сложнее (именно поэтому я считаю, что написать что-то универсальное - это "не поле перйти"). Приведу пример, не вдаваясь в подробности. Вот упрощенное правило стиля тела документа – поразмышляйте (когда будет время) о том, для чего это может понадобится и как это может изменить скрипты:

                                              body.hide .hide {
                                              display:none;
                                              }
                                              • 0
                                                >поразмышляйте (когда будет время) о том, для чего это может понадобится и как это может изменить скрипты:

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

                                                однако в данном случае body.hide определяет общее состояние, а сам .hide - состояние конкретного виджета. а нам надо найти именно состояние виджета. и как состояние приложения называтся не суть важно. тем более, что в таком случае мы получим не то что хотели:

                                                <body class="hide">
                                                <widget class="hide"></widget>
                                                <p id="p1">test</p>
                                                </body>
                                                isHidden($('p1')); // ==true

                                                поэтому состояние приложения лучше назвать по другому.

                                                или я что-то упускаю?
                                                • 0
                                                  >однако в данном случае

                                                  Если рассматривается частный случай, но термин "универсальное решение" неуместен. Разве нет?

                                                  >или я что-то упускаю?

                                                  Упускаете то, на что я намекал раньше (чуток перефразируя): isHidden не могут называться универсальными. Можно даже добавить, что вообще не может быть совершенного решения. Это я и пытался показать (но не доказать) на своих примерах. .
                                                  • 0
                                                    хм. понял что вы имеете ввиду.

                                                    Можно изменить функцию на:
                                                    function doesElementInheritCSSClass(el, cls)
                                                    {
                                                        var p=el;
                                                        var re=new RegExp('(^|\\s)'+cls+'($|\\s)');
                                                        while(p && !re.test(p.className))
                                                            p=p.parentNode;
                                                        return !!p;
                                                    }
                                                    • 0
                                                      >Можно изменить функцию на...

                                                      Конечно можно. Я просто пытаюсь отметить, что случай необычный, из разряда тех, для которых нужны также необычные методы. Я считаю, что есть простые и стандартные алгоритмы, которые можно расписать раз и навсегда, вроде addClassName, removeClassName, setOpacity и т.д. А вот в данном случае написать универсальное решение не то, чтобы не возможно, а скорее не нужно. Будет легче отталкиваться от конкретной ситуации (Вы ведь оптимизатор – понимаете). Можно, конечно, пытаться, но при этом будем наступать на одни и те же грабли. Взять, хотя бы, решение Владимира Токмакова. Автор руководствовался желанием найти унифицированное решение. И чем это все закончилось? Вашей статьей... ;)
                              • 0
                                Перезалейте пжл изображения на хабрасторэджь, не отображаются.
                                • 0
                                  оригинал — dpp.su/blog/reflow/
                                  PS. вот за это habrahabr.ru/ppg/ принципиально ничего править не буду. уважаемая редакция, не нужно относиться к пользователям как к скоту.

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