Дерево значений 1С удобно для иерархий: структура подразделений, номенклатура по группам, спецификации. Тормозить оно начинает по трем причинам: в форме дерево целиком гоняется между клиентом и сервером, обход делается глубокой рекурсией с лишними вызовами, а поиск — ручным перебором. Решения: подгружать узлы при раскрытии, обходить дерево циклом со стеком или по уровням, искать через НайтиСтроки или заранее собранное соответствие, а само дерево строить запросом с итогами или СКД.

Ниже — каждый прием с кодом, плюс честные границы: где рекурсия вполне допустима, а где действительно опасна.

Почему дерево значений тормозит

СимптомПричинаРешение
Форма долго открываетсяДерево заполняется целиком, все уровни сразуЛенивая загрузка узлов
Любая кнопка «думает»Контекстный серверный вызов передает все деревоВнеконтекстные вызовы, &НаСервереБезКонтекста
Обход занимает минутыРекурсия и запросы внутри обходаИтеративный обход, один запрос на уровень
Поиск медленныйПеребор Для Каждого по всем строкамНайтиСтроки(..., Истина) или соответствие
Ошибка на глубокой иерархииСлишком глубокая рекурсия или цикл в данныхОбход без рекурсии, контроль посещенных

Главное правило: дерево значений — объект для отображения и небольших структур. Считать большие объемы лучше в запросе, СКД или таблице значений.

Ленивая загрузка дерева в форме

Идея: при открытии формы загружаем только верхний уровень. Чтобы у групп отрисовался «плюс», добавляем им одну пустую строку-маркер. При раскрытии узла заменяем маркер реальными строками.

Первый уровень

1С
&НаСервере
Процедура ЗаполнитьВерхнийУровень()

	Корень = ДеревоНоменклатуры.ПолучитьЭлементы();
	Корень.Очистить();

	Для Каждого ДанныеСтроки Из ДочерниеЭлементы(Справочники.Номенклатура.ПустаяСсылка()) Цикл
		ДобавитьСтрокуДерева(Корень, ДанныеСтроки);
	КонецЦикла;

КонецПроцедуры

&НаКлиентеНаСервереБезКонтекста
Процедура ДобавитьСтрокуДерева(Коллекция, ДанныеСтроки)

	НоваяСтрока = Коллекция.Добавить();
	ЗаполнитьЗначенияСвойств(НоваяСтрока, ДанныеСтроки);
	Если ДанныеСтроки.ЭтоГруппа Тогда
		// Пустая строка-маркер: платформа нарисует «плюс» у группы.
		НоваяСтрока.ПолучитьЭлементы().Добавить();
	КонецЕсли;

КонецПроцедуры

Подгрузка при раскрытии узла

В событии таблицы ПередРазворачиванием параметр Строка — идентификатор строки. Проверяем, лежит ли под узлом только маркер, и получаем дочерние элементы внеконтекстным вызовом: на сервер уходит одна ссылка, а не вся форма с деревом.

1С
&НаКлиенте
Процедура ДеревоНоменклатурыПередРазворачиванием(Элемент, Строка, Отказ)

	Узел = ДеревоНоменклатуры.НайтиПоИдентификатору(Строка);
	Дочерние = Узел.ПолучитьЭлементы();

	ЕстьТолькоМаркер = Дочерние.Количество() = 1 И Не ЗначениеЗаполнено(Дочерние[0].Ссылка);
	Если Не ЕстьТолькоМаркер Тогда
		Возврат;
	КонецЕсли;

	Дочерние.Очистить();
	Для Каждого ДанныеСтроки Из ДочерниеЭлементы(Узел.Ссылка) Цикл
		ДобавитьСтрокуДерева(Дочерние, ДанныеСтроки);
	КонецЦикла;

КонецПроцедуры

&НаСервереБезКонтекста
Функция ДочерниеЭлементы(Родитель)

	Запрос = Новый Запрос;
	Запрос.Текст =
	"ВЫБРАТЬ
	|	Номенклатура.Ссылка КАК Ссылка,
	|	Номенклатура.Наименование КАК Наименование,
	|	Номенклатура.ЭтоГруппа КАК ЭтоГруппа
	|ИЗ
	|	Справочник.Номенклатура КАК Номенклатура
	|ГДЕ
	|	Номенклатура.Родитель = &Родитель
	|УПОРЯДОЧИТЬ ПО
	|	ЭтоГруппа УБЫВ,
	|	Наименование";
	Запрос.УстановитьПараметр("Родитель", Родитель);

	Результат = Новый Массив;
	Выборка = Запрос.Выполнить().Выбрать();
	Пока Выборка.Следующий() Цикл
		Результат.Добавить(Новый Структура("Ссылка, Наименование, ЭтоГруппа",
			Выборка.Ссылка, Выборка.Наименование, Выборка.ЭтоГруппа));
	КонецЦикла;

	Возврат Результат;

КонецФункции

Отличие от «наивной» реализации — именно &НаСервереБезКонтекста. Если подгружать узел контекстной серверной процедурой, платформа на каждый клик отправит на сервер и обратно все уже раскрытые ветки, и ленивая загрузка потеряет половину смысла.

Обход дерева значений без рекурсии

Рекурсия на дереве значений сама по себе не грех: для справочника глубиной 5–10 уровней она читается проще и работает нормально. Проблемы начинаются, когда:

  • глубина заранее не известна и может быть очень большой — растет стек вызовов, и процедура может упасть с ошибкой;
  • данные строятся по ссылкам, где возможен цикл (сотрудник указан руководителем своего руководителя) — рекурсия уходит в бесконечность;
  • внутри каждого вызова выполняется запрос — тормозит уже не рекурсия, а запросы.

Обход в глубину через стек

1С
// Обходит все строки дерева без рекурсии (в глубину).
Процедура ОбойтиДерево(Дерево) Экспорт

	Стек = Новый Массив;
	Для Каждого СтрокаДерева Из Дерево.Строки Цикл
		Стек.Добавить(СтрокаДерева);
	КонецЦикла;

	Пока Стек.Количество() > 0 Цикл

		ПоследнийИндекс = Стек.ВГраница();
		ТекущаяСтрока = Стек[ПоследнийИндекс];
		Стек.Удалить(ПоследнийИндекс);

		ОбработатьСтроку(ТекущаяСтрока);

		Для Каждого Подчиненная Из ТекущаяСтрока.Строки Цикл
			Стек.Добавить(Подчиненная);
		КонецЦикла;

	КонецЦикла;

КонецПроцедуры

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

Обход по уровням (в ширину)

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

Дерево значений 1С: обход по уровням и раскладка в плоскую таблицу
Дерево значений 1С: обход по уровням и раскладка в плоскую таблицу

Пример — разложить дерево подчиненности «руководитель → сотрудники» в плоскую таблицу пар со всеми вышестоящими руководителями и уровнями:

1С
// Раскладывает дерево подчиненности в таблицу пар «руководитель — сотрудник»
// по всем уровням иерархии, без рекурсии.
//
// Параметры:
//   Дерево - ДеревоЗначений - с колонкой Сотрудник.
//
// Возвращаемое значение:
//   ТаблицаЗначений - Руководитель, Сотрудник, УровеньРуководителя, УровеньСотрудника.
//
Функция ПарыРуководительСотрудник(Знач Дерево) Экспорт

	Результат = Новый ТаблицаЗначений;
	Результат.Колонки.Добавить("Руководитель");
	Результат.Колонки.Добавить("Сотрудник");
	Результат.Колонки.Добавить("УровеньРуководителя", Новый ОписаниеТипов("Число"));
	Результат.Колонки.Добавить("УровеньСотрудника", Новый ОписаниеТипов("Число"));

	ТекущийУровень = Новый Массив;
	Для Каждого СтрокаДерева Из Дерево.Строки Цикл
		ТекущийУровень.Добавить(СтрокаДерева);
	КонецЦикла;

	Пока ТекущийУровень.Количество() > 0 Цикл

		// Здесь можно одним запросом получить данные для всего уровня.

		СледующийУровень = Новый Массив;
		Для Каждого СтрокаДерева Из ТекущийУровень Цикл

			Если ЗначениеЗаполнено(СтрокаДерева.Сотрудник) Тогда
				Родитель = СтрокаДерева.Родитель;
				Пока Родитель <> Неопределено Цикл
					НоваяПара = Результат.Добавить();
					НоваяПара.Руководитель = Родитель.Сотрудник;
					НоваяПара.Сотрудник = СтрокаДерева.Сотрудник;
					НоваяПара.УровеньРуководителя = Родитель.Уровень();
					НоваяПара.УровеньСотрудника = СтрокаДерева.Уровень();
					Родитель = Родитель.Родитель;
				КонецЦикла;
			КонецЕсли;

			Для Каждого Подчиненная Из СтрокаДерева.Строки Цикл
				СледующийУровень.Добавить(Подчиненная);
			КонецЦикла;

		КонецЦикла;

		ТекущийУровень = СледующийУровень;

	КонецЦикла;

	Возврат Результат;

КонецФункции

Когда дерево строится не из ДеревоЗначений, а по ссылкам (Руководитель в справочнике сотрудников), заведите Соответствие уже посещенных элементов и пропускайте повторы. Это одна строка кода, которая спасает от бесконечного цикла на «кривых» данных.

Поиск в дереве значений

  • Разовый поиск: Дерево.Строки.НайтиСтроки(Новый Структура("Ссылка", Значение), Истина). Второй параметр включает поиск по подчиненным строкам на всех уровнях. Перебор выполняет платформа, а не интерпретатор, поэтому это быстрее ручного цикла.
  • Много поисков подряд: один раз обойдите дерево и соберите Соответствие «ключ — строка дерева». Дальше каждый поиск — обращение по ключу.
  • Только первое совпадение: Найти(Значение, "Колонка", Истина) у коллекции строк дерева.

Не ищите в дереве внутри цикла по другой большой коллекции через НайтиСтроки — это квадратичная сложность. Соответствие здесь выигрывает всегда.

Строим дерево средствами платформы

Самая быстрая ручная сборка дерева — та, которой нет.

Запрос с итогами. Если нужна иерархия с итогами, используйте секцию ИТОГИ ПО и выгрузку результата сразу в дерево:

Запрос 1С
ВЫБРАТЬ
	Продажи.Номенклатура КАК Номенклатура,
	СУММА(Продажи.Сумма) КАК Сумма
ИЗ
	РегистрНакопления.Продажи КАК Продажи
СГРУППИРОВАТЬ ПО
	Продажи.Номенклатура
ИТОГИ
	СУММА(Сумма)
ПО
	Номенклатура ИЕРАРХИЯ
1С
Дерево = Запрос.Выполнить().Выгрузить(ОбходРезультатаЗапроса.ПоГруппировкамСИерархией);

СКД. Для сложных отчетов с несколькими уровнями группировок настройте структуру в схеме компоновки и выведите результат в ДеревоЗначений через ПроцессорВыводаРезультатаКомпоновкиДанныхВКоллекциюЗначений (макет строится генератором для коллекции значений). Группировки, итоги и иерархию соберет платформа.

Оформление. Вместо сложного условного оформления с вложенными И/ИЛИ по нескольким колонкам рассчитайте в данных одну колонку-признак (например, индекс цвета) и сделайте по ней простое условие. Отрисовка большого дерева от этого заметно легче.

Типичные ошибки

  • Заполнение всего справочника в дерево формы при открытии.
  • Подгрузка узла контекстным серверным вызовом.
  • Проверка маркера как Дочерние.Ссылка.Пустая() — у коллекции нет поля Ссылка, проверять нужно первую строку.
  • Запрос внутри рекурсивной процедуры на каждый узел.
  • Поиск НайтиСтроки без второго параметра — ищет только на верхнем уровне.
  • Отсутствие защиты от циклов при построении иерархии по ссылкам.

Если формы и отчеты с иерархиями работают медленно, начните с замера: часто узкое место не в дереве, а в запросах вокруг него. Такие разборы я провожу в рамках аудита производительности 1С, а переделку отчетов на СКД — в услуге отчеты на СКД.

Коротко

  • Дерево значений 1С в форме тормозит из-за передачи целиком: грузите узлы при раскрытии и вызывайте сервер без контекста.
  • Обход без рекурсии — массив-стек или очередь уровней; для данных по ссылкам добавьте контроль посещенных.
  • Ищите через НайтиСтроки(..., Истина), а при массовом поиске — через соответствие.
  • Иерархию с итогами стройте запросом с ИТОГИ ПО или СКД, а не вручную.