innerHTML, insertAdjacentHTML(), appendChild(), append(), prepend(), insertAdjacentElement() и replaceChildren() сравниваются.

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

Чтобы вставить новое содержимое в элемент HTML, можно выбрать один из нескольких методов целевого элемента: _8 _, _ 9_, insertAdjacentHTML(), prepend(), replaceChildren(), innerHTML, insertAdjacentHTML(). В настоящее время, похоже, нет смысла использовать appendChild(). Современный метод append() делает больше, чем его предшественник, и требует меньше вспомогательного кода.

Я буду называть innerHTML метод, чтобы сделать предложения немного короче. Но на самом деле псевдосвойство innerHTML можно рассматривать как метод, потому что это не обычное свойство, а своего рода ссылка на методы получения и установки.

По сути, есть только два способа вставить HTML в элемент HTML - как строку HTML или как узлы DOM. Перечисленные выше методы также можно разделить на две группы по типу их параметров:

  • Способы приема HTML - innerHTML, insertAdjacentHTML()
  • Методы, ожидающие объектов Node - append(), appendChild(), insertAdjacentHTML(), prepend(), replaceChildren()

Два типа методов выполняют одни и те же задачи:

  • insertAdjacentHTML() аналогичен _27 _, _ 28_, insertAdjacentHTML() или prepend()
  • innerHTML эквивалентно replaceChildren(). Когда целевой элемент пуст, innerHTML может заменить любой из _34 _, _ 35_, prepend(), replaceChildren()

Различия между аргументами HTML и узла

Многословие

В некоторых случаях код, вставляющий узлы в элементы HTML, явно более подробен. Чтобы проиллюстрировать проблему, представим себе веб-приложение, которое отображает некоторые данные в виде таблицы, состоящей из div: таблица - это div, каждая строка - это div, каждая ячейка - это div. Строка содержит много ячеек divs, таблица содержит много строк divs. Ячейка divs содержит titles, поэтому пользователи могут читать содержимое любой ячейки, даже если содержимое не помещается в ячейку. Для визуализации исходные данные в любом формате преобразуются в двумерный массив, каждое значение которого соответствует ячейке таблицы. Массив можно отобразить в виде таблицы с помощью относительно удобочитаемого кода на основе innerHTML:

// innerHTML.js
function row(vals) {
   return '<div class="row">'
      + vals.map(val => `<div title="${val}">${val}</div>`).join('')
      + '</div>';
}
function table(data) {
    return data.map(vals => row(vals)).join('');
}
function render(data) {
    root.innerHTML = table(data);
}

Сравните приведенный выше код с эквивалентным кодом, в котором каждое значение массива явно преобразуется в узел DOM:

//  append.js
function row(vals) {
    const rowDiv = document.createElement("div");
    rowDiv.className = 'row';
    rowDiv.append(...vals.map(val => {
        const cell = document.createElement("div");
        cell.title = val;
        cell.append(val);
        return cell;
    }));
    return rowDiv;
}
function table(data) {
    return data.map(vals => row(vals));
}
function render(data) {
    root.append(...table(data));
}

Но не заблуждайтесь, код на основе узлов не всегда относительно многословен. В некоторых случаях код на основе узлов может быть более кратким, чем код на основе строк. Например, создание форм из input элементов может быть проще, чем объединение их HTML.

Безопасность при отображении пользовательского ввода

Методы, ожидающие узлов DOM в качестве аргументов, более безопасны при использовании для отображения пользовательского ввода. Когда innerHTML или insertAdjacentHTML() используются для отображения пользовательского ввода, пользователи могут отправлять нежелательный код HTML или даже JavaScript, который в случае сохранения может быть выполнен в другом пользовательском сеансе. В приведенном ниже коде показано, как можно выполнить JavaScript с использованием innerHTML или insertAdjacentHTML():

// security.js
root.innerHTML = `<img src=/ onerror='console.log("innerHTML")'>`;
root.insertAdjacentHTML('beforeend',
    `<img src=/ onerror='console.log("insertAdjacentHTML")'>`);

Каждый метод выводит сообщение в консоль:

Какой способ построить DOM быстрее - с помощью HTML или узлов?

Глядя на перечисленные выше преимущества, не так очевидно, какие методы в целом лучше. Может быть, некоторые из них более производительны?

В этом посте я сравниваю производительность семи упомянутых выше методов. Я повторно воспользуюсь подходом, который подробно описал в предыдущем сообщении о производительности appendChild() и innerHTML. Чтобы протестировать дополнительные методы, я лишь немного скорректировал код, описанный в предыдущем посте. Здесь я кратко повторю лишь существенные детали.

В предыдущем абзаце о многословности я описал, как я оспариваю каждый метод - я использую его для построения таблицы на основе div, включающей 5000 строк с 20 ячейками в каждой строке. Текстовое значение и title каждой ячейки - это номер столбца, отделенный запятой от номера строки.

Код, специфичный для метода, очень похож. Например, показанный выше код innerHTML.js отличается от insertAdjacentHTML.js только функцией render():

// insertAdjacentHTML.js
...
function render(data) {
    root.insertAdjacentHTML('beforeend', table(data));
}

prepend.js и replaceChildren.js отличаются от показанного выше append.js только методом, используемым вместо append(). Вы можете увидеть полный код в https://github.com/marianc000/HTMLvsNodes или в источнике https://nodesvshtml.onrender.com/.

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

// shared.js
...
function execute(label, render) {
    return new Promise(resolve => {
        requestAnimationFrame(() => {
            const start = Date.now();
            render(data);
            const domDone = Date.now();
            setTimeout(() => {
                addResult(label, start, domDone, Date.now());
                setTimeout(resolve);
            });
        });
    });
}

В приведенном выше коде DOM изменяется в течение domDone-start миллисекунд. Так как задача была поставлена ​​в очередь requestAnimationFrame(), обязательно сразу же будет покраска. После того, как браузер визуализирует модель DOM, следующая задача в очереди записывает время - миллисекунды, использованные для рисования, равны Date.now()-domDone. Если вы не уверены, как это работает, прочтите мой предыдущий пост и посмотрите рекомендованное там видео, потому что это самая важная концепция в веб-разработке.

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

Таким образом, методы, принимающие только узлы, а именно appendChild() и insertAdjacentElement(), кажутся немного на

Время рисования одного и того же дерева узлов должно быть одинаковым. И это. Разница (3551-3388) /3388=4.8% незначительна и, скорее, вызвана большим количеством фоновых работ, обрабатываемых на компьютере во время теста.

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

Время рисования можно сократить в несколько раз, используя свойство CSS content-visibility. Когда свойство установлено, браузер будет рисовать не все дерево документа, а только его часть, которая прокручивается до представления. Тем не менее, рисование изменений в DOM будет оставаться в 2–3 раза дольше, чем изменение DOM .

Выводы

Все протестированные методы кажутся одинаково эффективными. Максимальная разница в производительности, которую я наблюдал, составляет

Итак, код без innerHTML, insertAdjacentHTML(), несомненно, более безопасен, но он не намного быстрее и может быть немного более подробным. В целом и только из соображений безопасности кажется, что лучше использовать методы с аргументами узла, особенно вспомогательные методы append(), prepend() и replaceChildren().

Вы можете скачать исходный код с https://github.com/marianc000/HTMLvsNodes или посмотреть, как это работает https://nodesvshtml.onrender.com/. Если вы подождете достаточно долго ~ 15 минут, вы увидите две таблицы с результатами, аналогичными результатам на скриншотах выше.