Оптимизация селекторов

Библиотека Tabbable определяет элементы, которые могут получать фокус при навигации с клавиатуры. Основная задача библиотеки — найти элементы, которые:

  • могут быть сфокусированы (focusable)
  • участвуют в табуляции (tabbable)

Для поиска элементов библиотека активно использует CSS-селекторы и обход DOM-дерева. При работе с большими интерфейсами эффективность этих селекторов напрямую влияет на производительность.

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

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

  • уменьшение количества проверяемых элементов
  • сокращение числа DOM-операций
  • ускорение вычисления фокусируемых элементов
  • снижение нагрузки при повторных вызовах функций

Базовый механизм поиска элементов

Tabbable использует список селекторов, соответствующих элементам, потенциально способным принимать фокус.

Примеры таких элементов:

  • <input>
  • <select>
  • <textarea>
  • <button>
  • <a href>
  • <audio controls>
  • <video controls>
  • элементы с атрибутом tabindex
  • contenteditable

В библиотеке применяется объединённый селектор:

const candidateSelector = `
  input,
  select,
  textarea,
  a[href],
  button,
  [tabindex],
  audio[controls],
  video[controls],
  [contenteditable]
`;

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

Дальнейшая логика фильтрует найденные элементы по дополнительным условиям:

  • видимость
  • доступность
  • состояние disabled
  • корректность tabindex

Проблемы неоптимальных селекторов

Чрезмерно широкие селекторы

Использование слишком общих селекторов приводит к избыточному количеству кандидатов.

Пример:

document.querySelectorAll("*")

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

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


Избыточные вложенные селекторы

Сложные цепочки увеличивают стоимость поиска.

Неоптимальный пример:

.container div button

Браузеру требуется:

  1. найти .container
  2. найти все div
  3. внутри них искать button

При большом количестве элементов это замедляет выполнение.

Более эффективный вариант:

.container button

или

.container [tabindex], .container button

Повторные запросы DOM

Частое выполнение querySelectorAll с одинаковыми селекторами увеличивает нагрузку.

Неоптимально:

function getFocusable(container) {
  return container.querySelectorAll(candidateSelector);
}

Если функция вызывается много раз, каждый вызов запускает новый обход DOM.

Оптимизация заключается в кэшировании результатов или ограничении области поиска.


Ограничение области поиска

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

Вместо:

document.querySelectorAll(candidateSelector)

используется:

container.querySelectorAll(candidateSelector)

Это уменьшает количество проверяемых элементов.

Пример

import { tabbable } from "tabbable";

const modal = document.querySelector(".modal");

const elements = tabbable(modal);

В данном случае библиотека анализирует только элементы внутри модального окна, а не весь документ.

Преимущества:

  • уменьшение количества DOM-узлов
  • ускорение выполнения
  • снижение затрат памяти

Разделение селекторов

Вместо одного длинного селектора иногда выгоднее использовать несколько специализированных запросов.

Неоптимальный вариант:

container.querySelectorAll(`
  input,
  select,
  textarea,
  button,
  a[href],
  [tabindex],
  [contenteditable]
`)

Альтернативный подход:

const inputs = container.querySelectorAll("input, select, textarea");
const buttons = container.querySelectorAll("button");
const links = container.querySelectorAll("a[href]");

Это может улучшить производительность, если:

  • определённые типы элементов используются значительно чаще
  • требуется дополнительная фильтрация

Однако Tabbable использует объединённый селектор, поскольку современные браузеры оптимизируют такие запросы.


Исключающие селекторы

Часто требуется исключать элементы, которые заведомо не могут быть сфокусированы.

Пример:

button:not([disabled])

или

a[href]:not([tabindex="-1"])

Такая оптимизация переносит часть логики фильтрации на уровень селектора, уменьшая объём работы JavaScript.

Однако чрезмерное использование :not() усложняет селектор и может снизить производительность.

Баланс достигается сочетанием:

  • умеренно сложных селекторов
  • дополнительной фильтрации в коде

Фильтрация после поиска

Tabbable использует стратегию:

  1. быстрый поиск кандидатов
  2. точная фильтрация

Это снижает стоимость селектора.

Пример фильтрации:

function isFocusable(node) {
  if (node.disabled) return false;
  if (node.hidden) return false;
  return true;
}

Затем выполняется фильтрация:

const candidates = container.querySelectorAll(candidateSelector);

const focusable = Array.from(candidates).filter(isFocusable);

Такой подход:

  • сохраняет селекторы простыми
  • переносит сложную логику в JavaScript
  • повышает гибкость

Оптимизация проверки tabindex

Элементы с tabindex требуют дополнительной проверки.

Пример:

<div tabindex="0"></div>

Однако значение tabindex может быть:

  • положительным
  • нулевым
  • отрицательным

Tabbable проверяет:

const tabindex = node.getAttribute("tabindex");

И затем:

if (tabindex !== null && parseInt(tabindex, 10) < 0) {
  return false;
}

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

[tabindex]

Это позволяет избежать анализа элементов без атрибута.


Исключение скрытых элементов

Скрытые элементы не должны участвовать в табуляции.

Однако определение видимости через CSS-селекторы невозможно полностью. Проверяются свойства:

  • display: none
  • visibility: hidden
  • hidden
  • inert

Tabbable выполняет дополнительные проверки:

function isHidden(node) {
  return node.offsetParent === null;
}

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


Минимизация reflow и repaint

Некоторые проверки могут вызывать reflow, например:

  • offsetWidth
  • offsetHeight
  • getBoundingClientRect

Частые обращения к этим свойствам замедляют работу.

Оптимизация:

  • выполнять такие проверки после первичной фильтрации
  • уменьшать количество элементов, для которых выполняются измерения

Использование TreeWalker

В некоторых случаях обход DOM может выполняться через TreeWalker.

Пример:

const walker = document.createTreeWalker(
  container,
  NodeFilter.SHOW_ELEMENT,
  null
);

Этот подход позволяет:

  • обходить дерево без селекторов
  • фильтровать элементы по мере обхода

Однако querySelectorAll обычно быстрее благодаря внутренним оптимизациям браузера.


Кэширование селекторов

Селекторы и регулярные проверки могут кэшироваться.

Пример:

const selector = candidateSelector;

или

const candidates = container.querySelectorAll(selector);

Повторное создание строк селекторов увеличивает нагрузку на интерпретатор.


Уменьшение количества преобразований коллекций

querySelectorAll возвращает NodeList.

Преобразование:

Array.from(nodeList)

выполняет копирование элементов.

В оптимизированном коде:

  • преобразование выполняется только при необходимости
  • часть операций выполняется напрямую с NodeList

Ленивая обработка элементов

При большом количестве элементов полезна ленивая фильтрация.

Пример:

for (const node of candidates) {
  if (isFocusable(node)) {
    result.push(node);
  }
}

Этот подход:

  • избегает создания промежуточных массивов
  • снижает нагрузку на сборщик мусора

Практические рекомендации

Эффективная работа селекторов в Tabbable достигается соблюдением нескольких принципов.

Ограничение области поиска

Всегда использовать конкретный контейнер.

Простые селекторы

Избегать длинных и сложных цепочек.

Минимальная фильтрация селектором

Часть логики переносить в JavaScript.

Избегание универсальных селекторов

Не использовать *.

Сокращение DOM-операций

Минимизировать количество вызовов querySelectorAll.

Фильтрация после поиска

Применять дополнительные проверки только к потенциальным кандидатам.


Производительность в крупных интерфейсах

В приложениях с большим DOM оптимизация селекторов влияет на:

  • скорость открытия модальных окон
  • навигацию по формам
  • доступность интерфейсов
  • работу библиотек управления фокусом

Tabbable используется в таких инструментах, как:

  • системы модальных окон
  • компоненты accessibility
  • focus-trap библиотеки
  • сложные SPA-интерфейсы

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

Грамотное построение селекторов и фильтрации позволяет значительно ускорить определение tabbable-элементов даже в очень больших DOM-структурах.