Как ускорить медленную свертку и развертку больших деревьев на управляемой форме 1С?

Программист 1С v8.3 (Управляемые формы) IT и автоматизация бизнеса
← На главную

При работе с большими объемами данных в 1С программисты часто сталкиваются с критическим падением производительности интерфейса управляемых форм. Типичный сценарий: на форме отображается дерево значений, содержащее от нескольких тысяч до десятков тысяч строк. При попытке программно свернуть или развернуть это дерево до определенного уровня (особенно с использованием метода Развернуть() в цикле на клиенте) система зависает. Замер производительности показывает, что 99% времени уходит именно на выполнение метода развертывания, сопровождаясь неявными серверными вызовами и перерисовкой интерфейса.

В этой статье мы подробно разберем причины возникновения таких тормозов и рассмотрим эффективные практические способы оптимизации работы с большими иерархическими структурами в управляемом приложении 1С.

В чем кроется причина медленной работы?

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

  1. Накладные расходы на синхронизацию данных. Объект на форме связан с типом данных ДанныеФормыДерево. Когда мы вызываем метод Развернуть() на клиенте для множества строк, платформа вынуждена выполнять микро-синхронизации состояния элементов между клиентом и сервером. Множественные неявные вызовы парализуют поток выполнения интерфейса.
  2. Получение текстовых представлений ссылок. Если в колонках дерева находятся ссылочные типы данных (например, ссылки на элементы справочников или документов), то при визуализации и разворачивании узлов платформа начинает неявно запрашивать у сервера их текстовые представления. Это порождает лавину мелких сетевых запросов.
  3. Высокие затраты на рендеринг (перерисовку) интерфейса. В управляемой форме 1С отсутствует стандартный метод «заморозки» обновления экрана. Каждое изменение состояния отображения строк дерева заставляет веб-клиент или тонкий клиент пересчитывать геометрию элементов формы.

Решение 1. Реализация ленивой (отложенной) загрузки данных (Lazy Loading)

Это наиболее профессиональный и архитектурно правильный паттерн для работы с большими деревьями. Вместо того чтобы сразу строить и передавать на клиентское устройство все «тяжелое» дерево целиком, мы будем подгружать данные порциями — строго в тот момент, когда пользователь решит развернуть конкретный узел.

Рассмотрим по шагам, как реализовать этот алгоритм:

Шаг 1. Первичное заполнение дерева на сервере. При открытии формы или по команде заполнения мы строим только корневые элементы (верхний уровень). Для каждого узла, у которого потенциально могут быть подчиненные элементы, мы программно добавляем одну пустую строку-маркер.


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

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


&НаКлиенте
Процедура ДеревоДанныхПередРазворачиванием(Элемент, СтрокаИдентификатор, Отказ)
    
    ТекущийУзел = ДеревоДанных.НайтиПоИдентификатору(СтрокаИдентификатор);
    ПодчиненныеСтроки = ТекущийУзел.ПолучитьЭлементы();
    
    // Проверяем, содержит ли узел маркер отложенной загрузки
    Если ПодчиненныеСтроки.Количество() = 1 И ПодчиненныеСтроки[0].ЭтоМаркер Тогда
        
        // Отменяем стандартное интерактивное разворачивание на мгновение
        Отказ = ИСТИНА;
        
        // Загружаем подчиненные элементы с сервера без передачи контекста всей формы (для скорости)
        ПодгрузитьДанныеУзлаНаСервере(СтрокаИдентификатор, ТекущийУзел.Ссылка);
        
        // Разворачиваем узел, теперь уже наполненный реальными данными
        Элементы.ДеревоДанных.Развернуть(СтрокаИдентификатор);
        
    КонецЕсли;
    
КонецПроцедуры

Шаг 3. Быстрая подгрузка данных на сервере. Нам необходимо точечно модифицировать коллекцию ДанныеФормыДерево на сервере без перезагрузки всей структуры.


&НаСервере
Процедура ПодгрузитьДанныеУзлаНаСервере(Знач ИдентификаторРодителя, Знач СсылкаРодитель)
    
    РодительскаяСтрокаФормы = ДеревоДанных.НайтиПоИдентификатору(ИдентификаторРодителя);
    КоллекцияПодчиненных = РодительскаяСтрокаФормы.ПолучитьЭлементы();
    
    // Очищаем маркер "Загрузка данных..."
    КоллекцияПодчиненных.Очистить();
    
    // Выбираем только элементы следующего уровня для данного родителя
    Запрос = Новый Запрос;
    Запрос.Текст = 
        "ВЫБРАТЬ
        |	Номенклатура.Ссылка КАК Ссылка,
        |	Номенклатура.Наименование КАК Наименование,
        |	Номенклатура.ЭтоГруппа КАК ЭтоГруппа
        |ИЗ
        |	Справочник.Номенклатура КАК Номенклатура
        |ГДЕ
        |	Номенклатура.Родитель = &Родитель
        |	И Номенклатура.ПометкаУдаления = ЛОЖЬ";
    
    Запрос.УстановитьПараметр("Родитель", СсылкаРодитель);
    Выборка = Запрос.Выполнить().Выбрать();
    
    Пока Выборка.Следующий() Цикл
        НоваяСтрока = КоллекцияПодчиненных.Добавить();
        ЗаполнитьЗначенияСвойств(НоваяСтрока, Выборка);
        
        // Если новый элемент тоже группа, вешаем на него маркер для последующих уровней глубины
        Если Выборка.ЭтоГруппа Тогда
            СтрокаМаркер = НоваяСтрока.ПолучитьЭлементы().Добавить();
            СтрокаМаркер.Наименование = "Загрузка данных...";
            СтрокаМаркер.ЭтоМаркер = ИСТИНА;
        КонецЕсли;
    КонецЦикла;
    
КонецПроцедуры

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


Решение 2. Оптимизация получения представлений (отказ от ссылочных типов в дереве)

Если в вашем дереве присутствуют колонки, содержащие непосредственно объекты ссылочных типов (например, СправочникСсылка.Контрагенты, ДокументСсылка.РеализацияТоваровУслуг), то при отрисовке строк на клиенте платформа 1С совершает неявные запросы к серверу за представлениями этих ссылок.

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


Решение 3. Фоновое асинхронное развертывание дерева через обработчик ожидания

Если бизнес-логика задачи все же требует развернуть всё многотысячное дерево до определенного уровня за раз, стандартный программный обход в один проход "намертво" заблокирует интерфейс. Чтобы избежать этого, применим асинхронный подход с использованием метода ПодключитьОбработчикОжидания().

Рассмотрим пример реализации фонового развертывания порциями:


&НаКлиенте
Перем МассивСтрокДляРазвертывания; // Хранит массив идентификаторов строк дерева

&НаКлиенте
Процедура КомандаРазвернутьВсеДерево(Команда)
    
    МассивСтрокДляРазвертывания = Новый Массив;
    
    // Заполняем массив идентификаторами строк, требующих развертывания
    СобратьИдентификаторыСтрок(ДеревоДанных.ПолучитьЭлементы());
    
    Если МассивСтрокДляРазвертывания.Количество() > 0 Тогда
        // Подключаем однократный обработчик с интервалом 0.1 секунды
        ПодключитьОбработчикОжидания("ВыполнитьФоновоеРазвертывание", 0.1, Ложь);
    КонецЕсли;
    
КонецПроцедуры

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

&НаКлиенте
Процедура ВыполнитьФоновоеРазвертывание()
    
    // Задаем размер порции, чтобы интерфейс успевал обрабатывать действия пользователя
    РазмерПорции = 100; 
    
    Для Итератор = 1 По РазмерПорции Цикл
        Если МассивСтрокДляРазвертывания.Количество() = 0 Тогда
            ОтключитьОбработчикОжидания("ВыполнитьФоновоеРазвертывание");
            ПоказатьОповещениеПользователя("Готово", , "Развертывание дерева завершено!");
            Возврат;
        КонецЕсли;
        
        ИдентификаторСтроки = МассивСтрокДляРазвертывания[0];
        МассивСтрокДляРазвертывания.Удалить(0);
        
        // Разворачиваем конкретную строку дерева
        Элементы.ДеревоДанных.Развернуть(ИдентификаторСтроки);
    КонецЦикла;
    
КонецПроцедуры

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


Решение 4. Разделение интерфейса: Дерево групп + Таблица элементов

Часто проблема кроется в неоптимальном проектировании интерфейса (UX/UI). Зачем заставлять форму отрисовывать десятки тысяч строк в одной таблице, если пользователь физически не способен воспринять такой объем информации одновременно?

Проанализируем альтернативный вариант разделения данных:

  1. На форму выводятся два элемента управления рядом: слева — Дерево групп (оно содержит только группировочные узлы верхних уровней, объем которых редко превышает 100-200 строк), справа — Таблица элементов (обычная плоская таблица значений или динамический список).
  2. В дереве групп пользователь быстро навигируется по разделам.
  3. В событии дерева ПриАктивизацииСтроки на клиенте считывается ссылка текущей выбранной группы, и на нее накладывается быстрый отбор в правой таблице.

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


Решение 5. Переход на Динамический список

Если дерево значений используется в вашей форме в основном для просмотра и навигации, самым верным технологическим решением будет полный отказ от дерева в пользу ДинамическийСписок с включенным свойством Отображение = Дерево.

Преимущества динамического списка перед классическим деревом значений:


Полезные советы по настройке свойств дерева

Для минимизации зависаний обратите внимание на следующие параметры настройки элементов управляемой формы:

Применение описанных архитектурных приемов и методов оптимизации позволит вам полностью избавиться от зависаний интерфейса и гарантирует быструю и комфортную работу пользователей с иерархическими структурами любого объема в системе 1С Предприятие.

← На главную