DocumentFragment: что это такое и как с ним (не) бороться

    Дисклеймер
    Похоже, у меня начинается новая серия статей — немного скучная и сугубо утилитарная. В них будут содержаться разъяснения моментов, которые часто вызывают трудности у моих студентов. Если вы матёрый веб-девелопер, скорее всего, вам будет неинтересно. Если вы ждёте извращений в силе «Пятничного JS», их тут не будет, увы.


    Одна из вещей, с пониманием которых у студентов регулярно возникают проблемы — это DocumentFragment. В общем-то, я не могу их за это винить. При внешней простоте он имеет несколько неочевидных и даже контринтуитивных свойств. В данной статье я хочу собрать всё, что необходимо про него знать новичку.

    image

    Что это такое


    DocumentFragment — это контейнер, который может содержать произвольное количество элементов DOM. Если совсем по-простому, можно представлять его себе как ведро. Элементы складываются в него, чтобы в нужный момент их можно было разом вывалить куда надо.

    Как создать


    Элементарно.

    var fragment = document.createDocumentFragment();
    

    Существуют также другие способы, но о них ниже.

    Зачем нужен


    Как я уже писал выше — для того, чтобы хранить DOM-элементы. «Но их можно хранить и в обычном диве», — может возразить читатель. Верно, однако у фрагмента есть уникальное свойство, которое делает его лучшим кандидатом на эту роль. Рассмотрим следующий код:

    var fragment = document.createDocumentFragment();
    var parentDiv = document.createElement("div");
    var div1 = document.createElement("div");
    var div2 = document.createElement("div");
    
    fragment.appendChild(div1);
    fragment.appendChild(div2);
    //сейчас будет интересно
    parentDiv.appendChild(fragment);
    console.log(parentDiv.children);
    

    Что нам скажет консоль? Человек, не знакомый с DocumentFragment, может подумать, что у parentDiv'а будет один дочерний элемент — fragment. Но на самом деле у него окажется два дочерних элемента — div1 и div2. Дело в том, что сам фрагмент не является DOM-элементом, он лишь контейнер для DOM-элементов. И когда его передают в качестве аргумента в методы типа appendChild или insertBefore, он не встраивается в DOM-дерево, а вместо этого встраивает туда своё содержимое.

    И всё-таки зачем нужен?


    Свойство «ведра» — это, конечно, хорошо, но как это пригодится на практике? У DocumentFragment две основных области применения.

    1. Хранение кусков HTML, не имеющих общего предка.

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

    div.innerHTML = "";
    div.appendChild(fragmentWithAllContent);
    

    «Но ведь мы можем просто добавлять элементы в div сразу по мере их создания?» — спросит въедливый читатель. Можем, но так делать не стоит, и вот почему.

    2. Улучшение производительности в случае множественных вставок.

    Дело в том, что каждый раз, когда мы что-то меняем в активном DOM-дереве, браузеру приходится произвести кучу вычислений. Подробнее об этом можно почитать например, здесь. В этой статье ограничимся упоминанием того, что есть такой страшный зверь — reflow. Когда мы добавляем элемент на страницу, этот зверь просыпается и сжирает кусок процессорного времени. Если мы по очереди добавим сто элементов, зверь проснётся сто раз и сто раз сделает «кусь». Для пользователя это может быть уже вполне ощутимым «подвисанием».

    Когда мы добавляем элемент в DocumentFragment, это не вызывает reflow, потому что фрагмент не является (и в принципе не может являться) частью активного DOM-дерева. И самое главное: когда мы вставляем содержимое фрагмента с помощью appendChild или других подобных методов, независимо от того, сколько элементов внутри фрагмента, reflow вызывается только один раз.

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

    Upd: товарищ nuit поведал, что для современного Chrome мои слова уже не являются истинными. В нём reflow не выполняется раньше, чем необходимо, и благодаря этому код без DocumentFragment на самом деле срабатывает быстрее, да и с другими браузерами не всё так очевидно. Так что перед тем, как решать, использовать ли фрагменты, необходим профайлинг и исследование целевой аудитории сайта.

    Нюансы


    Есть две особенности, из-за которых новички часто испытывают трудности при использовании фрагментов. Первая: как я уже писал выше, фрагмент не является DOM-элементом. Это значит, что у него отсутствуют многие привычным методы и свойства, в частности — innerHTML. Поэтому нельзя просто так превратить строку в содержимое фрагмента. Как это сделать не просто, будет рассказано ниже.

    Вторая особенность: фрагмент при использовании «портится». Точнее — опустошается. Когда мы делаем div.appendChild(fragment), все дочерние элементы фрагмента переносятся в div. А поскольку элемент не может иметь более одного родителя, это означает, что они из фрагмента изымаются! Чтобы избежать этого поведения в случае, когда оно нежелательно, можно использовать cloneNode.

    Тег <template>


    Существует одно место, где можно встретить DocumentFragment, не создавая его через JS. Это — свойство content элемента template.

    Тэг <template> придуман специально для того, чтобы хранить куски HTML-кода, но раньше времени не нагружать ими браузер. То, что находится внутри этого тега, не становится частью активного DOM-дерева. В частности (на это новички тоже нередко напарываются), их нельзя найти с помощью querySelector. Элементы, созданные из HTML-кода, находящегося внутри тега <template>, не становятся ему дочерними. Вместо этого JavaScript может получить к ним доступ через свойство content, которое является — сюрприз! — как раз DocumentFragment'ом.

    С помощью элемента template можно создать фрагмент из строки:

    function createFragmentFromString(str){
        var template = document.createElement("template");
        template.innerHTML = str;
        return template.content;
    }
    

    Эпилог


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

    Комментарии 22

      0
      А почему ни одна современная UI библиотека, в которой задротствуют на тему производительности не использует DocumentFragment?
        0
        Для того, чтобы ответить на этот вопрос, мне пришлось бы изучить каждую «современную UI библиотека, в которой задротствуют на тему производительности». Без этого я вообще не уверен, что ваше утверждение истинно.

        В React и других фреймворках, основанных на Virtual DOM, фрагменты попросту некуда приткнуть из-за принципиального отличия алгоритмов. В Polymer используется Shadow DOM, который, по сути, является надстройкой над фрагментами, но это никак не связано с производительностью и никак не мешает использовать обычные фрагменты поверх этого. Если вас интересуют ещё какие-то библиотеки, то вам следует обратиться к кому-то, кто в них компетентен.
          0
          > В React и других фреймворках, основанных на Virtual DOM, фрагменты попросту некуда приткнуть из-за принципиального отличия алгоритмов

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

          А ещё можно открыть исходники хрома и понять что на самом деле происходит при вставке элементов:

          chromium.googlesource.com/chromium/blink/+/master/Source/core/dom/DocumentFragment.h#33
          chromium.googlesource.com/chromium/blink/+/master/Source/core/dom/ContainerNode.cpp#166
          chromium.googlesource.com/chromium/blink/+/master/Source/core/dom/ContainerNode.cpp#66
            +1
            Да, технически, я могу открыть исходники хрома. Но поскольку в последний раз я брал в руки C++ лет пять назад, когда делал другу какую-то лабу, и до этого ничего сложнее тех же лаб на нём не писал, я примерно с тем же успехом могу открыть тибетскую Книгу Мёртвых на языке оригинала.

            Если вы хотите что-то сказать, скажите, пожалуйста, прямым текстом. Я недостаточно умный, чтобы со мной имело смысл вести сократическую беседу.
              +2
              Использование фрагментов добавляет лишь дополнительный оверхэд от того что приходится создавать этот фрагмент, вставлять в него и потом перебрасывать из него в нужный элемент. Никакого рефлоу не произойдёт от вставки элементов в дом узлы, которые находятся в документе, рефлоу будет отложен до того момента когда кто-нибудь не начнёт читать свойства, которые зависят от вычислений производимых во время рефлоу.
                0
                Да, похоже, что касается свежего Хрома, вы правы. Внесу информацию в статью. Но в Firefox (свежая и не очень свежая версии) бенчмарк показывает выгоду от использования DocumentFragment.
                  +1
                  Во всех браузерах такое поведение уже очень давно. Единственная ситуация в которой с фрагментом должно быть быстрее — это бэнчмарки с плоскими листами на 10к элементов запущеные в браузере с экстеншенами типа адблока.
                    0
                    Без адблока в FF фрагмент всё равно работает быстрее, хотя преимущество и не такое впечатляющее.

                    Кстати, а в масштабах веба «очень давно» — это сколько? =)
                      +3
                      Даже не знаю точной даты, но точно ещё до того как хром форкнул вебкит.

                      Кстати, ещё интересный исторический факт — это то что в старых иешках и даже в старых версиях Edge'а, вставка элементов до того как у них отрендерены чилдрены была быстрее чем рендер вне документа и вставка.

                      Сначало случайно обнаружили это в эпоху когда пытались создать наиболее производительный vdom алгоритм github.com/localvoid/vdom-benchmark/issues/15#issuecomment-71692148, потом реактовцы добавили эту оптимизиацию github.com/sophiebits/innerhtml-vs-createelement-vs-clonenode, а сейчас наверное уже все повыкидывали этот хак :)
                        +1
                        А вы крутой.
                  0
                  Из личной практики — вставка тысяч svg-узлов через фрагмент действительно оказывалась намного быстрее, чем россыпью. Не помню уже точные цифры, но в разы.
                    0
                    Единственное отличие во вставке будет только в случае если где-то сверху висит MutationObserver[1], тогда на каждом вызове `insertBefore()` будет вызываться `dispatchSubtreeModifiedEvent()`[2] и генерировать эвент. Но даже в таком случае разница в производительности будет практически незаметна.

                    Скорее всего у вас проблемы с производительности были из-за чего-то другого.

                    1. chromium.googlesource.com/chromium/blink/+/master/Source/core/dom/Node.cpp#1989
                    2. chromium.googlesource.com/chromium/blink/+/master/Source/core/dom/ContainerNode.cpp#242
                      0
                      Фильтры в SVG пересчитывались. Кроме того, не Хромом единым…
                        0
                        Очень любопытно было бы взглянуть на пример и в каком браузере это проявлялось. Чтоб вставка нодов в свг триггерила репэйнт в любом браузере — как-то не особо верится.
            0
            Если мне не изменяет память, jQuery где-то использовала.
            UPD. Я прав: github.com/jquery/jquery/search?q=createDocumentFragment&unscoped_q=createDocumentFragment
              0
              Ключевые слова: «современная» и «UI библиотека» :) jQuery — это обёртка нормализующая работу императивного DOM api на старых браузерах.
            0
            Мне кажется, было бы правильнее пометить статью двумя тегами: XML, DOM. Было бы интересно увидеть XML, полученный в результате выполнения кода, приведенного в разделе Зачем нужен.
              0
              Ну… Там получится

              <div>
                  <div></div>
                  <div></div>
              </div>
              


              Или я неправильно понял вопрос?
                0
                Спасибо
                  0
                  Я правильно понял: при формировании XML (сериализации) DocFragment не добавляет собственные теги?
                    0
                    Именно так.
                0
                Спасибо за полезную информацию

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

                Самое читаемое