Дерево значений 1С удобно для иерархий: структура подразделений, номенклатура по группам, спецификации. Тормозить оно начинает по трем причинам: в форме дерево целиком гоняется между клиентом и сервером, обход делается глубокой рекурсией с лишними вызовами, а поиск — ручным перебором. Решения: подгружать узлы при раскрытии, обходить дерево циклом со стеком или по уровням, искать через НайтиСтроки или заранее собранное соответствие, а само дерево строить запросом с итогами или СКД.
Ниже — каждый прием с кодом, плюс честные границы: где рекурсия вполне допустима, а где действительно опасна.
Почему дерево значений тормозит
| Симптом | Причина | Решение |
|---|---|---|
| Форма долго открывается | Дерево заполняется целиком, все уровни сразу | Ленивая загрузка узлов |
| Любая кнопка «думает» | Контекстный серверный вызов передает все дерево | Внеконтекстные вызовы, &НаСервереБезКонтекста |
| Обход занимает минуты | Рекурсия и запросы внутри обхода | Итеративный обход, один запрос на уровень |
| Поиск медленный | Перебор Для Каждого по всем строкам | НайтиСтроки(..., Истина) или соответствие |
| Ошибка на глубокой иерархии | Слишком глубокая рекурсия или цикл в данных | Обход без рекурсии, контроль посещенных |
Главное правило: дерево значений — объект для отображения и небольших структур. Считать большие объемы лучше в запросе, СКД или таблице значений.
Ленивая загрузка дерева в форме
Идея: при открытии формы загружаем только верхний уровень. Чтобы у групп отрисовался «плюс», добавляем им одну пустую строку-маркер. При раскрытии узла заменяем маркер реальными строками.
Первый уровень
&НаСервере
Процедура ЗаполнитьВерхнийУровень()
Корень = ДеревоНоменклатуры.ПолучитьЭлементы();
Корень.Очистить();
Для Каждого ДанныеСтроки Из ДочерниеЭлементы(Справочники.Номенклатура.ПустаяСсылка()) Цикл
ДобавитьСтрокуДерева(Корень, ДанныеСтроки);
КонецЦикла;
КонецПроцедуры
&НаКлиентеНаСервереБезКонтекста
Процедура ДобавитьСтрокуДерева(Коллекция, ДанныеСтроки)
НоваяСтрока = Коллекция.Добавить();
ЗаполнитьЗначенияСвойств(НоваяСтрока, ДанныеСтроки);
Если ДанныеСтроки.ЭтоГруппа Тогда
// Пустая строка-маркер: платформа нарисует «плюс» у группы.
НоваяСтрока.ПолучитьЭлементы().Добавить();
КонецЕсли;
КонецПроцедурыПодгрузка при раскрытии узла
В событии таблицы ПередРазворачиванием параметр Строка — идентификатор строки. Проверяем, лежит ли под узлом только маркер, и получаем дочерние элементы внеконтекстным вызовом: на сервер уходит одна ссылка, а не вся форма с деревом.
&НаКлиенте
Процедура ДеревоНоменклатурыПередРазворачиванием(Элемент, Строка, Отказ)
Узел = ДеревоНоменклатуры.НайтиПоИдентификатору(Строка);
Дочерние = Узел.ПолучитьЭлементы();
ЕстьТолькоМаркер = Дочерние.Количество() = 1 И Не ЗначениеЗаполнено(Дочерние[0].Ссылка);
Если Не ЕстьТолькоМаркер Тогда
Возврат;
КонецЕсли;
Дочерние.Очистить();
Для Каждого ДанныеСтроки Из ДочерниеЭлементы(Узел.Ссылка) Цикл
ДобавитьСтрокуДерева(Дочерние, ДанныеСтроки);
КонецЦикла;
КонецПроцедуры
&НаСервереБезКонтекста
Функция ДочерниеЭлементы(Родитель)
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Номенклатура.Ссылка КАК Ссылка,
| Номенклатура.Наименование КАК Наименование,
| Номенклатура.ЭтоГруппа КАК ЭтоГруппа
|ИЗ
| Справочник.Номенклатура КАК Номенклатура
|ГДЕ
| Номенклатура.Родитель = &Родитель
|УПОРЯДОЧИТЬ ПО
| ЭтоГруппа УБЫВ,
| Наименование";
Запрос.УстановитьПараметр("Родитель", Родитель);
Результат = Новый Массив;
Выборка = Запрос.Выполнить().Выбрать();
Пока Выборка.Следующий() Цикл
Результат.Добавить(Новый Структура("Ссылка, Наименование, ЭтоГруппа",
Выборка.Ссылка, Выборка.Наименование, Выборка.ЭтоГруппа));
КонецЦикла;
Возврат Результат;
КонецФункцииОтличие от «наивной» реализации — именно &НаСервереБезКонтекста. Если подгружать узел контекстной серверной процедурой, платформа на каждый клик отправит на сервер и обратно все уже раскрытые ветки, и ленивая загрузка потеряет половину смысла.
Обход дерева значений без рекурсии
Рекурсия на дереве значений сама по себе не грех: для справочника глубиной 5–10 уровней она читается проще и работает нормально. Проблемы начинаются, когда:
- глубина заранее не известна и может быть очень большой — растет стек вызовов, и процедура может упасть с ошибкой;
- данные строятся по ссылкам, где возможен цикл (сотрудник указан руководителем своего руководителя) — рекурсия уходит в бесконечность;
- внутри каждого вызова выполняется запрос — тормозит уже не рекурсия, а запросы.
Обход в глубину через стек
// Обходит все строки дерева без рекурсии (в глубину).
Процедура ОбойтиДерево(Дерево) Экспорт
Стек = Новый Массив;
Для Каждого СтрокаДерева Из Дерево.Строки Цикл
Стек.Добавить(СтрокаДерева);
КонецЦикла;
Пока Стек.Количество() > 0 Цикл
ПоследнийИндекс = Стек.ВГраница();
ТекущаяСтрока = Стек[ПоследнийИндекс];
Стек.Удалить(ПоследнийИндекс);
ОбработатьСтроку(ТекущаяСтрока);
Для Каждого Подчиненная Из ТекущаяСтрока.Строки Цикл
Стек.Добавить(Подчиненная);
КонецЦикла;
КонецЦикла;
КонецПроцедурыПорядок обработки здесь обратный порядку строк на уровне. Если важен исходный порядок, добавляйте подчиненные строки в стек с конца.
Обход по уровням (в ширину)
Обход по уровням удобен, когда для всего уровня можно выполнить один запрос, а не запрос на каждую строку.

Пример — разложить дерево подчиненности «руководитель → сотрудники» в плоскую таблицу пар со всеми вышестоящими руководителями и уровнями:
// Раскладывает дерево подчиненности в таблицу пар «руководитель — сотрудник»
// по всем уровням иерархии, без рекурсии.
//
// Параметры:
// Дерево - ДеревоЗначений - с колонкой Сотрудник.
//
// Возвращаемое значение:
// ТаблицаЗначений - Руководитель, Сотрудник, УровеньРуководителя, УровеньСотрудника.
//
Функция ПарыРуководительСотрудник(Знач Дерево) Экспорт
Результат = Новый ТаблицаЗначений;
Результат.Колонки.Добавить("Руководитель");
Результат.Колонки.Добавить("Сотрудник");
Результат.Колонки.Добавить("УровеньРуководителя", Новый ОписаниеТипов("Число"));
Результат.Колонки.Добавить("УровеньСотрудника", Новый ОписаниеТипов("Число"));
ТекущийУровень = Новый Массив;
Для Каждого СтрокаДерева Из Дерево.Строки Цикл
ТекущийУровень.Добавить(СтрокаДерева);
КонецЦикла;
Пока ТекущийУровень.Количество() > 0 Цикл
// Здесь можно одним запросом получить данные для всего уровня.
СледующийУровень = Новый Массив;
Для Каждого СтрокаДерева Из ТекущийУровень Цикл
Если ЗначениеЗаполнено(СтрокаДерева.Сотрудник) Тогда
Родитель = СтрокаДерева.Родитель;
Пока Родитель <> Неопределено Цикл
НоваяПара = Результат.Добавить();
НоваяПара.Руководитель = Родитель.Сотрудник;
НоваяПара.Сотрудник = СтрокаДерева.Сотрудник;
НоваяПара.УровеньРуководителя = Родитель.Уровень();
НоваяПара.УровеньСотрудника = СтрокаДерева.Уровень();
Родитель = Родитель.Родитель;
КонецЦикла;
КонецЕсли;
Для Каждого Подчиненная Из СтрокаДерева.Строки Цикл
СледующийУровень.Добавить(Подчиненная);
КонецЦикла;
КонецЦикла;
ТекущийУровень = СледующийУровень;
КонецЦикла;
Возврат Результат;
КонецФункцииКогда дерево строится не из ДеревоЗначений, а по ссылкам (Руководитель в справочнике сотрудников), заведите Соответствие уже посещенных элементов и пропускайте повторы. Это одна строка кода, которая спасает от бесконечного цикла на «кривых» данных.
Поиск в дереве значений
- Разовый поиск:
Дерево.Строки.НайтиСтроки(Новый Структура("Ссылка", Значение), Истина). Второй параметр включает поиск по подчиненным строкам на всех уровнях. Перебор выполняет платформа, а не интерпретатор, поэтому это быстрее ручного цикла. - Много поисков подряд: один раз обойдите дерево и соберите
Соответствие«ключ — строка дерева». Дальше каждый поиск — обращение по ключу. - Только первое совпадение:
Найти(Значение, "Колонка", Истина)у коллекции строк дерева.
Не ищите в дереве внутри цикла по другой большой коллекции через НайтиСтроки — это квадратичная сложность. Соответствие здесь выигрывает всегда.
Строим дерево средствами платформы
Самая быстрая ручная сборка дерева — та, которой нет.
Запрос с итогами. Если нужна иерархия с итогами, используйте секцию ИТОГИ ПО и выгрузку результата сразу в дерево:
ВЫБРАТЬ
Продажи.Номенклатура КАК Номенклатура,
СУММА(Продажи.Сумма) КАК Сумма
ИЗ
РегистрНакопления.Продажи КАК Продажи
СГРУППИРОВАТЬ ПО
Продажи.Номенклатура
ИТОГИ
СУММА(Сумма)
ПО
Номенклатура ИЕРАРХИЯДерево = Запрос.Выполнить().Выгрузить(ОбходРезультатаЗапроса.ПоГруппировкамСИерархией);СКД. Для сложных отчетов с несколькими уровнями группировок настройте структуру в схеме компоновки и выведите результат в ДеревоЗначений через ПроцессорВыводаРезультатаКомпоновкиДанныхВКоллекциюЗначений (макет строится генератором для коллекции значений). Группировки, итоги и иерархию соберет платформа.
Оформление. Вместо сложного условного оформления с вложенными И/ИЛИ по нескольким колонкам рассчитайте в данных одну колонку-признак (например, индекс цвета) и сделайте по ней простое условие. Отрисовка большого дерева от этого заметно легче.
Типичные ошибки
- Заполнение всего справочника в дерево формы при открытии.
- Подгрузка узла контекстным серверным вызовом.
- Проверка маркера как
Дочерние.Ссылка.Пустая()— у коллекции нет поляСсылка, проверять нужно первую строку. - Запрос внутри рекурсивной процедуры на каждый узел.
- Поиск
НайтиСтрокибез второго параметра — ищет только на верхнем уровне. - Отсутствие защиты от циклов при построении иерархии по ссылкам.
Если формы и отчеты с иерархиями работают медленно, начните с замера: часто узкое место не в дереве, а в запросах вокруг него. Такие разборы я провожу в рамках аудита производительности 1С, а переделку отчетов на СКД — в услуге отчеты на СКД.
Коротко
- Дерево значений 1С в форме тормозит из-за передачи целиком: грузите узлы при раскрытии и вызывайте сервер без контекста.
- Обход без рекурсии — массив-стек или очередь уровней; для данных по ссылкам добавьте контроль посещенных.
- Ищите через
НайтиСтроки(..., Истина), а при массовом поиске — через соответствие. - Иерархию с итогами стройте запросом с
ИТОГИ ПОили СКД, а не вручную.




