Как устранить задержки (лаги) при активизации строки динамического списка в 1С?

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

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

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

Почему идея кэширования всей таблицы на сервере — это антипаттерн?

Изначально у разработчиков возникает мысль: "Давайте при открытии формы прочитаем весь регистр сведений в Таблицу Значений (ТЗ) на сервере, проиндексируем её по измерению Документ, и при активизации строки будем быстро искать данные в этой ТЗ без обращения к СУБД". Чтобы поддерживать ТЗ в актуальном состоянии, предлагается хранить время последнего изменения в константе и периодически сверять его.

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

  1. Избыточное потребление памяти: Если регистр содержит десятки или сотни тысяч записей, ТЗ займет значительный объем оперативной памяти на сервере приложений (процесс rphost) под каждый сеанс пользователя. При одновременной работе многих пользователей сервер быстро исчерпает физическую память.
  2. Длинная инициализация формы: При открытии формы пользователю придется ждать, пока весь регистр сведений считается из БД и запишется в ТЗ. Вместо секундных лагов при прокрутке пользователь получит зависание формы на 10–15 секунд при её открытии.
  3. Сложная и ненадежная синхронизация: Механизм отслеживания изменений через константы усложняет код, создает лишние транзакции и СУБД-блокировки при частой записи данных в регистр.

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

Решение 1. Использование обработчика ожидания (Debouncing)

Это самый простой и надежный способ защитить сервер от "шквала" запросов при быстром пролистывании строк. Суть метода заключается в том, что мы не отправляем запрос на сервер мгновенно. Вместо этого мы делаем небольшую паузу (например, 0.2–0.3 секунды). Если пользователь за это время переключился на другую строку, мы отменяем старый запрос и планируем новый. Серверный вызов сработает только тогда, когда пользователь остановится на конкретной строке.

Разберем по шагам реализацию этого механизма в модуле управляемой формы:


&НаКлиенте
Процедура СписокДокументовПриАктивизацииСтроки(Элемент)
    
    // Сбрасываем ранее запланированный обработчик ожидания
    ОтключитьОбработчикОжидания("ВыполнитьОбновлениеИсторииДокумента");
    
    // Запускаем новый обработчик с задержкой в 0.2 секунды.
    // Параметр "Однократно" устанавливаем в Истина.
    ПодключитьОбработчикОжидания("ВыполнитьОбновлениеИсторииДокумента", 0.2, Истина);
    
КонецПроцедуры

&НаКлиенте
Процедура ВыполнитьОбновлениеИсторииДокумента()
    
    ТекущиеДанные = Элементы.СписокДокументов.ТекущиеДанные;
    Если ТекущиеДанные = Неопределено Тогда
        Возврат;
    КонецЕсли;
    
    // Данный код выполнится только после того, как пользователь
    // прекратит быстрое перемещение по списку и задержится на строке на 0.2 сек.
    ОбновитьИсториюНаСервере(ТекущиеДанные.Ссылка);
    
КонецПроцедуры

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

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

Решение 2. Использование связанного динамического списка (Master-Detail)

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

Рассмотрим порядок настройки этого решения:

  1. Добавим на форму новый реквизит СписокИстория с типом ДинамическийСписок.
  2. В свойствах списка включим "Произвольный запрос" и напишем следующий текст:
    
    ВЫБРАТЬ
        История.Период КАК Период,
        История.Пользователь КАК Пользователь,
        История.Статус КАК Статус,
        История.Документ КАК Документ
    ИЗ
        РегистрСведений.ИсторияРаботы КАК История
    ГДЕ
        История.Документ = &ТекущийДокумент
    
  3. Вынесем этот список на форму в качестве таблицы.
  4. В событии активизации строки левого списка будем просто менять значение параметра динамического списка без явных серверных вызовов:

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

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

Решение 3. Локальный кэш на Тонком клиенте

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

Проанализируем пример кода, реализующий локальный кэш на форме:


&НаКлиенте
Перем ЛокальныйКэшИстории; // Переменная модуля для хранения кэша

&НаКлиенте
Процедура ПриОткрытии(Отказ)
    // Инициализируем соответствие при открытии формы
    ЛокальныйКэшИстории = Новый Соответствие;
КонецПроцедуры

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

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

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

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

Решение 4. Хранение промежуточного кэша во Временном хранилище

Вернемся к вопросу автора: "Где хранить таблицу значений, чтобы иметь к ней доступ в процедуре &НаСервереБезКонтекста?"

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

Посмотрим на пример реализации этой схемы:


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

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

Этот метод решает задачу автора, сохраняя высокую скорость работы за счет отсутствия передачи контекста формы (который может весить очень много) при вызовах &НаСервереБезКонтекста.

Резюме: какую стратегию выбрать?

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

← На главную