Как оптимизировать тяжелые расчеты при получении данных в динамическом списке 1С?

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

Разработчики систем на платформе «1С:Предприятие 8» часто сталкиваются с проблемой производительности при выводе расчетных данных в динамических списках (ДС). Типичный сценарий: в списке документов или справочников требуется отобразить дополнительную виртуальную колонку, данные для которой вычисляются динамически (например, статус связанных документов, сложные остатки, актуальное состояние бизнес-процесса). Если алгоритм расчета сложный, то при скроллинге или обновлении списка интерфейс начинает ощутимо зависать на несколько секунд. Это происходит из-за того, что обработчик события ПриПолученииДанныхНаСервере выполняется синхронно в контексте серверного вызова отрисовки формы.

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

Решение 1. Классическая оптимизация кода внутри обработчика

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

Проанализируем правильный шаблон реализации обработчика. Нам нужно собрать идентификаторы только тех строк, которые в данный момент реально отображаются на экране пользователя (обычно это не более 30–50 строк), выполнить один пакетный запрос к базе данных и быстро распределить полученные значения.

Рассмотрим пример кода:


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

Использование этого паттерна минимизирует количество обращений к СУБД, сводя их к одному быстрому запросу. Все поля, по которым происходит соединение или отбор в запросе (в примере это Ссылка), обязательно должны быть проиндексированы.

Решение 2. Реализация асинхронных расчетов через фоновые задания

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

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

Разберем этот алгоритм по шагам:

  1. При открытии формы или изменении отборов динамического списка запускается фоновое задание, выполняющее расчет для всего пула данных или первой видимой порции.
  2. На клиенте подключается обработчик ожидания, который с заданной периодичностью (например, раз в 1.5–2 секунды) проверяет статус фонового задания.
  3. В процессе работы фоновое задание помещает результат расчетов (таблицу соответствия Ссылка -> РезультатРасчета) во Временное хранилище.
  4. После успешного завершения фонового задания клиентская форма забирает адрес временного хранилища и передает его в параметры динамического списка.
  5. Программа принудительно обновляет динамический список с помощью вызова Элементы.Список.Обновить().
  6. Внутри обработчика ПриПолученииДанныхНаСервере данные мгновенно считываются из кэша (временного хранилища), минуя обращения к тяжелым запросам БД.

Рассмотрим программную реализацию этого подхода:


// В модуле формы

&НаКлиенте
Процедура ПриОткрытии(Отказ)
    ЗапуститьФоновыйРасчетДанных();
КонецПроцедуры

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

&НаКлиенте
Процедура Подключаемый_ПроверитьВыполнениеЗадания()
    
    Если ЗаданиеВыполнено(ИдентификаторЗадания) Тогда
        ОтключитьОбработчикОжидания("Подключаемый_ПроверитьВыполнениеЗадания");
        
        // Передаем адрес кэша в параметры динамического списка для быстрого считывания
        Список.Параметры.УстановитьПараметрЗначение("АдресКэшаДанных", АдресХранилищаКэша);
        
        // Принудительно обновляем список для отображения рассчитанных данных
        Элементы.Список.Обновить();
    КонецЕсли;
    
КонецПроцедуры

Благодаря такой схеме пользователь видит форму мгновенно. Сначала колонка может быть пустой или содержать статус "Расчет...", а по мере готовности фонового процесса значения автоматически подгружаются в интерфейс без единого зависания.

Решение 3. Кэширование данных на форме через настройки компоновщика

Часто возникает проблема: событие ПриПолученииДанныхНаСервере выполняется в неконтекстном вызове, поэтому мы не можем напрямую обратиться к реквизитам формы для сохранения промежуточных результатов расчета (кэша). Из-за этого при каждом скроллинге вверх-вниз платформа заново рассчитывает данные для одних и тех же строк.

Чтобы обойти это платформенное ограничение, мы можем использовать «лайфхак» с хранением кэша в настройках компоновщика данных динамического списка. Свойство Список.КомпоновщикНастроек.Настройки.ДополнительныеСвойства доступно как на чтение, так и на запись внутри обработчика получения данных.

Проанализируем пример кэширования:


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

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

Решение 4. Архитектурное решение: Денормализация (Регистр сведений)

Если расчет по-настоящему тяжелый (например, дерево связанных документов, сложные остатки по многим регистрам с учетом резервов и т.д.), то выполнять его «на лету» при каждом открытии списка — архитектурно неверно. Попытки оптимизировать код или настроить кэширование на форме лишь замаскируют проблему, но не решат ее радикально.

Наиболее правильным и масштабируемым решением является денормализация данных:

Посмотрим на пример текста запроса динамического списка после денормализации:


ВЫБРАТЬ
    ДокументПродажи.Ссылка КАК Ссылка,
    ДокументПродажи.Дата КАК Дата,
    ДокументПродажи.Номер КАК Номер,
    ЕСТЬNULL(КэшСтатусов.АктуальныйСтатус, "Не рассчитан") КАК ВиртуальныйСтатус
ИЗ
    Документ.РеализацияТоваровУслуг КАК ДокументПродажи
    ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.КэшСтатусовДокументов КАК КэшСтатусов
        ПО КэшСтатусов.Документ = ДокументПродажи.Ссылка

При таком подходе СУБД считывает уже готовые, предрассчитанные значения из индексированной таблицы регистра. Отрисовка списка происходит мгновенно, даже если в базе данных содержатся миллионы записей.

Решение 5. Расчет данных по требованию (UI/UX подход)

Часто пользователю не требуется видеть рассчитанные данные по всем строкам одновременно при открытии списка. Хорошей практикой является вынос тяжелого расчета на ручное управление:

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

Решение 6. Переход на собственную пагинацию через Таблицу Значений

В исключительных случаях, когда логика динамического списка накладывает жесткие ограничения, а бизнес-требования не позволяют использовать денормализацию, можно отказаться от использования стандартного динамического списка. Альтернативой выступает обычная ТаблицаЗначений, размещенная на форме, с реализованным вручную механизмом пагинации (постраничного вывода).

При этом разработчик самостоятельно управляет запросами к базе данных, ограничивая выборку жестким лимитом (например, ровно по 50 записей с использованием конструкции ВЫБРАТЬ ПЕРВЫЕ 50... и смещением по дате или коду). Расчет тяжелых колонок в данном случае происходит порционно только для выбранной страницы таблицы значений, что гарантирует полный контроль над скоростью работы интерфейса.

← На главную