Add document.getElementById('add').onclick = () => document.getElementById('list').innerHTML += 'Item' Проанализируйте проблемы с производительностью, управлением состоянием, доступностью (a11y) и безопасностью (XSS), предложите улучшения для масштабируемости и доступности (использование виртуизации списка, event delegation, управление фокусом, ARIA), и сравните подходы в чистом JS и в фреймворках типа React/Vue
Коротко — сначала проблемы в исходном коде, затем практические улучшения и сравнение подходов.
Проблемы
Производительность: Использование innerHTML += каждый раз заставляет браузер парсить/рендерить содержимое контейнера заново — дорого при большом числе элементов (амортизированная стоимость (O(n)) на каждое добавление).При большом количестве DOM-элементов интерфейс тормозит (репайнты/reflows).Управление состоянием: Нет централизованной модели списка — нельзя легко читать/фильтровать/персистить элементы.Нет механизма пакетного обновления (частые мелкие изменения вызывают много рендеров).Доступность (a11y): <li tabindex="0">Item</li> — элемент-лист просто получает фокус, но отсутствует семантика управления (кнопки, роли, управляемая клавиатура, объявления для скринридеров).Нет управления фокусом при добавлении/удалении, нет клавиатурной навигации (стрелки, Home/End).Нет ARIA-атрибутов для динамических изменений (например aria-live, role="list"/role="listitem" там, где нужно).Безопасность (XSS): innerHTML += с пользовательскими данными приводит к XSS, если данные не очищены.
Рекомендации по улучшению (с практическими приёмами) 1) Безопасное добавление DOM
Использовать DOM API вместо innerHTML: const li = document.createElement('li'); li.textContent = text; list.appendChild(li);Если добавляете много элементов — собирать в DocumentFragment и один appendChild(fragment).
2) Event delegation
Привязывать один обработчик к контейнеру: list.addEventListener('click', e => { const item = e.target.closest('li'); if (!item) return; /* ... */ });Это экономит память и упрощает управление.
3) Управление состоянием
Хранить массив данных модели, рендерить из него; при изменениях обновлять модель и синхронизировать DOM.Для частых изменений — батчить обновления через requestAnimationFrame или microtask.
4) Виртуализация списка (windowing) для масштабируемости
При (N{total}) очень большом рендерьте только диапазон видимых элементов (N{visible}), где (N{visible} \ll N{total}).Подходы: Реализовать окно на scroll: вычислять start/end по высоте/скроллу, рисовать только эти элементы, задавать padding/margin для имитации полной высоты.Использовать готовые либы: в React — react-window/react-virtualized, в Vue — vue-virtual-scroller.Выгода: количество DOM-узлов остаётся примерно постоянным ( \sim N_{visible} ), плавный скролл и меньшая память.
5) Управление фокусом и клавиатурная навигация
При добавлении/удалении явно управлять фокусом: newItem.focus() или вернуть фокус на соседний элемент.Реализовать клавиатурные обработчики: Стрелки вверх/вниз для перехода между элементами.Home/End для перехода к началу/концу.Enter/Space для активации элемента (если элемент интерактивный — использовать <button> внутри <li>).Использовать семантику: если элемент интерактивный — делайте внутри <button> или <a>, а не только tabindex.
6) ARIA и динамические обновления
Поместите role="list" на контейнер при необходимости, role="listitem" для элементов, если используете нестандартную разметку.Для объявлений о новых элементах можно использовать aria-live="polite" на вспомогательном элементе.Обновляйте aria-selected, aria-activedescendant при навигации клавиатурой.
7) Безопасность (XSS)
Всегда вставляйте пользовательские строки в textContent или через безопасный шаблонизатор.Если нужно вставлять HTML — сначала прогоняйте через sanitizer (например DOMPurify).
Код — улучшенный vanilla JS (минимум) const list = document.getElementById('list'); document.getElementById('add').addEventListener('click', () => { const li = document.createElement('li'); li.setAttribute('tabindex', '0'); li.textContent = 'Item'; list.appendChild(li); // безопасно и без innerHTML li.focus(); // фокус на новый элемент }); // делегируем клики/кейборд list.addEventListener('click', e => { const li = e.target.closest('li'); if (!li) return; // обработка }); list.addEventListener('keydown', e => { // обработка стрелок, Home/End, Enter/Space });
Производительность: React/Vue: виртуальный DOM / реактивность сокращают ручные манипуляции, но при экстремально больших списках всё равно нужна виртуализация.Vanilla: при аккуратной реализации (delegation, fragments, virtualization) можно получить очень хорошую производительность без накладных расходов фреймворка.Управление состояния: Фреймворки дают встроенные модели состояния (hooks/computed), удобное обновление UI при изменениях.Vanilla требует явной модели и синхронизации, но не навязывает структуру.Безопасность: React/Vue по умолчанию экранируют контент в шаблонах; XSS возможен только при явном использовании HTML вставки (dangerouslySetInnerHTML и др.).В vanilla нужно следить самому — использовать textContent/sanitizer.Доступность: Фреймворки не решают a11y автоматически; есть библиотеки/компоненты (Reach UI, Vue A11y) которые помогают.Vanilla позволяет тонкий контроль над DOM и фокусом — но нужно вручную реализовать ARIA/кейборд.Экосистема: Готовые виртуализаторы, aria-утилиты, тесты — проще найти в экосистеме React/Vue.Vanilla требует собрать решения самому или подключить маленькие либы.
Краткий чеклист для внедрения
Заменить innerHTML += на createElement/textContent/DocumentFragment.Делегировать события на контейнер.Ввести модель данных (массив) и батчить обновления.Для больших списков добавить виртуализацию (рисуйте только видимые элементы).Использовать aria-*, role, semantic markup; реализовать клавиатурную навигацию и управлять фокусом при изменениях.Никогда не вставлять непроверенный HTML без санации (DOMPurify и т.п.).
Если нужно — могу прислать компактную реализацию виртуализатора на чистом JS или пример на React с react-window и реализацией клавиатурной навигации.
Коротко — сначала проблемы в исходном коде, затем практические улучшения и сравнение подходов.
Проблемы
Производительность:Использование innerHTML += каждый раз заставляет браузер парсить/рендерить содержимое контейнера заново — дорого при большом числе элементов (амортизированная стоимость (O(n)) на каждое добавление).При большом количестве DOM-элементов интерфейс тормозит (репайнты/reflows).Управление состоянием:
Нет централизованной модели списка — нельзя легко читать/фильтровать/персистить элементы.Нет механизма пакетного обновления (частые мелкие изменения вызывают много рендеров).Доступность (a11y):
<li tabindex="0">Item</li> — элемент-лист просто получает фокус, но отсутствует семантика управления (кнопки, роли, управляемая клавиатура, объявления для скринридеров).Нет управления фокусом при добавлении/удалении, нет клавиатурной навигации (стрелки, Home/End).Нет ARIA-атрибутов для динамических изменений (например aria-live, role="list"/role="listitem" там, где нужно).Безопасность (XSS):
innerHTML += с пользовательскими данными приводит к XSS, если данные не очищены.
Рекомендации по улучшению (с практическими приёмами)
Использовать DOM API вместо innerHTML:1) Безопасное добавление DOM
const li = document.createElement('li'); li.textContent = text; list.appendChild(li);Если добавляете много элементов — собирать в DocumentFragment и один appendChild(fragment).
2) Event delegation
Привязывать один обработчик к контейнеру:list.addEventListener('click', e => { const item = e.target.closest('li'); if (!item) return; /* ... */ });Это экономит память и упрощает управление.
3) Управление состоянием
Хранить массив данных модели, рендерить из него; при изменениях обновлять модель и синхронизировать DOM.Для частых изменений — батчить обновления через requestAnimationFrame или microtask.4) Виртуализация списка (windowing) для масштабируемости
При (N{total}) очень большом рендерьте только диапазон видимых элементов (N{visible}), где (N{visible} \ll N{total}).Подходы:Реализовать окно на scroll: вычислять start/end по высоте/скроллу, рисовать только эти элементы, задавать padding/margin для имитации полной высоты.Использовать готовые либы: в React — react-window/react-virtualized, в Vue — vue-virtual-scroller.Выгода: количество DOM-узлов остаётся примерно постоянным ( \sim N_{visible} ), плавный скролл и меньшая память.
5) Управление фокусом и клавиатурная навигация
При добавлении/удалении явно управлять фокусом: newItem.focus() или вернуть фокус на соседний элемент.Реализовать клавиатурные обработчики:Стрелки вверх/вниз для перехода между элементами.Home/End для перехода к началу/концу.Enter/Space для активации элемента (если элемент интерактивный — использовать <button> внутри <li>).Использовать семантику: если элемент интерактивный — делайте внутри <button> или <a>, а не только tabindex.
6) ARIA и динамические обновления
Поместите role="list" на контейнер при необходимости, role="listitem" для элементов, если используете нестандартную разметку.Для объявлений о новых элементах можно использовать aria-live="polite" на вспомогательном элементе.Обновляйте aria-selected, aria-activedescendant при навигации клавиатурой.7) Безопасность (XSS)
Всегда вставляйте пользовательские строки в textContent или через безопасный шаблонизатор.Если нужно вставлять HTML — сначала прогоняйте через sanitizer (например DOMPurify).Код — улучшенный vanilla JS (минимум)
const list = document.getElementById('list');
document.getElementById('add').addEventListener('click', () => {
const li = document.createElement('li');
li.setAttribute('tabindex', '0');
li.textContent = 'Item';
list.appendChild(li); // безопасно и без innerHTML
li.focus(); // фокус на новый элемент
});
// делегируем клики/кейборд
list.addEventListener('click', e => {
const li = e.target.closest('li');
if (!li) return;
// обработка
});
list.addEventListener('keydown', e => {
// обработка стрелок, Home/End, Enter/Space
});
Virtualization — краткая схема (vanilla)
Измерьте высоту элемента (например itemHeight).На событии scroll вычисляйте start = Math.floor(scrollTop / itemHeight), end = start + visibleCount + buffer.Рендерьте элементы [start..end], контейнеру задавайте padding-top = start * itemHeight, padding-bottom = (N - end) * itemHeight.Сравнение: чистый JS vs React/Vue
Производительность:React/Vue: виртуальный DOM / реактивность сокращают ручные манипуляции, но при экстремально больших списках всё равно нужна виртуализация.Vanilla: при аккуратной реализации (delegation, fragments, virtualization) можно получить очень хорошую производительность без накладных расходов фреймворка.Управление состояния:
Фреймворки дают встроенные модели состояния (hooks/computed), удобное обновление UI при изменениях.Vanilla требует явной модели и синхронизации, но не навязывает структуру.Безопасность:
React/Vue по умолчанию экранируют контент в шаблонах; XSS возможен только при явном использовании HTML вставки (dangerouslySetInnerHTML и др.).В vanilla нужно следить самому — использовать textContent/sanitizer.Доступность:
Фреймворки не решают a11y автоматически; есть библиотеки/компоненты (Reach UI, Vue A11y) которые помогают.Vanilla позволяет тонкий контроль над DOM и фокусом — но нужно вручную реализовать ARIA/кейборд.Экосистема:
Готовые виртуализаторы, aria-утилиты, тесты — проще найти в экосистеме React/Vue.Vanilla требует собрать решения самому или подключить маленькие либы.
Краткий чеклист для внедрения
Заменить innerHTML += на createElement/textContent/DocumentFragment.Делегировать события на контейнер.Ввести модель данных (массив) и батчить обновления.Для больших списков добавить виртуализацию (рисуйте только видимые элементы).Использовать aria-*, role, semantic markup; реализовать клавиатурную навигацию и управлять фокусом при изменениях.Никогда не вставлять непроверенный HTML без санации (DOMPurify и т.п.).Если нужно — могу прислать компактную реализацию виртуализатора на чистом JS или пример на React с react-window и реализацией клавиатурной навигации.