Как оптимизировать медленный обход коллекции ДанныеФормыКоллекция на клиенте

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

Разработка интерфейсов на управляемых формах в «1С:Предприятии» накладывает жесткие ограничения на работу с большими массивами данных. Типичная ситуация: разработчик создает обработчик нажатия кнопки на клиенте, в котором пытается обойти строки табличной части документа для выполнения несложных расчетов. Если строк в таблице относительно немного (до 100), код отрабатывает мгновенно. Однако при росте объема данных до 10 000 строк и более простейший цикл начинает фатально тормозить, зависая на 10–15 секунд или даже полностью блокируя интерфейс.

Давайте подробно проанализируем, почему так происходит, как устроено клиент-серверное взаимодействие под капотом платформы «1С:Предприятие», и рассмотрим эффективные практические сценарии оптимизации таких расчетов.

Выясняем причину: почему обход на клиенте работает медленно

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

Объект ДанныеФормыКоллекция (которым на клиенте представлена табличная часть) реализует так называемую порционную (ленивую) загрузку данных с сервера. Платформа 1С оптимизирует потребление памяти и сетевой трафик: на клиент передаются только те строки, которые необходимы для текущего отображения на экране (обычно это порядка 20–40 видимых строк таблицы).

Когда мы запускаем цикл обхода на клиенте:


Для Каждого СтрокаРасход Из Объект.Расход Цикл
    // Выполнение вычислений
КонецЦикла;

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

Аналогично ведет себя метод НайтиСтроки(), вызванный на клиенте. Чтобы выполнить поиск, платформа вынуждена в один присест выгрузить всю таблицу с сервера на клиент, что порождает один тяжелый неявный серверный вызов, способный надолго «завесить» сеанс пользователя.

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

Решение 1. Перенос расчетов на сторону сервера (Рекомендуемый стандарт)

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

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

Если нам необходимо рассчитать итог по колонке для всей таблицы, мы пишем следующий код:


&НаКлиенте
Процедура РассчитатьИтоги(Команда)
    
    // Вызываем серверную функцию для быстрого расчета
    ИдентификаторПоиска = Объект.ИдентификаторСтроки;
    РезультатСуммы = РассчитатьСуммуНаСервере(ИдентификаторПоиска);
    
    // Выводим результат на форму
    Объект.ИтоговаяСумма = РезультатСуммы;
    
КонецПроцедуры

&НаСервереБезContext
Функция РассчитатьСуммуНаСервере(Знач ИдентификаторПоиска)
    
    // Здесь мы не тащим контекст формы.
    // Если данные уже сохранены в ИБ, мы можем быстро получить их запросом.
    // Если данные новые и есть только на форме, используем контекстный вызов &НаСервере.
    
КонецФункции

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


&НаКлиенте
Процедура ВыполнитьРасчет(Команда)
    РассчитатьНаСервере();
КонецПроцедуры

&НаСервере
Процедура РассчитатьНаСервере()
    
    СуммаРасход = 0;
    ТекущийИдентификатор = Объект.ИдентификаторСтроки;
    
    // На сервере этот обход выполнится за миллисекунды!
    Для Каждого СтрокаРасход Из Объект.Расход Цикл
        Если СтрокаРасход.ИдентификаторСтроки = ТекущийИдентификатор Тогда
            СуммаРасход = СуммаРасход + СтрокаРасход.Сумма;
        КонецЕсли;
    КонецЦикла;
    
    Объект.ВсегоПоРасходам = СуммаРасход;
    
КонецПроцедуры

Решение 2. Кэширование данных на клиенте через структуры

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

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

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

  1. Объявляем переменную кэша в самом верху модуля формы с директивой &НаКлиенте.
  2. При открытии формы один раз считываем данные табличной части на сервере, преобразуем их в легкий массив структур и передаем на клиент.
  3. При необходимости расчета производим поиск по нашему кэшу прямо на клиенте без обращений к серверу.
  4. При изменении строк пользователем точечно обновляем наш кэш.

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


&НаКлиенте
Перем КешРасходов; // Глобальная переменная модуля формы на клиенте

&НаКлиенте
Процедура ПриОткрытии(Отказ)
    // Инициализируем кэш при запуске формы
    ЗаполнитьКэшДанных();
КонецПроцедуры

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

&НаКлиенте
Процедура ПоместитьКэшНаКлиент(СписокКэша)
    КешРасходов = СписокКэша;
КонецПроцедуры

&НаКлиенте
Процедура БыстрыйРасчетНаКлиенте(Команда)
    
    СуммаРасход = 0;
    ИскомыйИдентификатор = Объект.ТекущийИдентификатор;
    
    // Быстрый обход локального массива структур без серверных вызовов!
    Для Каждого СтрокаКэша Из КешРасходов Цикл
        Если СтрокаКэша.ИдентификаторСтроки = ИскомыйИдентификатор Тогда
            СуммаРасход = СуммаРасход + СтрокаКэша.Сумма;
        КонецЕсли;
    КонецЦикла;
    
    Объект.РезультатРасчета = СуммаРасход;
    
КонецПроцедуры

Этот способ требует от разработчика аккуратности: если пользователь добавляет, изменяет или удаляет строки в табличной части на форме, необходимо настроить соответствующие события таблицы (ПриИзменении, ПередУдалением) и синхронно обновлять данные в нашей переменной КешРасходов. Тем не менее для экстремально нагруженных интерфейсов этот метод дает колоссальный выигрыш в скорости отклика.

Решение 3. Настройка свойства «Использовать всегда» для оптимизации открытия формы

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

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

Чтобы ускорить инициализацию и открытие формы с большими табличными частями, выполните следующие шаги:

  1. Откройте редактор управляемой формы в конфигураторе.
  2. Перейдите на вкладку «Реквизиты».
  3. Найдите вашу табличную часть внутри реквизита Объект.
  4. В панели свойств снимите флаг «Использовать всегда» (по умолчанию там установлено "Авто" или "Истина").

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

Резюме и лучшие практики

Подводя итог, сформулируем золотые правила оптимизации работы с большими таблицами в управляемых формах 1С:

Соблюдение этих простых правил проектирования клиент-серверного взаимодействия позволит вашим решениям работать стабильно и быстро независимо от объема данных в документах пользователей.

← На главную